diff --git a/commitlint.config.mjs b/commitlint.config.mjs index 66e8b8f41..29bce7335 100644 --- a/commitlint.config.mjs +++ b/commitlint.config.mjs @@ -22,7 +22,7 @@ const config = { ], ], "scope-enum": [1, "always", ["jco", "bindgen", "p2-shim"]], - "scope-case": [2, "always", "kebab-case"], + "scope-case": [2, "always", "lower-case"], }, }; diff --git a/docs/src/contributing.md b/docs/src/contributing.md index 888d31dea..59e649a2f 100644 --- a/docs/src/contributing.md +++ b/docs/src/contributing.md @@ -1,10 +1,12 @@ # Contributing to the Codebase -Development is based on a standard `npm install && npm run build && npm run test` workflow. +Development is based on a standard NodeJS workflow, i.e.: -Tests can be run without bundling via `npm run build:dev && npm run test:dev`. - -Specific tests can be run adding the mocha `--grep` / `-g` flag, for example: `npm run test:dev -- --grep exports_only`. +```console +npm install +npm run build +npm run test +``` ## Prerequisites @@ -49,7 +51,21 @@ npm run build ## Testing There are three test suites in jco: + * `npm run test`: Project-level transpilation, CLI & API tests. * `npm run test --workspace packages/preview2-shim`: `preview2-shim` unit tests. * `test/browser.html`: Bare-minimum browser validation test. * `cargo test`: Wasmtime preview2 conformance tests (not currently passing). + +### Running tests without bundling + +Tests can be run without bundling via `npm run build:dev && npm run test:dev`. + +### Running specific tests + +JS tests are powered by [`vitest`][vitest], and a specific test suite can be run by passing +the filename to `npm run test`: + +```console +npm run test runtime.js +``` diff --git a/packages/preview2-shim/package.json b/packages/preview2-shim/package.json index 7635b0e41..c520d3f6e 100644 --- a/packages/preview2-shim/package.json +++ b/packages/preview2-shim/package.json @@ -23,15 +23,12 @@ } }, "scripts": { - "test": "node --expose-gc ../../node_modules/mocha/bin/mocha.js -u tdd test/test.js --timeout 30000" + "test": "vitest run -c test/vitest.ts" }, "files": [ "types", "lib" ], - "devDependencies": { - "mocha": "^10.2.0" - }, "repository": { "type": "git", "url": "git+https://github.com/bytecodealliance/jco.git" diff --git a/packages/preview2-shim/test/meta-resolve-stub.ts b/packages/preview2-shim/test/meta-resolve-stub.ts new file mode 100644 index 000000000..93501b60f --- /dev/null +++ b/packages/preview2-shim/test/meta-resolve-stub.ts @@ -0,0 +1,4 @@ +// see: https://github.com/vitest-dev/vitest/issues/6953#issuecomment-2505310022 +import { vi } from 'vitest'; +import { createRequire } from 'node:module'; +vi.stubGlobal('globalCreateRequire', createRequire); diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index f49f15f8c..df3d0b0a7 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,23 +1,11 @@ import { - deepStrictEqual, - notDeepStrictEqual, - notEqual, - ok, - strictEqual, - throws, + throws } from "node:assert"; import { fileURLToPath } from "node:url"; -const symbolDispose = Symbol.dispose || Symbol.for("dispose"); +import { suite, test, assert } from "vitest"; -function testWithGCWrap(asyncTestFn) { - return async () => { - await asyncTestFn(); - // Force the JS GC to run finalizers - gc(); - await new Promise((resolve) => setTimeout(resolve, 200)); - }; -} +const symbolDispose = Symbol.dispose || Symbol.for("dispose"); suite("Node.js Preview2", () => { test("Stdio", async () => { @@ -38,14 +26,14 @@ suite("Node.js Preview2", () => { { const { seconds, nanoseconds } = wallClock.now(); - strictEqual(typeof seconds, "bigint"); - strictEqual(typeof nanoseconds, "number"); + assert.strictEqual(typeof seconds, "bigint"); + assert.strictEqual(typeof nanoseconds, "number"); } { const { seconds, nanoseconds } = wallClock.resolution(); - strictEqual(typeof seconds, "bigint"); - strictEqual(typeof nanoseconds, "number"); + assert.strictEqual(typeof seconds, "bigint"); + assert.strictEqual(typeof nanoseconds, "number"); } }); @@ -54,10 +42,10 @@ suite("Node.js Preview2", () => { clocks: { monotonicClock }, } = await import("@bytecodealliance/preview2-shim"); - strictEqual(typeof monotonicClock.resolution(), "bigint"); + assert.strictEqual(typeof monotonicClock.resolution(), "bigint"); const curNow = monotonicClock.now(); - strictEqual(typeof curNow, "bigint"); - ok(monotonicClock.now() > curNow); + assert.strictEqual(typeof curNow, "bigint"); + assert.ok(monotonicClock.now() > curNow); }); test("Monotonic clock immediately resolved polls", async () => { @@ -67,11 +55,11 @@ suite("Node.js Preview2", () => { const curNow = monotonicClock.now(); { const poll = monotonicClock.subscribeInstant(curNow - 10n); - ok(poll.ready()); + assert.ok(poll.ready()); } { const poll = monotonicClock.subscribeDuration(0n); - ok(poll.ready()); + assert.ok(poll.ready()); } }); @@ -87,8 +75,8 @@ suite("Node.js Preview2", () => { // verify we are at the right time, and within 1ms of the original now const nextNow = monotonicClock.now(); - ok(nextNow - curNow >= 10e6); - ok(nextNow - curNow < 15e6); + assert.ok(nextNow - curNow >= 10e6); + assert.ok(nextNow - curNow < 15e6); }); test("Monotonic clock subscribe instant", async () => { @@ -103,8 +91,8 @@ suite("Node.js Preview2", () => { // verify we are at the right time, and within 1ms of the original now const nextNow = monotonicClock.now(); - ok(nextNow - curNow >= 10e6); - ok(nextNow - curNow < 15e6); + assert.ok(nextNow - curNow >= 10e6); + assert.ok(nextNow - curNow < 15e6); }); }); @@ -125,7 +113,7 @@ suite("Node.js Preview2", () => { let buf = stream.read(10000n); while (buf.byteLength === 0) buf = stream.read(10000n); const source = new TextDecoder().decode(buf); - ok(source.includes("UNIQUE STRING")); + assert.ok(source.includes("UNIQUE STRING")); toDispose.push(stream); toDispose.push(childDescriptor); })(); @@ -192,9 +180,9 @@ suite("Node.js Preview2", () => { responseBody = new TextDecoder().decode(buf); } - strictEqual(status, 200); - ok(headers["content-type"].startsWith("text/html")); - ok(responseBody.includes("WebAssembly")); + assert.strictEqual(status, 200); + assert.ok(headers["content-type"].startsWith("text/html")); + assert.ok(responseBody.includes("WebAssembly")); }), ); @@ -203,13 +191,13 @@ suite("Node.js Preview2", () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network1 = sockets.instanceNetwork.instanceNetwork(); const network2 = sockets.instanceNetwork.instanceNetwork(); - strictEqual(network1, network2); + assert.strictEqual(network1, network2); }); test("sockets.tcpCreateSocket() should throw not-supported", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const socket = sockets.tcpCreateSocket.createTcpSocket("ipv4"); - notEqual(socket, null); + assert.notEqual(socket, null); throws( () => { @@ -232,14 +220,14 @@ suite("Node.js Preview2", () => { tcpSocket.startBind(network, localAddress); tcpSocket.finishBind(); - deepStrictEqual(tcpSocket.localAddress(), { + assert.deepStrictEqual(tcpSocket.localAddress(), { tag: "ipv4", val: { address: [0, 0, 0, 0], port: 1337, }, }); - strictEqual(tcpSocket.addressFamily(), "ipv4"); + assert.strictEqual(tcpSocket.addressFamily(), "ipv4"); }); test("tcp.bind(): should bind to a valid ipv6 address and port=0", async () => { @@ -256,7 +244,7 @@ suite("Node.js Preview2", () => { tcpSocket.startBind(network, localAddress); tcpSocket.finishBind(); - strictEqual(tcpSocket.addressFamily(), "ipv6"); + assert.strictEqual(tcpSocket.addressFamily(), "ipv6"); const boundAddress = tcpSocket.localAddress(); const expectedAddress = { @@ -268,9 +256,9 @@ suite("Node.js Preview2", () => { }, }; - strictEqual(boundAddress.tag, expectedAddress.tag); - deepStrictEqual(boundAddress.val.address, expectedAddress.val.address); - strictEqual(boundAddress.val.port > 0, true); + assert.strictEqual(boundAddress.tag, expectedAddress.tag); + assert.deepStrictEqual(boundAddress.val.address, expectedAddress.val.address); + assert.strictEqual(boundAddress.val.port > 0, true); }); test("tcp.bind(): should throw invalid-argument when invalid address family", async () => { @@ -361,15 +349,15 @@ suite("Node.js Preview2", () => { }, }); - ok(!pollable.ready()); + assert.ok(!pollable.ready()); pollable.block(); - ok(pollable.ready()); + assert.ok(pollable.ready()); const [input, output] = tcpSocket.finishConnect(); - strictEqual(tcpSocket.addressFamily(), "ipv4"); + assert.strictEqual(tcpSocket.addressFamily(), "ipv4"); - ok(pollable.ready()); + assert.ok(pollable.ready()); output.blockingWriteAndFlush( new TextEncoder().encode("GET http://www.google.com/ HTTP/1.1\n\n"), @@ -387,9 +375,9 @@ suite("Node.js Preview2", () => { } } const responseBody = new TextDecoder().decode(buf); - ok(responseBody.includes("Google")); - ok(responseBody.includes("<!doctype")); - ok(responseBody.includes("<script")); + assert.ok(responseBody.includes("<title>Google")); + assert.ok(responseBody.includes("<!doctype")); + assert.ok(responseBody.includes("<script")); } tcpSocket.shutdown("both"); @@ -401,9 +389,9 @@ suite("Node.js Preview2", () => { test("sockets.udpCreateSocket() should be a singleton", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const socket1 = sockets.udpCreateSocket.createUdpSocket("ipv4"); - notEqual(socket1.id, 1); + assert.notEqual(socket1.id, 1); const socket2 = sockets.udpCreateSocket.createUdpSocket("ipv4"); - notEqual(socket2.id, 1); + assert.notEqual(socket2.id, 1); }); // TODO: figure out how to mock handle.on("message", ...) @@ -424,10 +412,10 @@ suite("Node.js Preview2", () => { socket.finishBind(); const boundAddress = socket.localAddress(); - strictEqual(boundAddress.tag, "ipv4"); - deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0]); - strictEqual(boundAddress.val.port > 0, true); - strictEqual(socket.addressFamily(), "ipv4"); + assert.strictEqual(boundAddress.tag, "ipv4"); + assert.deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0]); + assert.strictEqual(boundAddress.val.port > 0, true); + assert.strictEqual(socket.addressFamily(), "ipv4"); }); test("udp.bind(): should bind to a valid ipv6 address and port=0", async () => { @@ -446,10 +434,10 @@ suite("Node.js Preview2", () => { socket.finishBind(); const boundAddress = socket.localAddress(); - strictEqual(boundAddress.tag, "ipv6"); - deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0, 0, 0, 0, 0]); - strictEqual(boundAddress.val.port > 0, true); - strictEqual(socket.addressFamily(), "ipv6"); + assert.strictEqual(boundAddress.tag, "ipv6"); + assert.deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0, 0, 0, 0, 0]); + assert.strictEqual(boundAddress.val.port > 0, true); + assert.strictEqual(socket.addressFamily(), "ipv6"); }); test("udp.stream(): should connect to a valid ipv4 address", async () => { @@ -475,13 +463,13 @@ suite("Node.js Preview2", () => { socket.finishBind(); socket.stream(remoteAddress); - strictEqual(socket.addressFamily(), "ipv4"); + assert.strictEqual(socket.addressFamily(), "ipv4"); const boundAddress = socket.localAddress(); - strictEqual(boundAddress.tag, "ipv4"); - notDeepStrictEqual(boundAddress.val.address, [0, 0, 0, 0]); - strictEqual(boundAddress.val.port > 0, true); + assert.strictEqual(boundAddress.tag, "ipv4"); + assert.notDeepEqual(boundAddress.val.address, [0, 0, 0, 0]); + assert.strictEqual(boundAddress.val.port > 0, true); }); test( @@ -502,11 +490,11 @@ suite("Node.js Preview2", () => { socket.finishBind(); socket.stream(); - strictEqual(socket.addressFamily(), "ipv6"); + assert.strictEqual(socket.addressFamily(), "ipv6"); const boundAddress = socket.localAddress(); - deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0, 0, 0, 0, 0]); - strictEqual(boundAddress.val.port, 1337); + assert.deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0, 0, 0, 0, 0]); + assert.strictEqual(boundAddress.val.port, 1337); }), ); }); @@ -519,16 +507,16 @@ suite("Instantiation", () => { "@bytecodealliance/preview2-shim/instantiation" ); const shim = new WASIShim(); - ok(shim); - deepStrictEqual( + assert.ok(shim); + assert.deepStrictEqual( Object.keys(shim.getImportObject()["wasi:random/random"]).sort(), Object.keys(random.random).sort(), ); - deepStrictEqual( + assert.deepStrictEqual( Object.keys(shim.getImportObject()["wasi:random/insecure-seed"]).sort(), Object.keys(random.insecureSeed).sort(), ); - deepStrictEqual( + assert.deepStrictEqual( Object.keys(shim.getImportObject()["wasi:random/insecure"]).sort(), Object.keys(random.insecure).sort(), ); @@ -547,16 +535,25 @@ suite("Instantiation", () => { }, }; const shim = new WASIShim(invalidWASIShim); - ok(shim); - notDeepStrictEqual( + assert.ok(shim); + assert.notDeepEqual( Object.keys(shim.getImportObject()["wasi:random/random"]).sort(), Object.keys(random.random).sort(), ); - strictEqual(shim.getImportObject()["wasi:random/insecure-seed"], undefined); - strictEqual(shim.getImportObject()["wasi:random/insecure"], undefined); - deepStrictEqual( + assert.strictEqual(shim.getImportObject()["wasi:random/insecure-seed"], undefined); + assert.strictEqual(shim.getImportObject()["wasi:random/insecure"], undefined); + assert.deepStrictEqual( Object.keys(shim.getImportObject()["wasi:random/random"]).sort(), Object.keys(invalidWASIShim.random.random).sort(), ); }); }); + +function testWithGCWrap(asyncTestFn) { + return async () => { + await asyncTestFn(); + // Force the JS GC to run finalizers + gc(); + await new Promise((resolve) => setTimeout(resolve, 200)); + }; +} diff --git a/packages/preview2-shim/test/vitest.ts b/packages/preview2-shim/test/vitest.ts new file mode 100644 index 000000000..210007e69 --- /dev/null +++ b/packages/preview2-shim/test/vitest.ts @@ -0,0 +1,28 @@ +import { defineConfig } from "vitest/config"; + +const DEFAULT_TIMEOUT_MS = 1000 * 60 * 10; // 10m + +const REPORTERS = process.env.GITHUB_ACTIONS + ? ["verbose", "github-actions"] + : ["verbose"]; + +export default defineConfig({ + test: { + retry: 3, + reporters: REPORTERS, + disableConsoleIntercept: true, + printConsoleTrace: true, + passWithNoTests: false, + include: ["test/*.js"], + setupFiles: ["test/meta-resolve-stub.ts"], + testTimeout: DEFAULT_TIMEOUT_MS, + hookTimeout: DEFAULT_TIMEOUT_MS, + teardownTimeout: DEFAULT_TIMEOUT_MS, + pool: "forks", + poolOptions: { + forks: { + execArgv: ["--expose-gc"], + }, + }, + }, +});