Files
scriptc/tests/node-parallel/run.mjs
T
Chris Tate f67678b422 Pipe decoded socket strings and give lane jobs their own tmpdirs
After socket.setEncoding(enc), socket.pipe(destination) wrote the raw
bytes it read to the destination. It now writes the decoded strings, as
Node's pipe writes each string chunk: the destination receives their
UTF-8 bytes, and a partial character the decoder holds at EOF is written
before the destination ends.

tests/node-parallel/run.mjs runs each job's files with its own
TEST_THREAD_ID, as Node's test runner does, so test/common/tmpdir gives
concurrent files different directories instead of one shared .tmp.0.
2026-10-29 23:16:13 -05:00

142 lines
5.8 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, wpt.cjs and tmpdir.cjs, next to the checkout's
// fixtures, common/fixtures.js, common/dns.js and common/internet.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"));
cpSync(join(here, "common", "tmpdir.cjs"), join(testRoot, "common", "tmpdir.js"));
for (const helper of ["dns.js", "internet.js"])
cpSync(join(nodeTest, "common", helper), join(testRoot, "common", helper));
}
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);
// Each job runs its files with its own TEST_THREAD_ID, as Node's test runner
// does, so test/common/tmpdir gives concurrent files different directories.
async function one(file, job) {
const env = { ...process.env, TEST_THREAD_ID: String(job) };
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, env, 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, env, 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 (_, job) => {
for (let file = queue.shift(); file !== undefined; file = queue.shift()) {
const result = await one(file, job);
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));