Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ jobs:
with:
js-locks: |
conformance/driver-ct/jco/package-lock.json
examples/jco-demo/package-lock.json
js/jco/package-lock.json
js/componentize/wpt/parity/package-lock.json

- name: Install Node 24
uses: actions/setup-node@v4
Expand Down
42 changes: 42 additions & 0 deletions conformance/driver-ct/jco/host-imports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// The suites' import object for `-I async` instantiation, shared by
// the Node and browser workers: the polymorph:webcrypto host
// interfaces discovered from the jco host module's lowercase namespace
// exports (the wildcard-map convention, applied at instantiate time —
// a new interface is a new namespace export, enumerated nowhere), the
// driver's test-context provider, and the caller's wasi shim
// namespaces (Node or browser build). Relative specifiers only: module
// Web Workers cannot see a page's import map.

import { bindImports } from "./node_modules/@polymorph/component-test-js/js/viewer/imports.mjs";
import * as webcrypto from "../../../js/jco/webcrypto.js";
import { Context } from "../context.js";

/** The camelCased namespace export's WIT interface name. */
const kebab = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();

/** Build the full import object over the given wasi shim namespaces. */
export function suiteImports(wasi) {
const sut = {};
for (const [name, impl] of Object.entries(webcrypto)) {
if (!/^[a-z]/.test(name) || typeof impl !== "object" || impl === null) {
continue;
}
sut[`polymorph:webcrypto/${kebab(name)}`] = impl;
}
// The driver's own context (diagnostic sink wiring), not the
// upstream default.
sut["polymorph:test/test-context"] = { Context };
return bindImports({ wasi, env: [], sut });
}

/** Instantiate one fresh suite: compiled cores by file name, plus the
* exported `tests` interface whichever spelling the transpile used. */
export async function instantiateSuite({ instantiate, modules, wasi }) {
const instance = await instantiate((name) => modules.get(name), suiteImports(wasi));
const tests =
instance.tests ?? instance["polymorph:test/tests@0.1.0"] ?? instance["polymorph:test/tests"];
if (!tests) {
throw new Error(`suite instance exports no tests interface: ${Object.keys(instance)}`);
}
return tests;
}
28 changes: 14 additions & 14 deletions conformance/driver-ct/jco/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions conformance/driver-ct/jco/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"type": "module",
"description": "jco drivers (Node + headless browser) for the ported conformance suites: transpiles conformance-guest-ct and conformance-signing-guest-ct against the browser-first host (js/jco/webcrypto.js) plus a JS test-context provider, and runs them with mark scheduling under Node (runner.mjs) or headless Chromium (run-browser.mjs).",
"scripts": {
"transpile": "component-test-jco-transpile transpile ../../../target/wasm32-wasip2/release/conformance_guest_ct.wasm --name conformance-guest-ct --async-mode jspi --map 'polymorph:webcrypto/*@0.1.0=../../../../js/jco/webcrypto.js#*' --map 'polymorph:test/test-context@0.1.0=../../context.js' --map 'wasi:cli/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/cli.js#*' --map 'wasi:clocks/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/clocks.js#*' --map 'wasi:io/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/io.js#*' --map 'wasi:random/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/random.js#*' --map 'wasi:filesystem/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/filesystem.js#*' -o generated",
"transpile:signing": "component-test-jco-transpile transpile ../../../target/wasm32-wasip2/release/conformance_signing_guest_ct.wasm --name conformance-signing-guest-ct --async-mode jspi --map 'polymorph:webcrypto/*@0.1.0=../../../../js/jco/webcrypto.js#*' --map 'polymorph:test/test-context@0.1.0=../../context.js' --map 'wasi:cli/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/cli.js#*' --map 'wasi:clocks/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/clocks.js#*' --map 'wasi:io/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/io.js#*' --map 'wasi:random/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/random.js#*' --map 'wasi:filesystem/*=../node_modules/@bytecodealliance/preview2-shim/lib/browser/filesystem.js#*' -o generated",
"transpile": "component-test-jco-transpile transpile ../../../target/wasm32-wasip2/release/conformance_guest_ct.wasm --name conformance-guest-ct -I async -o generated",
"transpile:signing": "component-test-jco-transpile transpile ../../../target/wasm32-wasip2/release/conformance_signing_guest_ct.wasm --name conformance-signing-guest-ct -I async -o generated",
"run": "node --experimental-wasm-jspi runner.mjs",
"run:browser": "node run-browser.mjs"
},
"dependencies": {
"@bytecodealliance/jco-transpile": "^0.5.2",
"@bytecodealliance/preview2-shim": "^0.17.4",
"@bytecodealliance/jco-transpile": "https://github.com/lann/jco/releases/download/jco-transpile-0.6.2-fa6990d/bytecodealliance-jco-transpile-0.6.2.tgz",
"@bytecodealliance/preview2-shim": "0.17.9",
"@polymorph/component-test-js": "github:polymorph-components/polymorph-test#76c094576076284c1a7d0ea1b7bb724e9e7384a5",
"playwright-core": "^1.61.1"
"playwright-core": "1.62.1"
}
}
29 changes: 13 additions & 16 deletions conformance/driver-ct/jco/run-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +47,24 @@ const SUITES = [
];

