Files
pleno-vue/scripts/run-vitest-unit-batches.mjs
T
Jeppe Bundgaard b5a8199472 Reduce default Vitest batch size to 5, enforce single-threaded execution, and refactor unit test wait conditions:
- Adjusted `run-vitest-unit-batches.mjs` to lower default batch size from 10 to 5 and enforce single-threaded execution for more predictable test behavior.
- Updated GitHub workflows to align with new batch size defaults.
- Introduced `waitForCondition` helper in unit tests to replace arbitrary delays with predicate-based waiting.
- Refactored `useEdgeGatewayTerminal` for improved session handling, including idle delay for shell polling and cleanup of request queuing logic.
2026-04-14 17:55:38 +02:00

266 lines
6.7 KiB
JavaScript

import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
const workingDirectory = process.cwd();
const vitestCliPath = path.join(workingDirectory, "node_modules", "vitest", "vitest.mjs");
const unitTestsRoot = path.join(workingDirectory, "tests", "unit");
const args = parseArgs(process.argv.slice(2));
const batchSize = normalizePositiveInt(args.batchSize ?? process.env.VITEST_BATCH_SIZE, 5);
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,
batchSize: null,
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 === "--batch-size" && rawArgs[index + 1]) {
parsed.batchSize = rawArgs[index + 1];
index += 1;
continue;
}
if (value.startsWith("--batch-size=")) {
parsed.batchSize = value.slice("--batch-size=".length);
continue;
}
if (value.startsWith("-")) {
parsed.forwardedArgs.push(...rawArgs.slice(index));
break;
}
parsed.specFilters.push(value);
}
return parsed;
}
function normalizePositiveInt(value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function normalizePathSegments(value) {
return value.replace(/\\/g, "/");
}
function printUsage() {
console.log(`
Vitest unit batch runner
Usage:
node scripts/run-vitest-unit-batches.mjs [spec-filter ...] [options] [-- <vitest args>]
Options:
--batch-size <n> Spec files per batch. Default: 5
-h, --help Show help
Examples:
npm run test:unit
npm run test:unit -- tests/unit/select-vehicle-form-pos.spec.js
npm run test:unit -- tests/unit/select-vehicle-form-pos.spec.js -- --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));
}
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 chunkFiles(files, size) {
const chunks = [];
for (let index = 0; index < files.length; index += size) {
chunks.push({
batch: Math.floor(index / size) + 1,
files: files.slice(index, index + size),
});
}
return chunks;
}
function formatDuration(durationMs) {
return `${(durationMs / 1000).toFixed(1)}s`;
}
function hasForwardedArg(name) {
return args.forwardedArgs.some((value) => value === name || value.startsWith(`${name}=`));
}
async function runBatch(chunk, totalBatches) {
console.log(`[vitest-batch] Running batch ${chunk.batch}/${totalBatches} with ${chunk.files.length} spec file(s).`);
const startedAt = Date.now();
const enforcedArgs = [];
if (!hasForwardedArg("--maxWorkers")) {
enforcedArgs.push("--maxWorkers=1");
}
if (!hasForwardedArg("--fileParallelism") && !hasForwardedArg("--no-file-parallelism")) {
enforcedArgs.push("--no-file-parallelism");
}
return new Promise((resolve) => {
activeChild = spawn(
process.execPath,
[vitestCliPath, "run", ...chunk.files, ...enforcedArgs, ...args.forwardedArgs],
{
cwd: workingDirectory,
env: process.env,
stdio: "inherit",
windowsHide: true,
}
);
activeChild.on("close", (code) => {
activeChild = null;
resolve({
batch: chunk.batch,
files: chunk.files.length,
code: code ?? 1,
durationMs: Date.now() - startedAt,
});
});
});
}
function printSummary(results, totalFiles) {
console.log("\nVitest batch summary");
console.log(`Total spec files: ${totalFiles}`);
console.log("batch\tstatus\tfiles\tduration");
for (const result of results) {
const status = result.code === 0 ? "passed" : "failed";
console.log(`${result.batch}\t${status}\t${result.files}\t${formatDuration(result.durationMs)}`);
}
}
async function shutdown(signal) {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
console.error(`Received ${signal}. Stopping active Vitest batch...`);
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 discoveredFiles = await collectSpecFiles(unitTestsRoot);
const selectedFiles = applyFilters(discoveredFiles, args.specFilters);
if (selectedFiles.length === 0) {
throw new Error("No unit spec files matched the provided filters.");
}
const chunks = chunkFiles(selectedFiles, batchSize);
console.log(
`Running ${selectedFiles.length} unit spec file(s) in ${chunks.length} batch(es) at ${batchSize} file(s) per batch.`
);
const results = [];
for (const chunk of chunks) {
results.push(await runBatch(chunk, chunks.length));
}
printSummary(results, selectedFiles.length);
const failedBatches = results.filter((result) => result.code !== 0);
if (failedBatches.length > 0) {
throw new Error(`Vitest batches failed: ${failedBatches.map((result) => result.batch).join(", ")}`);
}
}
await main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});