Some checks failed
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 50s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 36s
wai-app and Create / wai-app · does this change touch it (push) Successful in 34s
wai-fleet service / does this change touch it (push) Successful in 35s
wai-ledger service / does this change touch it (push) Successful in 36s
WAI Studio / does this change touch it (push) Successful in 31s
wai-fleet service / service · macos (push) Successful in 1m10s
wai-ledger service / service · macos (push) Successful in 1m46s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 5m23s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 7m36s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 12s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 2m28s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 42s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m47s
wai-ledger service / service · linux (push) Successful in 3m34s
wai-fleet service / service · linux (push) Successful in 4m25s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m50s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 6m55s
WAI Studio / Studio in WebKit · linux (push) Successful in 16m42s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 3m41s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 6m10s
- joule-moq-web's web/binding/sink.html answers each call of live-conformance/browser with wai-live-web's wasm build and the page's own side table (web/room/binding.js), each answer one JSON text in RFC 8785 form; e2e/conform_sink.mjs drives it in a browser and is the HTTP adapter of wai conform sink --suite browser. With --wai it runs the runner itself, the record named for the page and the browser's version, and admits it with wai verify --conformant. - Run on the build host in its installed browser (156.0.8078.80, headless): 110 of 110 cases, conformant, admitted. Planted (--plant: frames the page shows as refused answered verified): 106 of 110, failing at frames/object_refused among four, and its record refused at admission rule 2. - e2e/run.sh runs both as its conform step (joule-moq-web.yml), the planted record's refusal checked too. - wai-breaking.yml: the relay's --version for the record drops the comma after the number. Co-Authored-By: Claude Opus 5.5 <[email protected]>
117 lines
6.4 KiB
JavaScript
117 lines
6.4 KiB
JavaScript
// The browser sink in a real browser as `wai conform sink --suite browser` runs it: the page
|
|
// web/binding/sink.html (wai-live-web's wasm, built into web/pkg/, and the page's own side table,
|
|
// web/room/binding.js) answers each call of live-conformance/browser, and this script is the
|
|
// conformance runner's HTTP adapter for it (WAI extensions/conformance-results.md; wai conform's
|
|
// adapter.rs): each call `wai conform` POSTs is put to the page, and the page's answer, one JSON
|
|
// text in RFC 8785 form and a line feed, is the response.
|
|
//
|
|
// node e2e/conform_sink.mjs [--web <dir>] [--browser <bin>] [--port <n>] [--plant]
|
|
// serves the adapter on 127.0.0.1 and prints `adapter http://127.0.0.1:<port>/`; runs until
|
|
// interrupted;
|
|
// node e2e/conform_sink.mjs --wai <bin> --wai-dir <wai/> --out <record> [--independent] [...]
|
|
// the same, then runs `wai conform sink --suite browser --impl <the adapter>` itself, its
|
|
// record signed to <record> and named for the page and the browser's version, and, when the
|
|
// run passed, `wai verify --conformant` over the record; exits with the first that failed.
|
|
//
|
|
// `--browser` names an installed browser (BROWSER_BIN by default); without either, the automation
|
|
// package's own. PLAYWRIGHT_HOST names the installed automation package's node_modules, as for
|
|
// room.e2e.mjs. `--plant` is a falsification control: the adapter turns every frame the page
|
|
// shows as refused into one shown as verified (browser-sink §10), which a run must refuse.
|
|
// Nothing listens beyond loopback.
|
|
|
|
import fs from "node:fs";
|
|
import http from "node:http";
|
|
import path from "node:path";
|
|
import { createRequire } from "node:module";
|
|
import { spawn } from "node:child_process";
|
|
import { serve, browserArgs, memguardedBrowser, killStrays, tmpdir, until } from "./lib.mjs";
|
|
|
|
const args = Object.fromEntries(process.argv.slice(2).reduce((a, x, i, all) => (x.startsWith("--") ? [...a, [x.slice(2), all[i + 1] && !all[i + 1].startsWith("--") ? all[i + 1] : "1"]] : a), []));
|
|
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
const WEB = path.resolve(args.web || path.join(HERE, "..", "web"));
|
|
const BROWSER = args.browser || process.env.BROWSER_BIN || "";
|
|
const PLANT = args.plant === "1";
|
|
for (const f of ["binding/sink.html", "binding/sink.js", "room/binding.js", "pkg/wai_live_web.js", "pkg/wai_live_web_bg.wasm"]) {
|
|
if (!fs.existsSync(path.join(WEB, f))) {
|
|
console.error(`conform_sink: ${path.join(WEB, f)} is not there (e2e/run.sh builds web/pkg/)`);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
const require = createRequire(path.join(process.env.PLAYWRIGHT_HOST || path.join(HERE, "node_modules"), "x"));
|
|
const { chromium } = require("playwright");
|
|
|
|
const dir = tmpdir("conform-sink");
|
|
const srv = await serve(WEB);
|
|
const opts = { headless: true, args: browserArgs() };
|
|
if (BROWSER) opts.executablePath = memguardedBrowser(path.resolve(BROWSER), dir).executable;
|
|
const browser = await chromium.launch(opts);
|
|
const done = async (code) => {
|
|
await browser.close().catch(() => {});
|
|
await srv.close().catch(() => {});
|
|
killStrays();
|
|
process.exit(code);
|
|
};
|
|
process.on("SIGINT", () => done(130));
|
|
process.on("SIGTERM", () => done(143));
|
|
const page = await (await browser.newContext()).newPage();
|
|
const logs = [];
|
|
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
|
|
page.on("pageerror", (e) => logs.push(`[pageerror] ${e.stack || e}`));
|
|
await page.goto(`${srv.url}/binding/sink.html`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
|
try {
|
|
await until("the page's wasm is ready", 60000, () => page.evaluate(() => window.waiBrowserSink && window.waiBrowserSink.ready));
|
|
} catch (e) {
|
|
console.error(`conform_sink: ${e.message}\n${logs.join("\n")}`);
|
|
await done(1);
|
|
}
|
|
const ua = await page.evaluate(() => window.waiBrowserSink.version);
|
|
console.error(`conform_sink: browser ${browser.version()} (${BROWSER || "the automation package's own"}); page ${srv.url}/binding/sink.html`);
|
|
|
|
// The adapter: one call a POST, answered by the page. A call the page cannot answer is a 422
|
|
// with the reason, which the runner records as the case's answer.
|
|
const adapter = http.createServer((req, res) => {
|
|
if (req.method !== "POST") {
|
|
res.writeHead(405, { allow: "POST" });
|
|
return res.end();
|
|
}
|
|
const chunks = [];
|
|
req.on("data", (c) => chunks.push(c));
|
|
req.on("end", async () => {
|
|
let body;
|
|
try {
|
|
const call = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
body = await page.evaluate((c) => window.waiBrowserSink.answer(c), call);
|
|
if (PLANT) body = body.replaceAll('"state":"refused"', '"state":"verified"');
|
|
} catch (e) {
|
|
res.writeHead(422, { "content-type": "text/plain; charset=utf-8" });
|
|
return res.end(`the page did not answer: ${String(e.message || e).split("\n")[0]}\n`);
|
|
}
|
|
res.writeHead(200, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
|
|
res.end(body);
|
|
});
|
|
});
|
|
await new Promise((ok) => adapter.listen(Number(args.port || 0), "127.0.0.1", ok));
|
|
const url = `http://127.0.0.1:${adapter.address().port}/`;
|
|
console.log(`adapter ${url}${PLANT ? " (planted: frames shown refused answer verified)" : ""}`);
|
|
|
|
if (!args.wai) {
|
|
await new Promise(() => {}); // serve until interrupted
|
|
}
|
|
|
|
// Run the conformance runner against the adapter, in a child whose exit is awaited: a synchronous
|
|
// spawn would hold this process's event loop, and with it the adapter.
|
|
const run = (argv) =>
|
|
new Promise((ok) => {
|
|
const c = spawn(path.resolve(args.wai), argv, { stdio: "inherit" });
|
|
c.on("exit", (code, sig) => ok(code ?? (sig ? 128 : 1)));
|
|
});
|
|
const out = path.resolve(args.out || path.join(dir, "browser.wai-conform.json"));
|
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
const conform = ["conform", "sink", "--suite", "browser", "--impl", url, "--root", path.resolve(args["wai-dir"] || path.join(HERE, "..", "..", "..", "..", "..", "wai")), "--out", out, "--yes", "--text",
|
|
"--name", `the browser sink page (joule-moq-web web/binding/sink.html)${PLANT ? ", planted" : ""}`, "--version", `${browser.version()}; ${ua}`];
|
|
if (args.independent) conform.push("--independent");
|
|
let code = await run(conform);
|
|
if (code === 0) code = await run(["verify", "--conformant", "--root", conform[conform.indexOf("--root") + 1], out]);
|
|
console.log(`conform_sink: ${code === 0 ? "the page conforms, its record admitted" : `exit ${code}`}: ${out}`);
|
|
if (logs.some((l) => l.startsWith("[pageerror]"))) console.error(logs.join("\n"));
|
|
await done(code);
|