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
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ Layout (each directory's justfile module in parentheses):
`server/` (echod; `server/PROTOCOL.md` is its wire contract,
`server/echod.mjs` the shared Node spawn helpers). The suite crates
consume the harness as rev-pinned git dependencies (the two
`[workspace.dependencies]` entries in the root `Cargo.toml`; bump
both together), and the `component-test` CLI is cargo-installed at
the rev Cargo.lock records (`conformance-ct::_ct-tools`).
`[workspace.dependencies]` entries in the root `Cargo.toml`), the
jco legs consume its JS runner core as an npm git dep
(`@lann/component-test-js` in `driver-ct/jco/package.json`), and the
`component-test` CLI is cargo-installed at the rev Cargo.lock
records (`conformance-ct::_ct-tools`). One rev everywhere; the root
`Cargo.toml` comment is the bump checklist.
- `examples/` (`just demo::…`) — the echo-demo guest and its host runners.

Checks to run before committing, by what changed: WIT or `wit/README.md` →
Expand Down
14 changes: 7 additions & 7 deletions Cargo.lock

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

15 changes: 9 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ default-members = [
]

[workspace.dependencies]
# The component-test harness, rev-pinned (no registry release yet). The
# two entries and the CLI install (conformance/driver-ct/justfile reads
# the rev back out of Cargo.lock) must move together: bump both here,
# then `cargo update component-test-sdk component-test-runner`.
component-test-sdk = { git = "https://github.com/lann/component-test", rev = "706ee99c593be92a190bb22b1748d6544b04f6cb" }
component-test-runner = { git = "https://github.com/lann/component-test", rev = "706ee99c593be92a190bb22b1748d6544b04f6cb" }
# The component-test harness, rev-pinned (no registry release yet).
# One rev everywhere: these two entries, the CLI install
# (conformance/driver-ct/justfile reads the rev back out of Cargo.lock),
# and the @lann/component-test-js npm pin in
# conformance/driver-ct/jco/package.json. Bump all together, then
# `cargo update component-test-sdk component-test-runner` and
# `npm --prefix conformance/driver-ct/jco install`.
component-test-sdk = { git = "https://github.com/lann/component-test", rev = "135a395c54fb75e6989c0e4047fd6f2f0db2c62a" }
component-test-runner = { git = "https://github.com/lann/component-test", rev = "135a395c54fb75e6989c0e4047fd6f2f0db2c62a" }
anyhow = "1"
bytes = "1"
futures = "0.3"
Expand Down
11 changes: 6 additions & 5 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ section.
| --- | --- |
| [`guest-ct/`](guest-ct) | The suite component: 52 `#[case]`s importing `lann:websocket/connections`, owning **every assertion**. One wasm binary, run unchanged against every target. The committed `tests.lock` is its inventory (drift fails `lock-check` and the runner's own cross-check). |
| [`server/`](server) | The suite-owned echo/reference server (`conformance-echod`): echo plus the fault modes the close-semantics rows need. Wire contract in [`server/PROTOCOL.md`](server/PROTOCOL.md); `server/echod.mjs` holds the Node-side spawn helpers every JS leg shares. |
| [`driver-ct/`](driver-ct) | The legs and the aggregate. `ct-driver` (Rust) embeds the wasmtime host; `jco/` runs the transpiled suite in Node and headless Chromium through one shared `harness.mjs`. `targets.toml` declares targets, features, and expected-fail entries; `component-test aggregate` validates and renders the matrix. |
| [`driver-ct/`](driver-ct) | The legs and the aggregate. `ct-driver` (Rust) embeds the wasmtime host; `jco/` runs the transpiled suite in Node and headless Chromium through one shared `harness.mjs` — thin glue (SUT import wiring, config) over the upstream runner core (`@lann/component-test-js`, the npm git dep), which owns the case loop, verdict mapping, and tag-inventory drift check. `targets.toml` declares targets, features, and expected-fail entries; `component-test aggregate` validates and renders the matrix. |
| [`wit/`](wit) | The suite's world (`sut-imports`): only the surface under test — the export surface comes from the component-test SDK. The `lann:websocket` package arrives through the `deps/lann-websocket` symlink, never a copy. |

### The result stream
Expand Down Expand Up @@ -104,7 +104,8 @@ records the resolution). The `component-test` CLI used by the lockfile
and aggregate recipes is cargo-installed into `target/ct-tools` at the
rev read back out of Cargo.lock (`conformance-ct::_ct-tools`), so the
libraries and the CLI cannot drift apart. Registry dependencies replace
the git pins when component-test publishes. The same applies to any
future use of component-test's JS: npm git deps, not checkout paths
(none is consumed today — `driver-ct/jco` implements the harness and
test-context shim locally).
the git pins when component-test publishes. The jco legs consume the
JS runner core the same way: `@lann/component-test-js` in
`driver-ct/jco/package.json`, a `github:lann/component-test#<rev>` git
dep pinned to the same rev as the cargo entries (one rev everywhere —
the root `Cargo.toml` comment is the bump checklist).
11 changes: 0 additions & 11 deletions conformance/driver-ct/jco/context.js

