Files
pleno-vue/scripts/run-vitest-unit-fast.mjs

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;
});