map/tests/test-all.mjs

142 lines
4.3 KiB
JavaScript
Raw Normal View History

2026-08-08 17:41:30 +09:00
import { performance } from "node:perf_hooks";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
const suites = [
"core",
"terrain",
"admin",
"determinism-114514",
"determinism-12345",
"determinism-54321",
"determinism-777",
"determinism-999",
];
const concurrency = Math.max(1, Math.min(2, Number(process.env.TEST_CONCURRENCY) || 1));
const timeoutMs = 180_000;
const maxOutputBytes = 32 * 1024 * 1024;
const cwd = fileURLToPath(new URL(".", import.meta.url));
const testFile = fileURLToPath(new URL("./test.js", import.meta.url));
function runSuite(suite) {
return new Promise((resolve) => {
const started = performance.now();
console.error(`[test-all] start ${suite}`);
const child = spawn(process.execPath, [testFile, `--suite=${suite}`], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let outputOverflow = false;
let timedOut = false;
let forceKillTimer = null;
const append = (current, chunk) => {
if (outputOverflow) return current;
const next = current + chunk.toString("utf8");
if (Buffer.byteLength(next, "utf8") > maxOutputBytes) {
outputOverflow = true;
child.kill("SIGTERM");
return current;
}
return next;
};
child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
const timeout = setTimeout(() => {
timedOut = true;
child.kill("SIGTERM");
forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 2_000);
forceKillTimer.unref?.();
}, timeoutMs);
timeout.unref?.();
child.on("error", (error) => {
clearTimeout(timeout);
if (forceKillTimer) clearTimeout(forceKillTimer);
const seconds = Math.round((performance.now() - started) / 10) / 100;
console.error(`[test-all] error ${suite}: ${error.message}`);
resolve({
suite,
seconds,
fullMapGenerations: null,
failedAssertions: null,
status: null,
signal: null,
timedOut,
outputOverflow,
infrastructureError: error.message,
});
});
child.on("close", (status, signal) => {
clearTimeout(timeout);
if (forceKillTimer) clearTimeout(forceKillTimer);
const seconds = Math.round((performance.now() - started) / 10) / 100;
const output = `${stdout}\n${stderr}`;
const ng = (output.match(/^NG:/gm) || []).length;
const info = output.match(/INFO: suite=([^;]+); fullMapGenerations=(\d+); elapsedMs=(\d+)/);
const infrastructureError = outputOverflow
? `Output exceeded ${maxOutputBytes} bytes`
: null;
console.error(
`[test-all] end ${suite}: status=${status} signal=${signal || "none"} ` +
`failures=${ng} seconds=${seconds}`,
);
resolve({
suite,
seconds,
fullMapGenerations: info ? Number(info[2]) : null,
elapsedMsReported: info ? Number(info[3]) : null,
failedAssertions: ng,
status,
signal: signal || null,
timedOut,
outputOverflow,
infrastructureError,
});
});
});
}
const started = performance.now();
const queue = [...suites];
const completed = [];
async function worker() {
while (queue.length > 0) {
const suite = queue.shift();
if (!suite) return;
completed.push(await runSuite(suite));
}
}
await Promise.all(Array.from({ length: concurrency }, () => worker()));
completed.sort((a, b) => suites.indexOf(a.suite) - suites.indexOf(b.suite));
const wallSeconds = Math.round((performance.now() - started) / 10) / 100;
const failures = completed.reduce(
(sum, row) => sum + (Number.isFinite(row.failedAssertions) ? row.failedAssertions : 0),
0,
);
const infrastructureFailure = completed.some(
(row) => row.status !== 0 || row.signal || row.timedOut || row.outputOverflow || row.infrastructureError,
);
const underThreeMinutes = wallSeconds < 180 && completed.every(
(row) => row.seconds < 180 && !row.timedOut,
);
const result = {
underThreeMinutes,
wallSeconds,
concurrency,
failures,
infrastructureFailure,
suites: completed,
};
console.log(JSON.stringify(result, null, 2));
if (!underThreeMinutes || infrastructureFailure || failures > 0) process.exitCode = 1;