This file was deleted.

137 changes: 44 additions & 93 deletions conformance/driver-ct/jco/harness.mjs
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
// Environment-agnostic corpus driver for the component-test suite:
// instantiates the transpiled suite fresh per case, drives the tests
// contract, and emits component-test results JSONL (the #26 edge
// encoding). Runs in Node and inside the browser page unchanged.
// Leg-shared glue for the jco conformance legs: the SUT import wiring
// (bindImports) and the thin suite loop over the upstream runner core
// (@lann/component-test-js — one harness for every runner, per
// lann/component-test#5; the case loop, verdict mapping, and tag
// inventory live there, not here). Runs in Node and inside the browser
// page unchanged (the browser page maps the bare specifiers through an
// import map; see run-browser.mjs).
//
// Scheduling: this harness executes everything and says so —
// `"scheduling":"none"` in the envelope — so the aggregate applies
// feature applicability from the lockfile + manifest (the
// component-test #36 mechanism). No tags-section parsing in JS, no
// duplicated scheduling semantics to keep in sync.
// Scheduling: the upstream loop mark-schedules against the tag
// inventory read from the transpiled core wasm. The suite is untagged
// and targets.toml declares no features, so `missing` is always empty
// and every case runs — but the inventory lookup still gates drift:
// a case the tags section does not cover fails the run as unsound
// (the jco analog of the wasmtime runner's cross-check).
//
// Cases run sequentially: the corpus is loopback-I/O-bound (~tens of
// seconds total), and sequential execution sidesteps Chromium's
// per-endpoint handshake serialization (the incumbent driver's
// HANDSHAKE_BLOCKING workaround) entirely.

import { Context } from "./context.js";
import { envelope, inventoryLookup, runCases } from "@lann/component-test-js/harness";
import { Context } from "@lann/component-test-js/context";

// The single-attempt wall bound per case, matching the wasmtime leg's
// --case-timeout: a wedged case is reported (limit-exceeded provenance,
// the component-test vocabulary), never retried, and never allowed to
// hang the leg. The abandoned attempt's promise keeps running until the
// leg exits; each case gets a fresh instance anyway.
// --case-timeout: a wedged case is reported (limit-exceeded provenance),
// never retried, and never allowed to hang the leg. JSPI attempts
// cannot be cancelled — the abandoned attempt's promise keeps running
// until the leg exits, which is why every case gets a fresh instance
// (freshCases below): a timed-out instance may be wedged mid-suspension.
const CASE_TIMEOUT_MS = 60_000;

