Skip to content

Commit eac6e6e

Browse files
Add test example
1 parent b2f4162 commit eac6e6e

File tree

10 files changed

+265
-105
lines changed

10 files changed

+265
-105
lines changed

Diff for: Examples/Testing/.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Diff for: Examples/Testing/Package.swift

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version: 6.2
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "Counter",
8+
products: [
9+
// Products define the executables and libraries a package produces, making them visible to other packages.
10+
.library(
11+
name: "Counter",
12+
targets: ["Counter"]),
13+
],
14+
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
15+
targets: [
16+
// Targets are the basic building blocks of a package, defining a module or a test suite.
17+
// Targets can depend on other targets in this package and products from dependencies.
18+
.target(
19+
name: "Counter",
20+
dependencies: [
21+
.product(name: "JavaScriptKit", package: "JavaScriptKit")
22+
]),
23+
.testTarget(
24+
name: "CounterTests",
25+
dependencies: ["Counter"]
26+
),
27+
]
28+
)

Diff for: Examples/Testing/Sources/Counter/Counter.swift

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
public struct Counter {
2+
public private(set) var count = 0
3+
4+
public mutating func increment() {
5+
count += 1
6+
}
7+
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Testing
2+
@testable import Counter
3+
4+
@Test func increment() async throws {
5+
var counter = Counter()
6+
counter.increment()
7+
#expect(counter.count == 1)
8+
}
9+
10+
@Test func incrementTwice() async throws {
11+
var counter = Counter()
12+
counter.increment()
13+
counter.increment()
14+
#expect(counter.count == 2)
15+
}

Diff for: Examples/Testing/test.node.mjs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { WASI } from "wasi"
2+
import { instantiate } from "./.build/plugins/PackageToJS/outputs/Package/instantiate.js"
3+
import { readFile } from "fs/promises"
4+
5+
const wasi = new WASI({
6+
version: "preview1",
7+
args: ["--testing-library", "swift-testing"],
8+
returnOnExit: false,
9+
})
10+
const { swift } = await instantiate(
11+
await readFile("./.build/plugins/PackageToJS/outputs/Package/main.wasm"),
12+
{}, { wasi }
13+
)
14+
swift.main()

Diff for: Plugins/PackageToJS/PackageToJS.swift

+28-13
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,15 @@ struct PackageToJS: CommandPlugin {
101101
}
102102

103103
// Build products
104-
let (build, productName) = try buildWasm(options: options, context: context)
105-
guard build.succeeded else {
104+
let (productArtifact, build) = try buildWasm(options: options, context: context)
105+
guard let productArtifact = productArtifact else {
106106
for diagnostic in Self.friendlyBuildDiagnostics {
107107
if let message = diagnostic(build, arguments) {
108108
printStderr("\n" + message)
109109
}
110110
}
111111
exit(1)
112112
}
113-
114-
let productArtifact = try build.findWasmArtifact(for: productName)
115113
let outputDir =
116114
if let outputPath = options.outputPath {
117115
URL(fileURLWithPath: outputPath)
@@ -135,7 +133,7 @@ struct PackageToJS: CommandPlugin {
135133
}
136134

137135
private func buildWasm(options: Options, context: PluginContext) throws -> (
138-
build: PackageManager.BuildResult, productName: String
136+
productArtifact: URL?, build: PackageManager.BuildResult
139137
) {
140138
var parameters = PackageManager.BuildParameters(
141139
configuration: .inherit,
@@ -158,15 +156,32 @@ struct PackageToJS: CommandPlugin {
158156
}
159157
let productName = try options.product ?? deriveDefaultProduct(package: context.package)
160158
let build = try self.packageManager.build(.product(productName), parameters: parameters)
161-
return (build, productName)
159+
160+
var productArtifact: URL?
161+
if build.succeeded {
162+
let testProductName = "\(context.package.displayName)PackageTests"
163+
if productName == testProductName {
164+
for fileExtension in ["wasm", "xctest"] {
165+
let path = ".build/debug/\(testProductName).\(fileExtension)"
166+
if FileManager.default.fileExists(atPath: path) {
167+
productArtifact = URL(fileURLWithPath: path)
168+
break
169+
}
170+
}
171+
} else {
172+
productArtifact = try build.findWasmArtifact(for: productName)
173+
}
174+
}
175+
176+
return (productArtifact, build)
162177
}
163178

164179
/// Construct the build plan and return the root task key
165180
private func constructPackagingPlan(
166181
make: inout MiniMake,
167182
options: Options,
168183
context: PluginContext,
169-
wasmProductArtifact: PackageManager.BuildResult.BuiltArtifact,
184+
wasmProductArtifact: URL,
170185
selfPackage: Package,
171186
outputDir: URL
172187
) -> MiniMake.TaskKey {
@@ -194,10 +209,10 @@ struct PackageToJS: CommandPlugin {
194209
// Copy the wasm product artifact
195210
let wasmFilename = "main.wasm"
196211
let wasm = make.addTask(
197-
inputFiles: [selfPath, wasmProductArtifact.url.path], inputTasks: [outputDirTask],
212+
inputFiles: [selfPath, wasmProductArtifact.path], inputTasks: [outputDirTask],
198213
output: outputDir.appending(path: wasmFilename).path
199214
) {
200-
try syncFile(from: wasmProductArtifact.url.path, to: $0.output)
215+
try syncFile(from: wasmProductArtifact.path, to: $0.output)
201216
}
202217
packageInputs.append(wasm)
203218

@@ -231,6 +246,8 @@ struct PackageToJS: CommandPlugin {
231246
for (file, output) in [
232247
("Plugins/PackageToJS/Templates/index.js", "index.js"),
233248
("Plugins/PackageToJS/Templates/index.d.ts", "index.d.ts"),
249+
("Plugins/PackageToJS/Templates/instantiate.js", "instantiate.js"),
250+
("Plugins/PackageToJS/Templates/instantiate.d.ts", "instantiate.d.ts"),
234251
("Sources/JavaScriptKit/Runtime/index.mjs", "runtime.js"),
235252
] {
236253
let inputPath = selfPackageURL.appending(path: file)
@@ -291,9 +308,7 @@ internal func deriveDefaultProduct(package: Package) throws -> String {
291308

292309
extension PackageManager.BuildResult {
293310
/// Find `.wasm` executable artifact
294-
internal func findWasmArtifact(for product: String) throws
295-
-> PackageManager.BuildResult.BuiltArtifact
296-
{
311+
internal func findWasmArtifact(for product: String) throws -> URL {
297312
let executables = self.builtArtifacts.filter {
298313
($0.kind == .executable) && ($0.url.lastPathComponent == "\(product).wasm")
299314
}
@@ -307,7 +322,7 @@ extension PackageManager.BuildResult {
307322
"Failed to disambiguate executable product artifacts from \(executables.map(\.url.path).joined(separator: ", "))"
308323
)
309324
}
310-
return executable
325+
return executable.url
311326
}
312327
}
313328

Diff for: Plugins/PackageToJS/Templates/index.d.ts

+12-37
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,34 @@
1-
/* export */ type Import = {
2-
// TODO: Generate type from imported .d.ts files
3-
}
4-
/* export */ type Export = {
5-
// TODO: Generate type from .swift files
6-
}
7-
81
/**
92
* The path to the WebAssembly module relative to the root of the package
103
*/
114
export declare const MODULE_PATH: string;
125

13-
/**
14-
* Low-level interface to create an instance of a WebAssembly module
15-
*
16-
* This is used to have full control over the instantiation process
17-
* and to add custom imports.
18-
*/
19-
/* export */ interface Instantiator {
20-
/**
21-
* Add imports to the WebAssembly module
22-
* @param imports - The imports to add
23-
*/
24-
addImports(imports: WebAssembly.Imports): void
25-
6+
export type Options = {
267
/**
27-
* Create an interface to access exposed functionalities
28-
* @param instance - The instance of the WebAssembly module
29-
* @returns The interface to access the exposed functionalities
8+
* The CLI arguments to pass to the WebAssembly module
309
*/
31-
createExports(instance: WebAssembly.Instance): Export
10+
args?: string[]
3211
}
3312

3413
/**
35-
* Create an instantiator for the given imports
36-
* @param imports - The imports to add
37-
* @param options - The options
38-
*/
39-
/* export */ declare function createInstantiator(
40-
imports: Import,
41-
options: {} | undefined
42-
): Promise<Instantiator>
43-
44-
/**
45-
* Instantiate the given WebAssembly module
14+
* Initialize the given WebAssembly module
4615
*
4716
* This is a convenience function that creates an instantiator and instantiates the module.
4817
* @param moduleSource - The WebAssembly module to instantiate
4918
* @param imports - The imports to add
5019
* @param options - The options
5120
*/
52-
export declare function instantiate(
21+
export declare function init(
5322
moduleSource: WebAssembly.Module | ArrayBufferView | ArrayBuffer | Response | PromiseLike<Response>,
5423
imports: Import,
55-
options: {} | undefined
24+
options: Options | undefined
5625
): Promise<{
5726
instance: WebAssembly.Instance,
5827
exports: Export
5928
}>
29+
30+
export declare function runTest(
31+
moduleSource: WebAssembly.Module | ArrayBufferView | ArrayBuffer | Response | PromiseLike<Response>,
32+
imports: Import,
33+
options: Options | undefined
34+
): Promise<{ exitCode: number }>

Diff for: Plugins/PackageToJS/Templates/index.js

+27-55
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,15 @@
11
// @ts-check
2-
import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from '@bjorn3/browser_wasi_shim';
3-
// @ts-ignore
4-
import { SwiftRuntime } from "./runtime.js"
2+
import { WASI, WASIProcExit, File, OpenFile, ConsoleStdout, PreopenDirectory } from '@bjorn3/browser_wasi_shim';
3+
import { instantiate } from './instantiate.js';
54
export const MODULE_PATH = "@PACKAGE_TO_JS_MODULE_PATH@";
65

7-
/** @type {import('./index.d').createInstantiator} */
8-
/* export */ async function createInstantiator(
9-
imports,
10-
options = {}
11-
) {
12-
return {
13-
addImports: () => {},
14-
createExports: () => {
15-
return {};
16-
},
17-
}
18-
}
19-
20-
/** @type {import('./index.d').instantiate} */
21-
export async function instantiate(
6+
/** @type {import('./index.d').init} */
7+
export async function init(
228
moduleSource,
239
imports,
2410
options
2511
) {
26-
const instantiator = await createInstantiator(imports, options);
27-
const wasi = new WASI(/* args */[MODULE_PATH], /* env */[], /* fd */[
12+
const wasi = new WASI(/* args */[MODULE_PATH, ...(options?.args ?? [])], /* env */[], /* fd */[
2813
new OpenFile(new File([])), // stdin
2914
ConsoleStdout.lineBuffered((stdout) => {
3015
console.log(stdout);
@@ -33,44 +18,31 @@ export async function instantiate(
3318
console.error(stderr);
3419
}),
3520
new PreopenDirectory("/", new Map()),
36-
])
37-
const swift = new SwiftRuntime();
38-
39-
/** @type {WebAssembly.Imports} */
40-
const importObject = {
41-
wasi_snapshot_preview1: wasi.wasiImport,
42-
javascript_kit: swift.wasmImports,
43-
};
44-
instantiator.addImports(importObject);
45-
46-
let module;
47-
let instance;
48-
if (moduleSource instanceof WebAssembly.Module) {
49-
module = moduleSource;
50-
instance = await WebAssembly.instantiate(module, importObject);
51-
} else if (typeof Response === "function" && (moduleSource instanceof Response || moduleSource instanceof Promise)) {
52-
if (typeof WebAssembly.instantiateStreaming === "function") {
53-
const result = await WebAssembly.instantiateStreaming(moduleSource, importObject);
54-
module = result.module;
55-
instance = result.instance;
56-
} else {
57-
const moduleBytes = await (await moduleSource).arrayBuffer();
58-
module = await WebAssembly.compile(moduleBytes);
59-
instance = await WebAssembly.instantiate(module, importObject);
60-
}
61-
} else {
62-
// @ts-expect-error: Type 'Response' is not assignable to type 'BufferSource'
63-
module = await WebAssembly.compile(moduleSource);
64-
instance = await WebAssembly.instantiate(module, importObject);
65-
}
66-
67-
swift.setInstance(instance);
68-
// @ts-ignore: "exports" of the instance is not typed
69-
wasi.initialize(instance);
21+
], { debug: false })
22+
const { instance, exports, swift } = await instantiate(moduleSource, imports, {
23+
wasi: wasi
24+
});
7025
swift.main();
7126

7227
return {
7328
instance,
74-
exports: instantiator.createExports(instance),
29+
exports,
30+
}
31+
}
32+
33+
/** @type {import('./index.d').runTest} */
34+
export async function runTest(
35+
moduleSource,
36+
imports,
37+
options
38+
) {
39+
try {
40+
const { instance, exports } = await init(moduleSource, imports, options);
41+
return { exitCode: 0 };
42+
} catch (error) {
43+
if (error instanceof WASIProcExit) {
44+
return { exitCode: error.code };
45+
}
46+
throw error;
7547
}
7648
}

0 commit comments

Comments
 (0)