File: RuntimeTestHarness.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (173 lines) | stat: -rw-r--r-- 7,084 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import Foundation
import WasmKit
import WASI
import WIT
import XCTest
@testable import WITOverlayGenerator

/// This harness expects the following directory structure:
///
/// ```
/// |- Fixtures
/// |  |- ${TEST_CASE}
/// |     |- ${TEST_CASE}.swift
/// |     |- wit
/// |        |- ${WORLD}.wit
/// ```
struct RuntimeTestHarness {
    struct Configuration: Codable {
        let swiftExecutablePath: URL

        var swiftCompilerExecutablePath: URL {
            swiftExecutablePath.deletingLastPathComponent().appendingPathComponent("swiftc")
        }

        static let `default`: Configuration? = {
            let decoder = JSONDecoder()
            let defaultsPath = RuntimeTestHarness.testsDirectory
                .deletingLastPathComponent()
                .appendingPathComponent("default.json")
            guard let bytes = try? Data(contentsOf: defaultsPath) else { return nil }
            return try? decoder.decode(Configuration.self, from: bytes)
        }()
    }

    struct Error: Swift.Error, CustomStringConvertible {
        let description: String
    }

    let fixturePath: URL
    var fixtureName: String { fixturePath.lastPathComponent }
    let configuration: Configuration
    let fileManager: FileManager
    var temporaryFiles: [String] = []

    init(
        fixture: String,
        configuration: Configuration? = .default,
        fileManager: FileManager = .default
    ) throws {
        self.fixturePath = RuntimeTestHarness.testsDirectory
            .appendingPathComponent("Fixtures").appendingPathComponent(fixture)
        guard let configuration else {
            throw XCTSkip("""
            Please create 'Tests/default.json' with this or similar contents:
            {
                "swiftExecutablePath": "/Library/Developer/Toolchains/swift-wasm-5.8.0-RELEASE.xctoolchain/usr/bin/swift"
            }

            or specify `configuration` parameter in your test code.
            """)
        }
        self.configuration = configuration
        self.fileManager = fileManager
    }

    static let testsDirectory: URL =  URL(fileURLWithPath: #filePath)
        .deletingLastPathComponent() // Runtime
        .deletingLastPathComponent() // WITOverlayGeneratorTests

    static let sourcesDirectory: URL = testsDirectory
        .deletingLastPathComponent() // Tests
        .deletingLastPathComponent() // Package root
        .appendingPathComponent("Sources")

    static func createTemporaryFile(suffix: String = "") -> String {
        let tempdir = URL(fileURLWithPath: NSTemporaryDirectory())
        let templatePath = tempdir.appendingPathComponent("WasmKit.XXXXXX\(suffix)")
        var template = [UInt8](templatePath.path.utf8).map { Int8($0) } + [Int8(0)]
        let fd = mkstemps(&template, Int32(suffix.utf8.count))
        if fd == -1 {
            fatalError("Failed to create temp directory")
        }
        return String(cString: template)
    }

    private mutating func createTemporaryFile(suffix: String = "") -> String {
        let created = Self.createTemporaryFile(suffix: suffix)
        self.temporaryFiles.append(created)
        return created
    }

    private mutating func cleanupTemporaryFiles() {
        temporaryFiles.forEach {
            try! fileManager.removeItem(atPath: $0)
        }
        temporaryFiles = []
    }

    private mutating func collectGuestInputFiles() throws -> [String] {
        let implFile = fixturePath.appendingPathComponent(fixtureName + ".swift")

        let (mainPackage, packageResolver) = try PackageResolver.parse(
            directory: fixturePath.appendingPathComponent("wit").path,
            loader: LocalFileLoader()
        )
        let context = SemanticsContext(rootPackage: mainPackage, packageResolver: packageResolver)
        let guestContent = try WITOverlayGenerator.generateGuest(context: context)
        let generatedFile = Self.testsDirectory.appendingPathComponent("Generated")
            .appendingPathComponent(fixtureName + "GeneratedTargetOverlay.swift")
        try FileManager.default.createDirectory(
            at: generatedFile.deletingLastPathComponent(),
            withIntermediateDirectories: true
        )
        try guestContent.write(to: generatedFile, atomically: true, encoding: .utf8)
        return [implFile.path, generatedFile.path]
    }

    /// Build up WebAssembly module from the fixture and instantiate WasmKit runtime with the module.
    mutating func build(link: (inout [String: HostModule]) -> Void) throws -> (Runtime, ModuleInstance) {
        defer { cleanupTemporaryFiles() }
        let compiled = try compile(inputFiles: collectGuestInputFiles())

        let wasi = try WASIBridgeToHost(args: [compiled.path])
        var hostModules: [String: HostModule] = wasi.hostModules
        link(&hostModules)

        let module = try parseWasm(filePath: .init(compiled.path))
        let runtime = Runtime(hostModules: hostModules)
        let instance = try runtime.instantiate(module: module)
        return (runtime, instance)
    }

    /// Compile the given input Swift source files into core Wasm module
    func compile(inputFiles: [String]) throws -> URL {
        let outputPath = Self.testsDirectory
            .appendingPathComponent("Compiled")
            .appendingPathComponent("\(fixtureName).core.wasm")
        try fileManager.createDirectory(at: outputPath.deletingLastPathComponent(), withIntermediateDirectories: true)
        let process = Process()
        process.launchPath = configuration.swiftCompilerExecutablePath.path
        process.arguments = inputFiles + [
            "-target", "wasm32-unknown-wasi",
            "-I\(Self.sourcesDirectory.appendingPathComponent("_CabiShims").appendingPathComponent("include").path)",
            "-Xclang-linker", "-mexec-model=reactor",
            // TODO: Remove `--export-all` linker option by replacing `@_cdecl` with `@_expose(wasm)`
            "-Xlinker", "--export-all",
            "-o", outputPath.path
        ]
        // NOTE: Clear environment variables to avoid inheriting from the current process.
        //       A test process launched by SwiftPM includes SDKROOT environment variable
        //       and it makes Swift Driver wrongly pick the SDK root from the environment
        //       variable (typically host SDK root) instead of wasi-sysroot.
        process.environment = [:]
        process.launch()
        process.waitUntilExit()
        guard process.terminationStatus == 0 else {
            let fileContents = inputFiles.map {
                """
                // MARK: - \($0)
                \((try? String(contentsOfFile: $0)) ?? "Failed to read \($0)")
                """
            }.joined(separator: "\n====================\n")      
            let message = """
            Failed to execute \(process.arguments?.joined(separator: " ") ?? " ")
            Exit status: \(process.terminationStatus)
            Input files:
            \(fileContents)
            """
            throw Error(description: message)
        }
        return outputPath
    }
}