diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96c36e547..e6b0db563 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,22 +25,27 @@ jobs: - { os: ubuntu-20.04, toolchain: wasm-5.7.1-RELEASE, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} + env: + JAVASCRIPTKIT_WASI_BACKEND: ${{ matrix.entry.wasi-backend }} + SWIFT_VERSION: ${{ matrix.entry.toolchain }} steps: - name: Checkout uses: actions/checkout@master with: fetch-depth: 1 - - name: Run Test - env: - JAVASCRIPTKIT_WASI_BACKEND: ${{ matrix.entry.wasi-backend }} + - name: Install swiftenv run: | git clone https://github.com/kylef/swiftenv.git ~/.swiftenv export SWIFTENV_ROOT="$HOME/.swiftenv" export PATH="$SWIFTENV_ROOT/bin:$PATH" eval "$(swiftenv init -)" - SWIFT_VERSION=${{ matrix.entry.toolchain }} make bootstrap + echo $PATH >> $GITHUB_PATH + env >> $GITHUB_ENV echo ${{ matrix.entry.toolchain }} > .swift-version - make test + - run: make bootstrap + - run: make test + - run: make unittest + if: ${{ startsWith(matrix.toolchain, 'wasm-5.7.') }} - name: Check if SwiftPM resources are stale run: | make regenerate_swiftpm_resources diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index a0af77527..e708816cb 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -9,7 +9,7 @@ const fs = require("fs"); const readFile = promisify(fs.readFile); const WASI = { - Wasmer: () => { + Wasmer: ({ programName }) => { // Instantiate a new WASI Instance const wasmFs = new WasmFs(); // Output stdout and stderr to console @@ -27,7 +27,7 @@ const WASI = { return originalWriteSync(fd, buffer, offset, length, position); }; const wasi = new WasmerWASI({ - args: [], + args: [programName], env: {}, bindings: { ...WasmerWASI.defaultBindings, @@ -44,9 +44,9 @@ const WASI = { } } }, - MicroWASI: () => { + MicroWASI: ({ programName }) => { const wasi = new MicroWASI({ - args: [], + args: [programName], env: {}, features: [useAll()], }) @@ -59,11 +59,11 @@ const WASI = { } } }, - Node: () => { + Node: ({ programName }) => { const wasi = new NodeWASI({ - args: [], + args: [programName], env: {}, - returnOnExit: true, + returnOnExit: false, }) return { @@ -91,7 +91,7 @@ const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => const swift = new SwiftRuntime(); // Fetch our Wasm File const wasmBinary = await readFile(wasmPath); - const wasi = wasiConstructor(); + const wasi = wasiConstructor({ programName: wasmPath }); // Instantiate the WebAssembly file let { instance } = await WebAssembly.instantiate(wasmBinary, { diff --git a/Makefile b/Makefile index bd93f2e60..7b8736221 100644 --- a/Makefile +++ b/Makefile @@ -12,12 +12,19 @@ build: .PHONY: test test: + @echo Running integration tests cd IntegrationTests && \ CONFIGURATION=debug make test && \ CONFIGURATION=debug SWIFT_BUILD_FLAGS="-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" make test && \ CONFIGURATION=release make test && \ CONFIGURATION=release SWIFT_BUILD_FLAGS="-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" make test +.PHONY: unittest +unittest: + @echo Running unit tests + swift build --build-tests --triple wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=main + node --experimental-wasi-unstable-preview1 scripts/test-harness.js ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm + .PHONY: benchmark_setup benchmark_setup: cd IntegrationTests && CONFIGURATION=release make benchmark_setup diff --git a/Package.swift b/Package.swift index d278e5ab9..c8f55dd0b 100644 --- a/Package.swift +++ b/Package.swift @@ -8,6 +8,7 @@ let package = Package( .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), + .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), ], targets: [ .target( @@ -26,5 +27,20 @@ let package = Package( dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"] ), .target(name: "_CJavaScriptEventLoop"), + .target( + name: "JavaScriptEventLoopTestSupport", + dependencies: [ + "_CJavaScriptEventLoopTestSupport", + "JavaScriptEventLoop", + ] + ), + .target(name: "_CJavaScriptEventLoopTestSupport"), + .testTarget( + name: "JavaScriptEventLoopTestSupportTests", + dependencies: [ + "JavaScriptKit", + "JavaScriptEventLoopTestSupport" + ] + ), ] ) diff --git a/README.md b/README.md index 42af64320..0c2c6988c 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,23 @@ asyncButtonElement.onclick = .object(JSClosure { _ in _ = document.body.appendChild(asyncButtonElement) ``` +### `JavaScriptEventLoop` activation in XCTest suites + +If you need to execute Swift async functions that can be resumed by JS event loop in your XCTest suites, please add `JavaScriptEventLoopTestSupport` to your test target dependencies. + +```diff + .testTarget( + name: "MyAppTests", + dependencies: [ + "MyApp", ++ "JavaScriptEventLoopTestSupport", + ] + ) +``` + +Linking this module automatically activates JS event loop based global executor by calling `JavaScriptEventLoop.installGlobalExecutor()` + + ## Requirements ### For developers diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift new file mode 100644 index 000000000..9922de945 --- /dev/null +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -0,0 +1,31 @@ +/// If you need to execute Swift async functions that can be resumed by JS +/// event loop in your XCTest suites, please add `JavaScriptEventLoopTestSupport` +/// to your test target dependencies. +/// +/// ```diff +/// .testTarget( +/// name: "MyAppTests", +/// dependencies: [ +/// "MyApp", +/// + "JavaScriptEventLoopTestSupport", +/// ] +/// ) +/// ``` +/// +/// Linking this module automatically activates JS event loop based global +/// executor by calling `JavaScriptEventLoop.installGlobalExecutor()` + +import JavaScriptEventLoop + +// This module just expose 'JavaScriptEventLoop.installGlobalExecutor' to C ABI +// See _CJavaScriptEventLoopTestSupport.c for why this is needed + +#if compiler(>=5.5) + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@_cdecl("swift_javascriptkit_activate_js_executor_impl") +func swift_javascriptkit_activate_js_executor_impl() { + JavaScriptEventLoop.installGlobalExecutor() +} + +#endif diff --git a/Sources/_CJavaScriptEventLoopTestSupport/_CJavaScriptEventLoopTestSupport.c b/Sources/_CJavaScriptEventLoopTestSupport/_CJavaScriptEventLoopTestSupport.c new file mode 100644 index 000000000..7dfdbe2e8 --- /dev/null +++ b/Sources/_CJavaScriptEventLoopTestSupport/_CJavaScriptEventLoopTestSupport.c @@ -0,0 +1,19 @@ +// This 'ctor' function is called at startup time of this program. +// It's invoked by '_start' of command-line or '_initialize' of reactor. +// This ctor activate the event loop based global executor automatically +// before running the test cases. For general applications, applications +// have to activate the event loop manually on their responsibility. +// However, XCTest framework doesn't provide a way to run arbitrary code +// before running all of the test suites. So, we have to do it here. +// +// See also: https://github.com/WebAssembly/WASI/blob/main/legacy/application-abi.md#current-unstable-abi + +extern void swift_javascriptkit_activate_js_executor_impl(void); + +// priority 0~100 is reserved by wasi-libc +// https://github.com/WebAssembly/wasi-libc/blob/30094b6ed05f19cee102115215863d185f2db4f0/libc-bottom-half/sources/environ.c#L20 +__attribute__((constructor(/* priority */ 200))) +void swift_javascriptkit_activate_js_executor(void) { + swift_javascriptkit_activate_js_executor_impl(); +} + diff --git a/Sources/_CJavaScriptEventLoopTestSupport/include/dummy.h b/Sources/_CJavaScriptEventLoopTestSupport/include/dummy.h new file mode 100644 index 000000000..e69de29bb diff --git a/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift b/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift new file mode 100644 index 000000000..cca303a09 --- /dev/null +++ b/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift @@ -0,0 +1,15 @@ +import XCTest +import JavaScriptKit + +final class JavaScriptEventLoopTestSupportTests: XCTestCase { + func testAwaitMicrotask() async { + let _: () = await withCheckedContinuation { cont in + JSObject.global.queueMicrotask.function!( + JSOneshotClosure { _ in + cont.resume(returning: ()) + return .undefined + } + ) + } + } +} diff --git a/scripts/test-harness.js b/scripts/test-harness.js new file mode 100644 index 000000000..39a7dbe9a --- /dev/null +++ b/scripts/test-harness.js @@ -0,0 +1,10 @@ +Error.stackTraceLimit = Infinity; + +const { startWasiTask, WASI } = require("../IntegrationTests/lib"); + +const handleExitOrError = (error) => { + console.log(error); + process.exit(1); +} + +startWasiTask(process.argv[2]).catch(handleExitOrError);