// Resolve each suite's core-module list Node-side (the transpile emits
// one or two cores), so the page never fetches a missing file — a 404
// would be tolerated but pollutes the console the driver mirrors.
// however many cores the composition needs), so the page never fetches
// a missing file — a 404 would be tolerated but pollutes the console
// the driver mirrors.
import { readdir } from "node:fs/promises";
for (const entry of SUITES) {
entry.cores = [];
for (const core of [`${entry.suite}.core.wasm`, `${entry.suite}.core2.wasm`]) {
try {
await access(new URL(`./generated/${core}`, import.meta.url));
entry.cores.push(core);
} catch {
// Not emitted by this transpile.
}
}
const names = await readdir(new URL("./generated/", import.meta.url));
entry.cores = names
.filter((n) => n.startsWith(`${entry.suite}.core`) && n.endsWith(".wasm"))
.sort();
}

// The in-page harness: spawns a pool of module Web Workers
// (worker-browser.mjs), each running one shard of a suite's case loop
// with its own instance of the transpiled suite (whose polymorph:webcrypto
// imports resolve to js/jco/webcrypto.js — the browser-first host,
// feature-detecting per call — and whose wasi imports resolve to
// relative paths into the preview2-shim browser build, mapped at
// transpile time: module workers cannot see a page's import map).
// with its own instance of the transpiled suite, instantiated with
// imports bound at run time (host-imports.mjs: js/jco/webcrypto.js —
// the browser-first host, feature-detecting per call — the driver's
// test-context, and the preview2-shim browser build, all by relative
// path: module workers cannot see a page's import map).
// Rows come back tagged with their suite-order index and are re-sorted
// before reporting.
//
Expand Down
33 changes: 24 additions & 9 deletions conformance/driver-ct/jco/worker-browser.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
// The Web Worker half of the jco-browser driver (run-browser.mjs): its
// own instance of the transpiled suite runs one shard of the case loop
// (harness.mjs — module workers cannot see the page's import map, which
// is why the transpile maps the wasi shim to relative paths), streaming
// each results-JSONL event back with its suite-order index, then the
// shard's counts. The browser counterpart of worker-node.mjs.
// own instance of the transpiled suite runs one shard of the case loop,
// streaming each results-JSONL event back with its suite-order index,
// then the shard's counts. The browser counterpart of worker-node.mjs.
// Relative specifiers throughout: module workers cannot see the page's
// import map, so the wasi shim (browser build), the harness core, and
// the host module all resolve by path.
import { inventoryLookup, runCases } from "./node_modules/@polymorph/component-test-js/js/viewer/harness.mjs";
import * as cli from "./node_modules/@bytecodealliance/preview2-shim/lib/browser/cli.js";
import * as clocks from "./node_modules/@bytecodealliance/preview2-shim/lib/browser/clocks.js";
import * as io from "./node_modules/@bytecodealliance/preview2-shim/lib/browser/io.js";
import * as random from "./node_modules/@bytecodealliance/preview2-shim/lib/browser/random.js";
import * as filesystem from "./node_modules/@bytecodealliance/preview2-shim/lib/browser/filesystem.js";
import { Context } from "../context.js";
import { instantiateSuite } from "./host-imports.mjs";