/**
* The suite's import object, both key spellings (the generated code
* mixes versioned and unversioned): the SUT host, the test-context
* shim, the config environment, and the wasi shims.
* provider (upstream's), the config environment, and the wasi shims.
*/
export function bindImports({ connections, Context, env, cli, clocks, io }) {
export function bindImports({ connections, env, cli, clocks, io }) {
const imports = {};
const bind = (name, impl) => {
imports[name] = impl;
Expand Down Expand Up @@ -65,86 +71,31 @@ export function envInterface(vars) {
}

/**
* Run the whole suite. `newInstance()` must return a fresh instantiated
* suite (exports object); `emit(line)` receives each JSONL line.
* Returns `{ total, failed }`.
* Run the whole suite through the upstream case loop. `newInstance()`
* must return a fresh instantiated suite (exports object) — census and
* every case each get their own; `coreBytes` are the transpiled core
* wasm bytes carrying the tag inventory; `emit(line)` receives each
* JSONL line. Returns `{ total, failed }`.
*/
export async function runSuite({ newInstance, target, suiteName, emit, log }) {
emit(
JSON.stringify({
"component-test-results": "0.1",
target,
suite: { name: suiteName },
run: { segment: 0, scheduling: "none" },
}),
);

// Census from a fresh instance; each case then runs in its own.
export async function runSuite({ newInstance, coreBytes, target, suiteName, emit, log }) {
emit(JSON.stringify(envelope(target, suiteName)));
const tagsOf = inventoryLookup(coreBytes);
const census = await (await newInstance()).tests.all();
const names = [];
for (const testCase of census) {
names.push(testCase.name());
}
if (names.length === 0) {
const counts = await runCases({
cases: census,
Context,
tagsOf,
missing: [],
emit: (event) => {
emit(JSON.stringify(event));
log?.(`${event.case} … ${event.status}`);
},
caseTimeoutMs: CASE_TIMEOUT_MS,
freshCases: async () => (await newInstance()).tests.all(),
});
if (counts.total === 0) {
throw new Error("suite enumerated zero cases (empty selection is a run error)");
}

let failed = 0;
for (const name of names) {
const diagnostics = [];
const instance = await newInstance();
const cases = await instance.tests.all();
const testCase = cases.find((c) => c.name() === name);
if (!testCase) {
throw new Error(`case ${name} vanished on re-enumeration`);
}
let event;
let timer;
const outcome = await Promise.race([
testCase.run(new Context(diagnostics)).then(
() => ({ kind: "pass" }),
(e) => ({ kind: "thrown", e }),
),
new Promise((r) => {
timer = setTimeout(() => r({ kind: "timeout" }), CASE_TIMEOUT_MS);
}),
]);
clearTimeout(timer);
if (outcome.kind === "pass") {
event = { case: name, status: "pass", provenance: "returned" };
} else if (outcome.kind === "timeout") {
failed += 1;
event = {
case: name,
status: "fail",
provenance: { "limit-exceeded": "case-timeout" },
detail: `case timeout exceeded (${CASE_TIMEOUT_MS / 1000}s)`,
};
} else {
// jco maps result::err to a thrown ComponentError with .payload;
// anything else is a trap (or shim bug), attributed as such.
const payload = outcome.e?.payload ?? outcome.e;
if (payload?.tag === "failed") {
failed += 1;
event = { case: name, status: "fail", provenance: "returned", detail: payload.val };
} else if (payload?.tag === "skipped") {
event = { case: name, status: "skipped", provenance: "returned", detail: payload.val };
} else {
failed += 1;
event = {
case: name,
status: "fail",
provenance: "trap",
detail: String(outcome.e?.message ?? outcome.e).split("\n")[0],
};
}
}
event.diagnostics = diagnostics;
event["diagnostics-complete"] = event.status !== "fail" || event.provenance === "returned";
emit(JSON.stringify(event));
log?.(`${name} … ${event.status}`);
}

emit(JSON.stringify({ "segment-end": true }));
return { total: names.length, failed };
return { total: counts.total, failed: counts.failed };
}
8 changes: 8 additions & 0 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.

1 change: 1 addition & 0 deletions conformance/driver-ct/jco/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"devDependencies": {
"@bytecodealliance/jco": "^1.26.1",
"@bytecodealliance/preview2-shim": "^0.17.2",
"@lann/component-test-js": "github:lann/component-test#135a395c54fb75e6989c0e4047fd6f2f0db2c62a",
"playwright-core": "^1.61.1"
},
"engines": {
Expand Down
Loading
Loading