Files
scriptc/tests/node-parallel/run.mjs
T
Chris Tate d813fdafd3 Compare termination signals and resolve the Node checkout path in the test/parallel runner
A run that a signal ends has a null exit code, so the runner recorded a
pass when Node and the executable ended by different signals, or when one
ended by a signal and the other with an exit code compared as null. The
verdict now compares both the exit code and the signal (verdict.mjs), and
records Node's signal in nodeExit as it does for the executable.

A relative --node path also made the minimal lane's fixtures symlink
relative to the copied tree, where it does not resolve. The checkout path
is now resolved first.

tests/harness/node-parallel-runner.test.ts covers both: the termination
comparison, and a relative --node path whose fixtures resolve from the
minimal tree.
2026-10-24 16:00:06 -05:00

136 lines
5.3 KiB
JavaScript

// Builds Node's test/parallel files with scriptc and runs them next to Node.
// See README.md for the options.
import { spawn } from "node:child_process";
import {
cpSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { sameTermination, termination } from "./verdict.mjs";
const here = dirname(fileURLToPath(import.meta.url));
const repo = resolve(here, "../..");
const { values } = parseArgs({
options: {
node: { type: "string" },
pattern: { type: "string" },
common: { type: "string", default: "minimal" },
out: { type: "string", default: join(repo, "node_modules/.cache/scriptc-node-parallel") },
cli: { type: "string", default: join(repo, "packages/cli/dist/main.js") },
jobs: { type: "string", default: "4" },
},
});
if (!values.node || !values.pattern || !["minimal", "node"].includes(values.common)) {
console.error(
"usage: node tests/node-parallel/run.mjs --node <node checkout> --pattern <regex> [--common minimal|node] [--out <dir>] [--cli <scriptc main.js>] [--jobs <n>]",
);
process.exit(2);
}
// Absolute, so the minimal lane's fixtures symlink resolves from the tree.
const nodeRoot = resolve(values.node);
const nodeTest = existsSync(join(nodeRoot, "test", "parallel")) ? join(nodeRoot, "test") : nodeRoot;
const pattern = new RegExp(values.pattern);
const out = resolve(values.out);
mkdirSync(out, { recursive: true });
// The minimal lane copies the selected files into a tree whose test/common is
// this directory's index.cjs and wpt.cjs, next to the checkout's fixtures and
// common/fixtures.js.
let testRoot = nodeTest;
const files = readdirSync(join(nodeTest, "parallel"))
.filter((name) => name.endsWith(".js") && pattern.test(name))
.sort();
if (values.common === "minimal") {
testRoot = join(out, "tree", "test");
rmSync(join(out, "tree"), { recursive: true, force: true });
mkdirSync(join(testRoot, "parallel"), { recursive: true });
mkdirSync(join(testRoot, "common"), { recursive: true });
for (const name of files)
cpSync(join(nodeTest, "parallel", name), join(testRoot, "parallel", name));
symlinkSync(join(nodeTest, "fixtures"), join(testRoot, "fixtures"));
cpSync(join(nodeTest, "common", "fixtures.js"), join(testRoot, "common", "fixtures.js"));
cpSync(join(here, "common", "index.cjs"), join(testRoot, "common", "index.js"));
cpSync(join(here, "common", "wpt.cjs"), join(testRoot, "common", "wpt.js"));
}
function run(cmd, args, opts) {
return new Promise((done) => {
const child = spawn(cmd, args, { ...opts, stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => (stdout += d));
child.stderr.on("data", (d) => (stderr += d));
const timer = setTimeout(() => child.kill("SIGKILL"), opts.timeout);
child.on("close", (code, signal) => {
clearTimeout(timer);
done({ code, signal, stdout, stderr });
});
});
}
const firstCause = (text) =>
(
text.split("\n").find((line) => /error|Error|assert|SC\d{4}/.test(line)) ??
text.split("\n")[0] ??
""
)
.replaceAll(testRoot, "test")
.trim()
.slice(0, 300);
async function one(file) {
const dir = join(testRoot, "parallel");
const source = readFileSync(join(dir, file), "utf8");
const flags = (/^\/\/ Flags: (.*)$/m.exec(source)?.[1] ?? "").split(/\s+/).filter(Boolean);
const node = await run(process.execPath, [...flags, file], { cwd: dir, timeout: 120_000 });
const exe = join(out, "bin", `${file}.exe`);
mkdirSync(join(out, "bin"), { recursive: true });
const build = await run(process.execPath, [values.cli, "build", file, "-o", exe], {
cwd: dir,
timeout: 900_000,
});
const result = { file, flags: flags.join(" "), nodeExit: termination(node) };
if (build.code !== 0) {
result.verdict = "refused";
result.cause = firstCause(build.stdout + build.stderr);
return result;
}
const native = await run(exe, [], { cwd: dir, timeout: 120_000 });
result.exit = termination(native);
result.verdict = sameTermination(node, native) ? "pass" : "fail";
if (result.verdict !== "pass") result.cause = firstCause(native.stderr + native.stdout);
return result;
}
const results = [];
const queue = [...files];
await Promise.all(
Array.from({ length: Number(values.jobs) }, async () => {
for (let file = queue.shift(); file !== undefined; file = queue.shift()) {
const result = await one(file);
results.push(result);
console.log(`${result.verdict.padEnd(8)} ${file}${result.cause ? ` ${result.cause}` : ""}`);
}
}),
);
results.sort((a, b) => (a.file < b.file ? -1 : 1));
const count = (verdict) => results.filter((r) => r.verdict === verdict).length;
const table = [
"| file | verdict | first cause |",
"| --- | --- | --- |",
...results.map((r) => `| ${r.file} | ${r.verdict} | ${(r.cause ?? "").replaceAll("|", "\\|")} |`),
"",
`pass ${count("pass")}, fail ${count("fail")}, refused ${count("refused")} of ${results.length} (test/common: ${values.common})`,
];
writeFileSync(join(out, "results.json"), `${JSON.stringify(results, null, 2)}\n`);
writeFileSync(join(out, "results.md"), `${table.join("\n")}\n`);
console.log(table.at(-1));