Deleted the entire `ws` library and its associated files. This likely reflects a move away from the WebSocket library, possibly signaling an alternate implementation or unused/legacy code cleanup.
748 lines
21 KiB
JavaScript
748 lines
21 KiB
JavaScript
import { execFile, spawn } from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import readline from "node:readline";
|
|
import { promisify } from "node:util";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const workingDirectory = process.cwd();
|
|
const playwrightCliPath = path.join(workingDirectory, "node_modules", "@playwright", "test", "cli.js");
|
|
const projects = ["chromium-desktop", "chromium-mobile"];
|
|
const outputDirectory = path.join(workingDirectory, "output", "playwright", "batched-chromium");
|
|
const testListDirectory = path.join(outputDirectory, "test-lists");
|
|
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const batchSize = normalizePositiveInt(args.batchSize ?? process.env.PLAYWRIGHT_BATCH_SIZE, 25);
|
|
const shardWorkers = normalizePositiveInt(
|
|
args.workers ?? process.env.PLAYWRIGHT_BATCH_WORKERS ?? process.env.PLAYWRIGHT_WORKERS,
|
|
2
|
|
);
|
|
const devPort = normalizePositiveInt(args.port ?? process.env.PLAYWRIGHT_BATCH_DEV_PORT ?? process.env.PLAYWRIGHT_DEV_PORT, 5193);
|
|
const baseURL = `http://localhost:${devPort}`;
|
|
|
|
let activePlaywrightChild = null;
|
|
let activeDevServerProcess = 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 = {
|
|
rerunFailed: false,
|
|
dryRun: false,
|
|
help: false,
|
|
batchSize: null,
|
|
workers: null,
|
|
port: null,
|
|
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 === "--rerun-failed") {
|
|
parsed.rerunFailed = true;
|
|
continue;
|
|
}
|
|
|
|
if (value === "--dry-run") {
|
|
parsed.dryRun = true;
|
|
continue;
|
|
}
|
|
|
|
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 === "--workers" && rawArgs[index + 1]) {
|
|
parsed.workers = rawArgs[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (value.startsWith("--workers=")) {
|
|
parsed.workers = value.slice("--workers=".length);
|
|
continue;
|
|
}
|
|
|
|
if (value === "--port" && rawArgs[index + 1]) {
|
|
parsed.port = rawArgs[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (value.startsWith("--port=")) {
|
|
parsed.port = value.slice("--port=".length);
|
|
continue;
|
|
}
|
|
|
|
parsed.forwardedArgs.push(value);
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function normalizePositiveInt(value, fallback) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
function stripAnsi(input) {
|
|
return input.replace(/\u001b\[[0-9;]*m/g, "");
|
|
}
|
|
|
|
function formatDuration(durationMs) {
|
|
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
}
|
|
|
|
function chunkTests(testLines, size) {
|
|
const chunks = [];
|
|
for (let index = 0; index < testLines.length; index += size) {
|
|
const shard = Math.floor(index / size) + 1;
|
|
const lines = testLines.slice(index, index + size);
|
|
chunks.push({
|
|
shard,
|
|
start: index + 1,
|
|
end: index + lines.length,
|
|
lines,
|
|
});
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
function printUsage() {
|
|
console.log(`
|
|
Playwright batched chromium runner
|
|
|
|
Usage:
|
|
node scripts/run-playwright-batched-chromium.mjs [options] [-- <playwright args>]
|
|
|
|
Options:
|
|
--batch-size <n> Tests per chunk. Default: 25
|
|
--workers <n> PLAYWRIGHT_WORKERS for primary runs. Default: 2
|
|
--port <n> Vite dev server port. Default: 5193
|
|
--rerun-failed Re-run failed chunks with PLAYWRIGHT_WORKERS=1
|
|
--dry-run Compute and print chunk plan without executing tests
|
|
-h, --help Show help
|
|
`);
|
|
}
|
|
|
|
function validateForwardedArgs(forwardedArgs) {
|
|
for (const value of forwardedArgs) {
|
|
if (value === "--list" || value.startsWith("--list=")) {
|
|
throw new Error("--list is managed by this script and cannot be forwarded.");
|
|
}
|
|
|
|
if (value === "--project" || value.startsWith("--project=")) {
|
|
throw new Error("--project is managed by this script and cannot be forwarded.");
|
|
}
|
|
|
|
if (value === "--shard" || value.startsWith("--shard=")) {
|
|
throw new Error("--shard is managed by this script and cannot be forwarded.");
|
|
}
|
|
|
|
if (value === "--test-list" || value.startsWith("--test-list=")) {
|
|
throw new Error("--test-list is managed by this script and cannot be forwarded.");
|
|
}
|
|
|
|
if (value === "--test-list-invert" || value.startsWith("--test-list-invert=")) {
|
|
throw new Error("--test-list-invert is managed by this script and cannot be forwarded.");
|
|
}
|
|
}
|
|
}
|
|
|
|
function prefixStream(stream, prefix) {
|
|
const lineReader = readline.createInterface({ input: stream });
|
|
lineReader.on("line", (line) => {
|
|
process.stdout.write(`[${prefix}] ${line}\n`);
|
|
});
|
|
}
|
|
|
|
async function getListeningProcessOnWindows(port) {
|
|
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], { cwd: workingDirectory });
|
|
const match = stdout.match(new RegExp(`^\\s*TCP\\s+[^\\s]+:${port}\\s+[^\\s]+\\s+LISTENING\\s+(\\d+)\\s*$`, "mi"));
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const pid = Number(match[1]);
|
|
const command = `Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}' | Select-Object -ExpandProperty CommandLine`;
|
|
const processResult = await execFileAsync("powershell", ["-NoProfile", "-Command", command], {
|
|
cwd: workingDirectory,
|
|
}).catch(() => ({ stdout: "" }));
|
|
|
|
return {
|
|
id: pid,
|
|
commandLine: processResult.stdout.trim(),
|
|
};
|
|
}
|
|
|
|
async function getListeningProcessOnUnix(port) {
|
|
try {
|
|
const { stdout } = await execFileAsync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp", "-Fc"], {
|
|
cwd: workingDirectory,
|
|
});
|
|
const pidMatch = stdout.match(/^p(\d+)$/m);
|
|
const commandMatch = stdout.match(/^c(.+)$/m);
|
|
if (!pidMatch) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: Number(pidMatch[1]),
|
|
commandLine: commandMatch ? commandMatch[1] : "",
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getListeningProcess(port) {
|
|
if (process.platform === "win32") {
|
|
return getListeningProcessOnWindows(port);
|
|
}
|
|
|
|
return getListeningProcessOnUnix(port);
|
|
}
|
|
|
|
async function killProcessTree(pid) {
|
|
if (!pid) {
|
|
return;
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
await execFileAsync("taskkill", ["/PID", String(pid), "/T", "/F"], { cwd: workingDirectory }).catch(() => {});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
process.kill(-pid, "SIGTERM");
|
|
} catch {
|
|
try {
|
|
process.kill(pid, "SIGTERM");
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
async function waitForServerReady(url, timeoutMs = 120_000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const response = await fetch(url, { redirect: "manual" });
|
|
if (response.status < 500) {
|
|
return;
|
|
}
|
|
} catch {
|
|
// keep polling
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for Vite server at ${url}.`);
|
|
}
|
|
|
|
async function startDevServer() {
|
|
const existingProcess = await getListeningProcess(devPort);
|
|
if (existingProcess) {
|
|
if (!/vite(?:\.js)?/i.test(existingProcess.commandLine || "")) {
|
|
throw new Error(`Port ${devPort} is already in use by a non-Vite process: ${existingProcess.commandLine}`);
|
|
}
|
|
|
|
await killProcessTree(existingProcess.id);
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
activeDevServerProcess =
|
|
process.platform === "win32"
|
|
? spawn(
|
|
"cmd.exe",
|
|
["/d", "/s", "/c", `npm.cmd run dev -- --host localhost --port ${devPort} --strictPort`],
|
|
{
|
|
cwd: workingDirectory,
|
|
env: {
|
|
...process.env,
|
|
PLAYWRIGHT: "1",
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
windowsHide: true,
|
|
}
|
|
)
|
|
: spawn("npm", ["run", "dev", "--", "--host", "localhost", "--port", String(devPort), "--strictPort"], {
|
|
cwd: workingDirectory,
|
|
env: {
|
|
...process.env,
|
|
PLAYWRIGHT: "1",
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
prefixStream(activeDevServerProcess.stdout, "dev-server");
|
|
prefixStream(activeDevServerProcess.stderr, "dev-server:err");
|
|
|
|
await waitForServerReady(baseURL);
|
|
console.log(`Vite dev server is ready at ${baseURL}.`);
|
|
}
|
|
|
|
async function stopDevServer() {
|
|
if (!activeDevServerProcess) {
|
|
return;
|
|
}
|
|
|
|
await killProcessTree(activeDevServerProcess.pid);
|
|
activeDevServerProcess = null;
|
|
}
|
|
|
|
async function runPlaywrightCommand(commandArgs, envOverrides = {}, inheritOutput = false) {
|
|
if (inheritOutput) {
|
|
return new Promise((resolve) => {
|
|
activePlaywrightChild = spawn(process.execPath, [playwrightCliPath, ...commandArgs], {
|
|
cwd: workingDirectory,
|
|
env: {
|
|
...process.env,
|
|
...envOverrides,
|
|
},
|
|
stdio: "inherit",
|
|
windowsHide: true,
|
|
});
|
|
|
|
activePlaywrightChild.on("close", (code) => {
|
|
activePlaywrightChild = null;
|
|
resolve({
|
|
code: code ?? 1,
|
|
stdout: "",
|
|
stderr: "",
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
try {
|
|
const result = await execFileAsync(process.execPath, [playwrightCliPath, ...commandArgs], {
|
|
cwd: workingDirectory,
|
|
env: {
|
|
...process.env,
|
|
...envOverrides,
|
|
},
|
|
maxBuffer: 30 * 1024 * 1024,
|
|
windowsHide: true,
|
|
});
|
|
|
|
return {
|
|
code: 0,
|
|
stdout: result.stdout,
|
|
stderr: result.stderr,
|
|
};
|
|
} catch (error) {
|
|
const stdout = typeof error.stdout === "string" ? error.stdout : "";
|
|
const stderr = typeof error.stderr === "string" ? error.stderr : "";
|
|
const code = typeof error.code === "number" ? error.code : 1;
|
|
return {
|
|
code,
|
|
stdout,
|
|
stderr,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function preflightCollectTests(forwardedArgs) {
|
|
const commandArgs = [
|
|
"test",
|
|
...projects.flatMap((project) => ["--project", project]),
|
|
"--list",
|
|
...forwardedArgs,
|
|
];
|
|
const result = await runPlaywrightCommand(
|
|
commandArgs,
|
|
{
|
|
PLAYWRIGHT_BASE_URL: baseURL,
|
|
PLAYWRIGHT: "1",
|
|
},
|
|
false
|
|
);
|
|
|
|
const combinedOutput = stripAnsi(`${result.stdout}\n${result.stderr}`);
|
|
const totalMatch = combinedOutput.match(/Total:\s+(\d+)\s+tests?/i);
|
|
if (!totalMatch) {
|
|
throw new Error(`Failed to parse Playwright test count from --list output.\n${combinedOutput}`);
|
|
}
|
|
|
|
const listedTests = combinedOutput
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter((line) => line.startsWith("["));
|
|
|
|
const parsedTotal = Number(totalMatch[1]);
|
|
if (listedTests.length === 0) {
|
|
throw new Error("Playwright --list returned zero runnable tests.");
|
|
}
|
|
|
|
if (listedTests.length !== parsedTotal) {
|
|
console.warn(
|
|
`Warning: --list reported ${parsedTotal} tests but ${listedTests.length} test lines were parsed. Using parsed lines.`
|
|
);
|
|
}
|
|
|
|
return {
|
|
totalTests: listedTests.length,
|
|
listedTests,
|
|
};
|
|
}
|
|
|
|
async function writeTestListFile(passLabel, chunk, totalShards) {
|
|
await fs.mkdir(testListDirectory, { recursive: true });
|
|
const filename = `${passLabel}-shard-${chunk.shard}-of-${totalShards}.txt`;
|
|
const targetPath = path.join(testListDirectory, filename);
|
|
await fs.writeFile(targetPath, `${chunk.lines.join("\n")}\n`, "utf8");
|
|
return targetPath;
|
|
}
|
|
|
|
async function runShard({ chunk, totalShards, workers, passLabel, forwardedArgs }) {
|
|
const testListPath = await writeTestListFile(passLabel, chunk, totalShards);
|
|
const commandArgs = [
|
|
"test",
|
|
...projects.flatMap((project) => ["--project", project]),
|
|
"--test-list",
|
|
testListPath,
|
|
...forwardedArgs,
|
|
];
|
|
|
|
const isRerun = passLabel === "rerun";
|
|
const artifactNamespace = isRerun
|
|
? `batched-chromium-rerun-shard-${chunk.shard}-of-${totalShards}`
|
|
: `batched-chromium-shard-${chunk.shard}-of-${totalShards}`;
|
|
|
|
console.log(
|
|
`[${passLabel}] Running shard ${chunk.shard}/${totalShards} (${chunk.start}-${chunk.end}, ${chunk.lines.length} tests, workers=${workers})`
|
|
);
|
|
|
|
const startedAt = Date.now();
|
|
const result = await runPlaywrightCommand(
|
|
commandArgs,
|
|
{
|
|
PLAYWRIGHT_BASE_URL: baseURL,
|
|
PLAYWRIGHT_DEV_PORT: String(devPort),
|
|
PLAYWRIGHT_WORKERS: String(workers),
|
|
PLAYWRIGHT_ARTIFACT_NAMESPACE: artifactNamespace,
|
|
PLAYWRIGHT_REPORTER_MODE: "line-html",
|
|
PLAYWRIGHT: "1",
|
|
},
|
|
true
|
|
);
|
|
|
|
return {
|
|
pass: passLabel,
|
|
shard: chunk.shard,
|
|
totalShards,
|
|
tests: chunk.lines.length,
|
|
start: chunk.start,
|
|
end: chunk.end,
|
|
workers,
|
|
code: result.code,
|
|
durationMs: Date.now() - startedAt,
|
|
artifactNamespace,
|
|
testListPath: path.relative(workingDirectory, testListPath),
|
|
};
|
|
}
|
|
|
|
function printSummary(primaryResults, rerunResults) {
|
|
const rows = [...primaryResults, ...rerunResults];
|
|
console.log("\nBatched Chromium Summary");
|
|
console.log("pass\tshard\tstatus\ttests\tworkers\tduration\tartifact");
|
|
for (const row of rows) {
|
|
const status = row.code === 0 ? "passed" : "failed";
|
|
console.log(
|
|
`${row.pass}\t${row.shard}/${row.totalShards}\t${status}\t${row.start}-${row.end} (${row.tests})\t${row.workers}\t${formatDuration(row.durationMs)}\t${row.artifactNamespace}`
|
|
);
|
|
}
|
|
}
|
|
|
|
async function writeRunArtifacts({ totalTests, totalShards, chunks, primaryResults, rerunResults }) {
|
|
const failedPrimaryShards = primaryResults.filter((result) => result.code !== 0).map((result) => result.shard);
|
|
const failedRerunShards = rerunResults.filter((result) => result.code !== 0).map((result) => result.shard);
|
|
|
|
await fs.mkdir(outputDirectory, { recursive: true });
|
|
|
|
const runReport = {
|
|
generatedAt: new Date().toISOString(),
|
|
baseURL,
|
|
projects,
|
|
batchSize,
|
|
totalTests,
|
|
totalShards,
|
|
primaryWorkers: shardWorkers,
|
|
rerunFailed: args.rerunFailed,
|
|
forwardedArgs: args.forwardedArgs,
|
|
chunks: chunks.map((chunk) => ({
|
|
shard: chunk.shard,
|
|
start: chunk.start,
|
|
end: chunk.end,
|
|
tests: chunk.lines.length,
|
|
})),
|
|
primaryResults,
|
|
rerunResults,
|
|
failedPrimaryShards,
|
|
failedRerunShards,
|
|
};
|
|
|
|
await fs.writeFile(path.join(outputDirectory, "last-run.json"), JSON.stringify(runReport, null, 2), "utf8");
|
|
await fs.writeFile(path.join(outputDirectory, "failed-shards.json"), JSON.stringify(failedPrimaryShards, null, 2), "utf8");
|
|
|
|
const primaryRows = primaryResults
|
|
.map((result) => {
|
|
const status = result.code === 0 ? "passed" : "failed";
|
|
const color = result.code === 0 ? "#166534" : "#991b1b";
|
|
return `<tr>
|
|
<td>${result.shard}/${result.totalShards}</td>
|
|
<td>${result.start}-${result.end} (${result.tests})</td>
|
|
<td style="color:${color};font-weight:600">${status}</td>
|
|
<td>${result.workers}</td>
|
|
<td>${formatDuration(result.durationMs)}</td>
|
|
<td>${result.testListPath}</td>
|
|
<td><a href="../${result.artifactNamespace}/report/index.html">Open report</a></td>
|
|
</tr>`;
|
|
})
|
|
.join("\n");
|
|
|
|
const rerunRows =
|
|
rerunResults.length === 0
|
|
? "<tr><td colspan=\"7\">No rerun shards executed.</td></tr>"
|
|
: rerunResults
|
|
.map((result) => {
|
|
const status = result.code === 0 ? "passed" : "failed";
|
|
const color = result.code === 0 ? "#166534" : "#991b1b";
|
|
return `<tr>
|
|
<td>${result.shard}/${result.totalShards}</td>
|
|
<td>${result.start}-${result.end} (${result.tests})</td>
|
|
<td style="color:${color};font-weight:600">${status}</td>
|
|
<td>${result.workers}</td>
|
|
<td>${formatDuration(result.durationMs)}</td>
|
|
<td>${result.testListPath}</td>
|
|
<td><a href="../${result.artifactNamespace}/report/index.html">Open report</a></td>
|
|
</tr>`;
|
|
})
|
|
.join("\n");
|
|
|
|
const html = `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Batched Chromium Playwright Report Index</title>
|
|
<style>
|
|
body { font-family: Segoe UI, Arial, sans-serif; margin: 32px; color: #111827; }
|
|
h1, h2 { margin-bottom: 8px; }
|
|
p { color: #4b5563; }
|
|
table { border-collapse: collapse; width: 100%; margin: 16px 0 28px; }
|
|
th, td { border: 1px solid #d1d5db; padding: 8px 10px; text-align: left; }
|
|
th { background: #f3f4f6; }
|
|
a { color: #1d4ed8; text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Batched Chromium Playwright Reports</h1>
|
|
<p>Total tests: <strong>${totalTests}</strong> | Batch size: <strong>${batchSize}</strong> | Shards: <strong>${totalShards}</strong></p>
|
|
<p>JSON summary: <code>output/playwright/batched-chromium/last-run.json</code></p>
|
|
|
|
<h2>Primary pass</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Shard</th>
|
|
<th>Test range</th>
|
|
<th>Status</th>
|
|
<th>Workers</th>
|
|
<th>Duration</th>
|
|
<th>Test list</th>
|
|
<th>Report</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${primaryRows}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h2>Rerun pass</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Shard</th>
|
|
<th>Test range</th>
|
|
<th>Status</th>
|
|
<th>Workers</th>
|
|
<th>Duration</th>
|
|
<th>Test list</th>
|
|
<th>Report</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rerunRows}
|
|
</tbody>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
|
|
await fs.writeFile(path.join(outputDirectory, "report-index.html"), html, "utf8");
|
|
}
|
|
|
|
async function shutdown(signal) {
|
|
if (isShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
isShuttingDown = true;
|
|
console.error(`Received ${signal}. Stopping active Playwright chunk and dev server...`);
|
|
|
|
await Promise.all([
|
|
killProcessTree(activePlaywrightChild?.pid),
|
|
killProcessTree(activeDevServerProcess?.pid),
|
|
]);
|
|
|
|
activePlaywrightChild = null;
|
|
activeDevServerProcess = null;
|
|
}
|
|
|
|
async function main() {
|
|
if (args.help) {
|
|
printUsage();
|
|
return;
|
|
}
|
|
|
|
validateForwardedArgs(args.forwardedArgs);
|
|
await fs.access(playwrightCliPath);
|
|
|
|
const preflight = await preflightCollectTests(args.forwardedArgs);
|
|
const chunks = chunkTests(preflight.listedTests, batchSize);
|
|
const totalShards = chunks.length;
|
|
|
|
console.log(
|
|
`Batched Chromium preflight: ${preflight.totalTests} tests across ${projects.join(", ")} -> ${totalShards} shards at ${batchSize} tests/chunk.`
|
|
);
|
|
|
|
if (args.dryRun) {
|
|
await fs.mkdir(outputDirectory, { recursive: true });
|
|
const dryRunFile = path.join(outputDirectory, "dry-run.json");
|
|
await fs.writeFile(
|
|
dryRunFile,
|
|
JSON.stringify(
|
|
{
|
|
generatedAt: new Date().toISOString(),
|
|
baseURL,
|
|
projects,
|
|
batchSize,
|
|
shardWorkers,
|
|
totalTests: preflight.totalTests,
|
|
totalShards,
|
|
rerunFailed: args.rerunFailed,
|
|
forwardedArgs: args.forwardedArgs,
|
|
shards: chunks.map((chunk) => ({
|
|
shard: chunk.shard,
|
|
start: chunk.start,
|
|
end: chunk.end,
|
|
tests: chunk.lines.length,
|
|
})),
|
|
},
|
|
null,
|
|
2
|
|
),
|
|
"utf8"
|
|
);
|
|
console.log(`Dry run complete. Summary written to ${path.relative(workingDirectory, dryRunFile)}.`);
|
|
return;
|
|
}
|
|
|
|
await startDevServer();
|
|
|
|
const primaryResults = [];
|
|
for (const chunk of chunks) {
|
|
primaryResults.push(
|
|
await runShard({
|
|
chunk,
|
|
totalShards,
|
|
workers: shardWorkers,
|
|
passLabel: "primary",
|
|
forwardedArgs: args.forwardedArgs,
|
|
})
|
|
);
|
|
}
|
|
|
|
const failedPrimaryShards = primaryResults.filter((result) => result.code !== 0).map((result) => result.shard);
|
|
const rerunResults = [];
|
|
|
|
if (args.rerunFailed && failedPrimaryShards.length > 0) {
|
|
console.log(`\nRe-running failed chunks with workers=1: ${failedPrimaryShards.join(", ")}`);
|
|
for (const shard of failedPrimaryShards) {
|
|
const chunk = chunks[shard - 1];
|
|
rerunResults.push(
|
|
await runShard({
|
|
chunk,
|
|
totalShards,
|
|
workers: 1,
|
|
passLabel: "rerun",
|
|
forwardedArgs: args.forwardedArgs,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
printSummary(primaryResults, rerunResults);
|
|
await writeRunArtifacts({
|
|
totalTests: preflight.totalTests,
|
|
totalShards,
|
|
chunks,
|
|
primaryResults,
|
|
rerunResults,
|
|
});
|
|
await stopDevServer();
|
|
|
|
const failedRerunShards = rerunResults.filter((result) => result.code !== 0).map((result) => result.shard);
|
|
|
|
if (failedPrimaryShards.length === 0) {
|
|
console.log("\nAll batched Chromium chunks passed.");
|
|
return;
|
|
}
|
|
|
|
console.error(`\nPrimary failed shards: ${failedPrimaryShards.join(", ")}`);
|
|
if (args.rerunFailed) {
|
|
if (failedRerunShards.length > 0) {
|
|
console.error(`Rerun failed shards: ${failedRerunShards.join(", ")}`);
|
|
} else {
|
|
console.error("All failed shards passed during rerun. Treating this as flaky and returning non-zero.");
|
|
}
|
|
}
|
|
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
await main().catch(async (error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
await stopDevServer();
|
|
process.exitCode = 1;
|
|
});
|