Files
SnapOtter/scripts/run-fuzz-gate.mjs
T

200 lines
6.0 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
import { execFileSync, spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const MAX_FUZZ_RUNS = 10_000;
const MAX_FUZZ_SEED = 2_147_483_647;
export const FUZZ_USAGE = "usage: pnpm test:fuzz [--runs <1-10000>] [--seed <0-2147483647>]";
function parseInteger(name, value, { min, max }) {
if (typeof value !== "string" || !/^-?\d+$/.test(value)) {
throw new Error(`${name} must be an integer`);
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed)) throw new Error(`${name} must be an integer`);
if (parsed < min || parsed > max) {
throw new Error(`${name} must be between ${min} and ${max}`);
}
return parsed;
}
export function parseFuzzArguments(argv) {
const result = { help: false, runs: undefined, seed: undefined };
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--") continue;
if (argument === "--help" || argument === "-h") {
result.help = true;
continue;
}
const [name, inlineValue] = argument.split("=", 2);
if (name !== "--runs" && name !== "--seed") {
throw new Error(`unknown argument: ${argument}\n${FUZZ_USAGE}`);
}
const nextValue = inlineValue ?? argv[index + 1];
if (inlineValue === undefined) index += 1;
if (nextValue === undefined) throw new Error(`${name} requires a value`);
const key = name === "--runs" ? "runs" : "seed";
if (result[key] !== undefined) throw new Error(`${name} may only be provided once`);
result[key] = parseInteger(name, nextValue, {
min: key === "runs" ? 1 : 0,
max: key === "runs" ? MAX_FUZZ_RUNS : MAX_FUZZ_SEED,
});
}
return result;
}
export function buildFuzzEnvironment(baseEnvironment, arguments_) {
const environment = {
...baseEnvironment,
FUZZ: "1",
VITEST_MAX_FORKS: "1",
};
if (arguments_.runs !== undefined) environment.FUZZ_RUNS = String(arguments_.runs);
if (arguments_.seed !== undefined) {
environment.FUZZ_SEED = String(arguments_.seed);
delete environment.FC_SEED;
}
return environment;
}
export function fuzzLockPath(cwd) {
let repositoryIdentity = resolve(cwd);
try {
const gitCommonDirectory = execFileSync("git", ["rev-parse", "--git-common-dir"], {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 5_000,
windowsHide: true,
}).trim();
if (gitCommonDirectory) repositoryIdentity = resolve(cwd, gitCommonDirectory);
} catch {
// Outside a Git repository, keep the resolved working directory identity.
}
const repoHash = createHash("sha256").update(repositoryIdentity).digest("hex");
return resolve(tmpdir(), `snapotter-fuzz-${repoHash}.lock`);
}
function describeLockOwner(lockPath) {
try {
const owner = JSON.parse(readFileSync(resolve(lockPath, "owner.json"), "utf8"));
return {
pid: Number.isInteger(owner.pid) ? owner.pid : "unknown",
startedAt: typeof owner.startedAt === "string" ? owner.startedAt : "unknown",
};
} catch {
return { pid: "unknown", startedAt: "unknown" };
}
}
export function acquireFuzzLock(lockPath, owner) {
try {
mkdirSync(lockPath);
} catch (error) {
if (error?.code !== "EEXIST") throw error;
const existing = describeLockOwner(lockPath);
throw new Error(
`another fuzz campaign is already running (pid=${existing.pid}, ` +
`startedAt=${existing.startedAt}, lock=${lockPath})`,
);
}
try {
writeFileSync(resolve(lockPath, "owner.json"), `${JSON.stringify(owner, null, 2)}\n`, {
flag: "wx",
});
} catch (error) {
rmSync(lockPath, { force: true, recursive: true });
throw error;
}
let released = false;
return {
release() {
if (released) return;
const currentOwner = JSON.parse(readFileSync(resolve(lockPath, "owner.json"), "utf8"));
if (currentOwner.token !== owner.token) {
throw new Error("refusing to release a fuzz lock owned by another process");
}
rmSync(lockPath, { recursive: true });
released = true;
},
};
}
function runVitest(cwd, environment) {
const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
const args = [
"exec",
"vitest",
"run",
"tests/integration/generated/fuzz-settings.test.ts",
"--reporter=verbose",
];
return new Promise((fulfill, reject) => {
const child = spawn(command, args, { cwd, env: environment, stdio: "inherit" });
const forwardSignal = (signal) => child.kill(signal);
const interrupt = () => forwardSignal("SIGINT");
const terminate = () => forwardSignal("SIGTERM");
process.once("SIGINT", interrupt);
process.once("SIGTERM", terminate);
const cleanup = () => {
process.off("SIGINT", interrupt);
process.off("SIGTERM", terminate);
};
child.once("error", (error) => {
cleanup();
reject(error);
});
child.once("exit", (code) => {
cleanup();
fulfill(code ?? 1);
});
});
}
async function main() {
const arguments_ = parseFuzzArguments(process.argv.slice(2));
if (arguments_.help) {
console.info(FUZZ_USAGE);
return 0;
}
const cwd = process.cwd();
const lockPath = fuzzLockPath(cwd);
const lock = acquireFuzzLock(lockPath, {
pid: process.pid,
startedAt: new Date().toISOString(),
token: randomUUID(),
});
try {
console.info(`[fuzz-runner] acquired exclusive lock ${lockPath}; VITEST_MAX_FORKS=1`);
return await runVitest(cwd, buildFuzzEnvironment(process.env, arguments_));
} finally {
lock.release();
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main()
.then((exitCode) => {
process.exitCode = exitCode;
})
.catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}