// run-node.mjs: the module of record on Node's engine, as a sink runs it. // // node run-node.mjs [--external DIR] [--only PREFIX] [--limit SECONDS] CORPUS MODULE... // // For each module, every case of the module corpus (corpus.json) on an instance of its own, // through sink.mjs (waism §5; §6 checks 11 to 13), in a worker that this script ends when one // run passes the wall-clock limit (waism §7, `wall-clock`; default 300 seconds a case). Each // output is held to the corpus, and each run prints the same line as the native host // (host/): its module, engine, the cases it held and its run digest. Every run digest must // be the same. // // Exit status 0 when every case of every run is the corpus's and every run digest is the // same; 1 otherwise; 2 for a usage or input error. import { readFileSync } from 'node:fs'; import { arch } from 'node:process'; import { Worker } from 'node:worker_threads'; import { judge, loadCases, sha256 } from './cases.mjs'; function usage(why) { if (why) console.error(`run-node.mjs: ${why}`); console.error('usage: node run-node.mjs [--external DIR] [--only PREFIX] [--limit SECONDS] CORPUS MODULE...'); process.exit(2); } const argv = process.argv.slice(2); let external = null, only = null, limit = 300; const rest = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--external') external = argv[++i] ?? usage(); else if (a === '--only') only = argv[++i] ?? usage(); else if (a === '--limit') limit = Number(argv[++i] ?? usage()); else if (a.startsWith('--')) usage(`no option ${a}`); else rest.push(a); } if (rest.length < 2 || !(limit > 0)) usage(); let cases; try { cases = loadCases(rest[0], { external, only }); } catch (e) { console.error(`run-node.mjs: ${e.message}`); process.exit(2); } /** Every case through one module in a worker, each under the limit. */ function runModule(moduleBytes) { return new Promise((done) => { const results = new Array(cases.length); const w = new Worker(new URL('./node-worker.mjs', import.meta.url), { workerData: { moduleBytes, cases } }); let timer = null, current = -1; const finish = () => { clearTimeout(timer); done(results); }; w.on('message', (m) => { if (m.start !== undefined) { current = m.start; clearTimeout(timer); timer = setTimeout(() => { results[current] = { sink: 'module_timeout', detail: `past ${limit} s` }; for (let i = current + 1; i < cases.length; i++) results[i] = { sink: 'module_timeout', detail: 'not run: an earlier case timed out' }; w.terminate().then(finish); }, limit * 1000); } else if (m.done !== undefined) { results[m.done] = m.r; } else if (m.end) { w.terminate().then(finish); } }); w.on('error', (e) => { for (let i = Math.max(current, 0); i < cases.length; i++) results[i] ??= { sink: 'module_trap', detail: e.message }; finish(); }); }); } const digests = new Map(); let failed = false; const engine = `node@${process.versions.node}/v8@${process.versions.v8}`; for (const path of rest.slice(1)) { const bytes = new Uint8Array(readFileSync(path)); const t0 = Date.now(); const results = await runModule(bytes); const { wrong, report, digest } = judge(cases, results); for (const line of report) console.log(line); console.log(`${path} (SHA-256 ${sha256(bytes)}) on ${engine}, ${arch}: ${cases.length - wrong} of ${cases.length} cases the corpus's; run digest ${digest}; ${((Date.now() - t0) / 1000).toFixed(1)} s`); failed ||= wrong > 0; digests.set(path, digest); } const distinct = new Set(digests.values()); if (distinct.size > 1) { console.log(`run-node.mjs: the runs differ: ${distinct.size} run digests`); failed = true; } else { console.log(`run-node.mjs: ${digests.size} runs, one run digest ${[...distinct][0]}`); } process.exit(failed ? 1 : 0);