// A rejection escaping the awaited chain (e.g. a platform quirk
// surfacing through the transpiled guest's async plumbing) would
Expand All @@ -19,14 +26,22 @@ self.onunhandledrejection = (event) => {
self.onmessage = async ({ data }) => {
const { suite, missing, cores, shard } = data;
try {
const coreModules = [];
const coreBytes = [];
const modules = new Map();
for (const core of cores) {
const res = await fetch(new URL(`./generated/${core}`, import.meta.url));
if (!res.ok) throw new Error(`fetching ${core}: ${res.status}`);
coreModules.push(new Uint8Array(await res.arrayBuffer()));
const bytes = new Uint8Array(await res.arrayBuffer());
coreBytes.push(bytes);
modules.set(core, await WebAssembly.compile(bytes));
}
const tagsOf = inventoryLookup(coreModules);
const { tests } = await import(`./generated/${suite}.js`);
const tagsOf = inventoryLookup(coreBytes);
const { instantiate } = await import(`./generated/${suite}.js`);
const tests = await instantiateSuite({
instantiate,
modules,
wasi: { cli, clocks, io, random, filesystem },
});
const counts = await runCases({
cases: await tests.all(),
Context,
Expand Down
29 changes: 19 additions & 10 deletions conformance/driver-ct/jco/worker-node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,32 @@
// suite-order index so the parent can restore suite order — back through
// `parentPort`, then reporting the shard's counts. Workers inherit the
// parent's execArgv, so `--experimental-wasm-jspi` carries over.
import { readFile } from "node:fs/promises";
import { readFile, readdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { parentPort, workerData } from "node:worker_threads";
import { cli, clocks, io, random, filesystem } from "@bytecodealliance/preview2-shim";
import { inventoryLookup, runCases } from "@polymorph/component-test-js/harness";
import { Context } from "../context.js";
import { instantiateSuite } from "./host-imports.mjs";

const { suite, missing, only, shard } = workerData;

const coreModules = [];
for (const core of [`${suite}.core.wasm`, `${suite}.core2.wasm`]) {
try {
coreModules.push(new Uint8Array(await readFile(new URL(`./generated/${core}`, import.meta.url))));
} catch {
continue;
}
const generatedDir = new URL("./generated/", import.meta.url);
const coreBytes = [];
const modules = new Map();
for (const name of (await readdir(fileURLToPath(generatedDir))).sort()) {
if (!name.startsWith(`${suite}.core`) || !name.endsWith(".wasm")) continue;
const bytes = new Uint8Array(await readFile(new URL(`./generated/${name}`, import.meta.url)));
coreBytes.push(bytes);
modules.set(name, await WebAssembly.compile(bytes));
}
const tagsOf = inventoryLookup(coreModules);
const { tests } = await import(`./generated/${suite}.js`);
const tagsOf = inventoryLookup(coreBytes);
const { instantiate } = await import(`./generated/${suite}.js`);
const tests = await instantiateSuite({
instantiate,
modules,
wasi: { cli, clocks, io, random, filesystem },
});

const counts = await runCases({
cases: await tests.all(),
Expand Down
12 changes: 9 additions & 3 deletions conformance/driver-ct/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ _ct-tools:
# generator. Bump both together: the rev in Cargo.toml + `npm
# install --save-exact github:polymorph-components/polymorph-test#<rev>` in jco/.
"{{ct-tools}}/bin/component-test" pins --cargo-lock "{{root}}/Cargo.lock" \
--js-lock jco/package-lock.json --expect "$rev" > /dev/null
--js-lock jco/package-lock.json \
--js-lock ../../examples/jco-demo/package-lock.json \
--js-lock ../../js/jco/package-lock.json \
--js-lock ../../js/componentize/wpt/parity/package-lock.json \
--expect "$rev" > /dev/null

# Enumerate each suite's full case census (ct-driver instantiates the
# suite against the wasmtime host) into the `lock --leaves` inputs: the
Expand Down Expand Up @@ -184,8 +188,10 @@ _jco-prepared: build
# cwd but not from jco/ — and not always relative: module-from-root
# invocation resolves `root` absolute (justfile.shared.just), while
# in-directory invocation leaves it `../..`. Never re-anchor these
# variables with a textual prefix.
stamp=$(cat "{{guest}}" "{{signing}}" | sha256sum | cut -d' ' -f1)
# variables with a textual prefix. The package.json rides in the
# stamp: the transpile flags and the pinned transpiler both live
# there, and either changing must invalidate the generated tree.
stamp=$(cat "{{guest}}" "{{signing}}" jco/package.json | sha256sum | cut -d' ' -f1)
cd jco
exec 9> .prepare.lock
flock 9
Expand Down
Loading
Loading