Crafter.Build/tests/WasiBrowserRuntimeExit/fixture/harness.mjs

167 lines
7.4 KiB
JavaScript
Raw Normal View History

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Runs wasi-runtime/runtime.js (argv[2], copied to a .mjs so that node loads
// it as a module) once per scenario against the module assembled below, and
// prints one line per scenario for the test to check. Between _start and the
// call that ends it, the module has the frames C++ built with
// -fwasm-exceptions puts there: a destructor (catch_all_ref, then
// throw_ref), a catch (...) (catch_all) and a noexcept function (catch_all,
// then std::terminate). Each reports through env.trace when it runs, and on
// the way out of an exit or a park none of them may.
import { pathToFileURL } from "node:url";
const uleb = (n) => {
const out = [];
do {
const byte = n & 0x7f;
n >>>= 7;
out.push(n ? byte | 0x80 : byte);
} while (n);
return out;
};
const sleb = (n) => {
const out = [];
for (;;) {
const byte = n & 0x7f;
n >>= 7;
const done = (n === 0 && !(byte & 0x40)) || (n === -1 && (byte & 0x40));
out.push(done ? byte : byte | 0x80);
if (done) return out;
}
};
const vec = (items) => [...uleb(items.length), ...items.flat()];
const name = (text) => vec([...text].map((c) => [c.charCodeAt(0)]));
const section = (id, items) => {
const bytes = vec(items);
return [id, ...uleb(bytes.length), ...bytes];
};
const UNREACHABLE = 0x00, BLOCK = 0x02, IF = 0x04, ELSE = 0x05, THROW = 0x08, THROW_REF = 0x0a, END = 0x0b;
const RETURN = 0x0f, CALL = 0x10, TRY_TABLE = 0x1f, LOCAL_GET = 0x20, LOCAL_SET = 0x21, GLOBAL_GET = 0x23;
const GLOBAL_SET = 0x24, I32_CONST = 0x41, I32_EQ = 0x46;
const I32 = 0x7f, EXNREF = 0x69, NO_RESULT = 0x40, FUNC_TYPE = 0x60, CATCH_ALL = 0x02, CATCH_ALL_REF = 0x03;
const code = (locals, instructions) => {
const bytes = [...vec(locals), ...instructions, END];
return [...uleb(bytes.length), ...bytes];
};
// Types, imported functions, then the module's own.
const TAKES_I32 = 0, NOTHING = 1, GIVES_I32 = 2;
const PROC_EXIT = 0, PARK = 1, SCENARIO = 2, TRACE = 3;
const END_WITH = 4, NOEXCEPT = 5, CATCHES_ALL = 6, CLEANS_UP = 7, START = 8, FRAME = 9, QUIT = 10;
// What env.trace reports.
const DESTRUCTOR = 1, CATCH = 2, TERMINATE = 3, AFTER_PARK = 4;
// What env.scenario selects in _start.
const EXIT = 1, PARK_MAIN = 2, THROW_UNCAUGHT = 4, CRASH = 5;
const PARK_CODE = -1;
const ifScenario = (scenario, instructions) => [LOCAL_GET, 0, I32_CONST, ...sleb(scenario), I32_EQ, IF, NO_RESULT, ...instructions, END];
const module = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
...section(1, [
[FUNC_TYPE, ...vec([[I32]]), ...vec([])],
[FUNC_TYPE, ...vec([]), ...vec([])],
[FUNC_TYPE, ...vec([]), ...vec([[I32]])],
]),
...section(2, [
[...name("wasi_snapshot_preview1"), ...name("proc_exit"), 0x00, TAKES_I32],
[...name("crafter_runtime"), ...name("park"), 0x00, NOTHING],
[...name("env"), ...name("scenario"), 0x00, GIVES_I32],
[...name("env"), ...name("trace"), 0x00, TAKES_I32],
]),
...section(3, [[TAKES_I32], [TAKES_I32], [TAKES_I32], [TAKES_I32], [NOTHING], [GIVES_I32], [TAKES_I32]]),
...section(5, [[0x00, 0x01]]),
// One tag, for the exception nothing catches.
...section(13, [[0x00, TAKES_I32]]),
...section(6, [[I32, 0x01, I32_CONST, 0x00, END]]),
...section(7, [
[...name("memory"), 0x02, 0],
[...name("_start"), 0x00, START],
[...name("frame"), 0x00, FRAME],
[...name("quit"), 0x00, QUIT],
]),
...section(10, [
// END_WITH(code): park for PARK_CODE, else proc_exit(code).
code([], [LOCAL_GET, 0, I32_CONST, ...sleb(PARK_CODE), I32_EQ, IF, NO_RESULT, CALL, PARK, ELSE, LOCAL_GET, 0, CALL, PROC_EXIT, END]),
// NOEXCEPT(code): anything thrown through it is std::terminate.
code([], [
BLOCK, NO_RESULT, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL, 0]]), LOCAL_GET, 0, CALL, END_WITH, END, RETURN, END,
I32_CONST, TERMINATE, CALL, TRACE, UNREACHABLE,
]),
// CATCHES_ALL(code): try { NOEXCEPT(code); } catch (...) {}
code([], [
BLOCK, NO_RESULT, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL, 0]]), LOCAL_GET, 0, CALL, NOEXCEPT, END, RETURN, END,
I32_CONST, CATCH, CALL, TRACE,
]),
// CLEANS_UP(code): a frame with a destructor around CATCHES_ALL(code).
code([[...uleb(1), EXNREF]], [
BLOCK, EXNREF, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL_REF, 0]]), LOCAL_GET, 0, CALL, CATCHES_ALL, END, RETURN, END,
LOCAL_SET, 1, I32_CONST, DESTRUCTOR, CALL, TRACE, LOCAL_GET, 1, THROW_REF,
]),
// _start: what env.scenario says; anything else returns from main.
code([[...uleb(1), I32]], [
CALL, SCENARIO, LOCAL_SET, 0,
...ifScenario(EXIT, [I32_CONST, 3, CALL, CLEANS_UP]),
...ifScenario(PARK_MAIN, [I32_CONST, 42, GLOBAL_SET, 0, I32_CONST, ...sleb(PARK_CODE), CALL, CLEANS_UP, I32_CONST, AFTER_PARK, CALL, TRACE]),
...ifScenario(THROW_UNCAUGHT, [I32_CONST, 9, THROW, 0]),
...ifScenario(CRASH, [UNREACHABLE]),
]),
// frame(): the state _start left, as a later frame callback reads it.
code([], [GLOBAL_GET, 0]),
// quit(code): an exit from a later call, through the same frames.
code([], [LOCAL_GET, 0, CALL, CLEANS_UP]),
]),
]);
// The standardized encoding (try_table) is what the module and runtime.js
// both depend on; an older node has only the legacy one.
const exnrefProbe = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 8, 1, 6, 0, 31, 64, 0, 11, 11]);
if (!WebAssembly.validate(exnrefProbe)) {
console.log("SKIP: this node has no WebAssembly exception handling");
process.exit(0);
}
if (!WebAssembly.validate(module)) {
console.log("FAIL: the fixture module does not validate");
process.exit(1);
}
// What runtime.js expects of a page: no variants.json or files.json, so it
// loads CRAFTER_WASM_URL.
globalThis.window = globalThis;
globalThis.addEventListener = () => {};
window.CRAFTER_WASM_URL = "fixture.wasm";
globalThis.fetch = async (url) => {
if (String(url).split("?")[0] === "fixture.wasm") return new Response(module, { headers: { "Content-Type": "application/wasm" } });
return new Response(null, { status: 404 });
};
const runtime = pathToFileURL(process.argv[2]).href;
const run = async (label, scenario, after = () => "") => {
const trace = [];
window.crafter_webbuild_env = { scenario: () => scenario, trace: (what) => { trace.push(what); } };
delete window.crafter_wasi;
let outcome = "ended";
try {
// A query makes it a module of its own, run afresh.
await import(`${runtime}?scenario=${scenario}`);
} catch (e) {
outcome = `rejected with ${e?.constructor?.name}`;
}
const wasi = window.crafter_wasi;
console.log(`${label}: ${outcome}, exitCode=${wasi?.exitCode} parked=${wasi?.parked} trace=[${trace}]${after(wasi, trace)}`);
};
await run("exit", EXIT);
await run("park", PARK_MAIN, (wasi, trace) => {
const frame = wasi.instance.exports.frame();
let quit = "returned";
try {
wasi.instance.exports.quit(7);
} catch (e) {
quit = `threw ${e.constructor.name}`;
}
return `; frame()=${frame}; quit(7) ${quit}, exitCode=${wasi.exitCode} trace=[${trace}]`;
});
await run("return", 3);
await run("uncaught exception", THROW_UNCAUGHT);
await run("crash", CRASH);