220 lines
5.5 KiB
JavaScript
220 lines
5.5 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
const workingDirectory = await fs.realpath(process.cwd());
|
|
const vitestCliPath = path.join(workingDirectory, "node_modules", "vitest", "vitest.mjs");
|
|
const unitTestsRoot = path.join(workingDirectory, "tests", "unit");
|
|
const serialListPath = path.join(unitTestsRoot, "serial-tests.txt");
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
let activeChild = null;
|
|
let isShuttingDown = false;
|
|
|
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
process.on(signal, () => {
|
|
void shutdown(signal).finally(() => {
|
|
process.exit(130);
|
|
});
|
|
});
|
|
}
|
|
|
|
function parseArgs(rawArgs) {
|
|
const parsed = {
|
|
help: false,
|
|
specFilters: [],
|
|
forwardedArgs: [],
|
|
};
|
|
|
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
const value = rawArgs[index];
|
|
|
|
if (value === "--") {
|
|
parsed.forwardedArgs.push(...rawArgs.slice(index + 1));
|
|
break;
|
|
}
|
|
|
|
if (value === "--help" || value === "-h") {
|
|
parsed.help = true;
|
|
continue;
|
|
}
|
|
|
|
if (value.startsWith("-")) {
|
|
parsed.forwardedArgs.push(...rawArgs.slice(index));
|
|
break;
|
|
}
|
|
|
|
parsed.specFilters.push(value);
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function normalizePathSegments(value) {
|
|
return value.replace(/\\/g, "/");
|
|
}
|
|
|
|
function printUsage() {
|
|
console.log(`
|
|
Vitest fast unit runner
|
|
|
|
Usage:
|
|
node scripts/run-vitest-unit-fast.mjs [spec-filter ...] [-- <vitest args>]
|
|
|
|
Runs all unit specs except entries in tests/unit/serial-tests.txt.
|
|
|
|
Environment:
|
|
VITEST_FAST_MAX_WORKERS Optional max workers for the fast pass
|
|
|
|
Examples:
|
|
npm run test:unit:fast
|
|
npm run test:unit:fast -- tests/unit/workfeed
|
|
npm run test:unit:fast -- --reporter=verbose
|
|
`);
|
|
}
|
|
|
|
async function collectSpecFiles(targetDirectory) {
|
|
const directoryEntries = await fs.readdir(targetDirectory, { withFileTypes: true });
|
|
const files = [];
|
|
|
|
for (const entry of directoryEntries) {
|
|
const absolutePath = path.join(targetDirectory, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
files.push(...(await collectSpecFiles(absolutePath)));
|
|
continue;
|
|
}
|
|
|
|
if (entry.isFile() && entry.name.endsWith(".spec.js")) {
|
|
files.push(absolutePath);
|
|
}
|
|
}
|
|
|
|
return files.sort((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
async function readSerialAllowlist() {
|
|
const contents = await fs.readFile(serialListPath, "utf8");
|
|
|
|
return new Set(
|
|
contents
|
|
.split(/\r?\n/u)
|
|
.map((line) => line.replace(/#.*/u, "").trim())
|
|
.filter(Boolean)
|
|
.map(normalizePathSegments)
|
|
);
|
|
}
|
|
|
|
function matchesFilter(relativePath, rawFilter) {
|
|
const normalizedRelativePath = normalizePathSegments(relativePath);
|
|
const normalizedFilter = normalizePathSegments(rawFilter.trim());
|
|
|
|
if (!normalizedFilter) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
normalizedRelativePath === normalizedFilter ||
|
|
normalizedRelativePath.endsWith(normalizedFilter) ||
|
|
normalizedRelativePath.includes(normalizedFilter)
|
|
);
|
|
}
|
|
|
|
function applyFilters(files, specFilters) {
|
|
if (specFilters.length === 0) {
|
|
return files;
|
|
}
|
|
|
|
return files.filter((absolutePath) => {
|
|
const relativePath = path.relative(workingDirectory, absolutePath);
|
|
return specFilters.some((filter) => matchesFilter(relativePath, filter));
|
|
});
|
|
}
|
|
|
|
function hasForwardedArg(name) {
|
|
return args.forwardedArgs.some((value) => value === name || value.startsWith(`${name}=`));
|
|
}
|
|
|
|
function resolveMaxWorkers() {
|
|
return (process.env.VITEST_FAST_MAX_WORKERS || "").trim();
|
|
}
|
|
|
|
async function runVitest(files) {
|
|
const enforcedArgs = [];
|
|
|
|
const configuredMaxWorkers = resolveMaxWorkers();
|
|
if (configuredMaxWorkers && !hasForwardedArg("--maxWorkers")) {
|
|
enforcedArgs.push(`--maxWorkers=${configuredMaxWorkers}`);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
activeChild = spawn(process.execPath, [vitestCliPath, "run", ...files, ...enforcedArgs, ...args.forwardedArgs], {
|
|
cwd: workingDirectory,
|
|
env: process.env,
|
|
stdio: "inherit",
|
|
windowsHide: true,
|
|
});
|
|
|
|
activeChild.on("close", (code) => {
|
|
activeChild = null;
|
|
resolve(code ?? 1);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function shutdown(signal) {
|
|
if (isShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
isShuttingDown = true;
|
|
console.error(`Received ${signal}. Stopping active Vitest fast run...`);
|
|
|
|
if (activeChild?.pid) {
|
|
if (process.platform === "win32") {
|
|
spawn("taskkill", ["/PID", String(activeChild.pid), "/T", "/F"], {
|
|
stdio: "ignore",
|
|
windowsHide: true,
|
|
});
|
|
} else {
|
|
activeChild.kill("SIGTERM");
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
if (args.help) {
|
|
printUsage();
|
|
return;
|
|
}
|
|
|
|
await fs.access(vitestCliPath);
|
|
|
|
const serialAllowlist = await readSerialAllowlist();
|
|
const discoveredFiles = await collectSpecFiles(unitTestsRoot);
|
|
const fastFiles = discoveredFiles.filter((absolutePath) => {
|
|
const relativePath = normalizePathSegments(path.relative(workingDirectory, absolutePath));
|
|
return !serialAllowlist.has(relativePath);
|
|
});
|
|
const selectedFiles = applyFilters(fastFiles, args.specFilters);
|
|
|
|
if (selectedFiles.length === 0) {
|
|
console.log("No parallel-safe unit spec files matched the provided filters.");
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
`Running ${selectedFiles.length} parallel-safe unit spec file(s); ${serialAllowlist.size} spec file(s) remain in the serial allowlist.`
|
|
);
|
|
|
|
const code = await runVitest(selectedFiles);
|
|
if (code !== 0) {
|
|
process.exitCode = code;
|
|
}
|
|
}
|
|
|
|
await main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
});
|