Files
pleno-vue/scripts/run-playwright-ci-parallel.mjs
T
Jeppe Bundgaard bef9eaeb72 Add unit tests and new components for edge gateway workflows:
- Introduced unit tests for edge gateway workflow helpers, including workflow step resolution, incident action mapping, relay health row formatting, and workspace state merging.
- Added new components for advanced operations, configuration panel, context panel, fleet rail, and health summary.
- Enhanced gateway management UI with support for advanced actions, fallback operations, relay health visualization, and device binding features.
2026-04-16 13:44:36 +02:00

285 lines
8.2 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 forwardedArgs = process.argv.slice(2);
const workingDirectory = process.cwd();
const playwrightCliPath = path.join(workingDirectory, "node_modules", "@playwright", "test", "cli.js");
const basePort = normalizePositiveInt(process.env.PLAYWRIGHT_PARALLEL_BASE_PORT, 5191);
const legacyPerProcessWorkers = parsePositiveInt(process.env.PLAYWRIGHT_PARALLEL_WORKERS);
const reportIndexDirectory = path.join(workingDirectory, "output", "playwright", "ci-parallel-report");
const execFileAsync = promisify(execFile);
const activeChildren = new Set();
let isShuttingDown = false;
const groups = [
{
name: "chromium",
projects: ["chromium-desktop", "chromium-tablet", "chromium-mobile"],
defaultWorkers: 2,
},
{
name: "firefox",
projects: ["firefox-desktop", "firefox-tablet", "firefox-mobile"],
defaultWorkers: 1,
},
{
name: "webkit",
projects: ["webkit-desktop", "webkit-tablet", "webkit-mobile"],
defaultWorkers: 2,
},
];
function parsePositiveInt(value) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
function normalizePositiveInt(value, fallback) {
const parsed = parsePositiveInt(value);
return parsed ?? fallback;
}
function resolveGroupWorkers(group) {
const envName = `PLAYWRIGHT_PARALLEL_WORKERS_${group.name.toUpperCase()}`;
const groupWorkers = parsePositiveInt(process.env[envName]);
if (groupWorkers !== null) {
return groupWorkers;
}
if (legacyPerProcessWorkers !== null) {
return legacyPerProcessWorkers;
}
return group.defaultWorkers;
}
function resolveConfiguredGroups() {
return groups.map((group) => ({
...group,
workers: resolveGroupWorkers(group),
}));
}
function validateWorkerCap(configuredGroups) {
const totalWorkers = configuredGroups.reduce((sum, group) => sum + group.workers, 0);
if (totalWorkers > 5) {
const groupSummary = configuredGroups.map((group) => `${group.name}=${group.workers}`).join(", ");
throw new Error(
`Parallel Playwright worker cap exceeded (${totalWorkers} > 5). Configure the group workers so their sum stays at or below 5. Current values: ${groupSummary}`
);
}
return totalWorkers;
}
function getArtifactNamespace(group) {
return `ci-parallel-${group.name}`;
}
function getDevServerPidFile(artifactNamespace) {
return path.join(workingDirectory, "output", "playwright", `dev-server-${artifactNamespace}.json`);
}
async function killProcessTree(pid) {
if (!pid) {
return;
}
if (process.platform === "win32") {
await execFileAsync("taskkill", ["/PID", String(pid), "/T", "/F"]).catch(() => {});
return;
}
try {
process.kill(-pid, "SIGTERM");
} catch {
try {
process.kill(pid, "SIGTERM");
} catch {
// ignore
}
}
}
async function cleanupDevServerArtifacts(artifactNamespace) {
const pidFile = getDevServerPidFile(artifactNamespace);
try {
const file = await fs.readFile(pidFile, "utf8");
const { pid } = JSON.parse(file);
await killProcessTree(pid);
} catch {
// ignore missing pid files or already-exited processes
}
await fs.rm(pidFile, { force: true }).catch(() => {});
}
function prefixStream(stream, prefix) {
const lineReader = readline.createInterface({ input: stream });
lineReader.on("line", (line) => {
process.stdout.write(`[${prefix}] ${line}\n`);
});
}
function spawnGroup(group, index) {
const devPort = basePort + index;
const artifactNamespace = getArtifactNamespace(group);
const args = ["test", ...forwardedArgs, ...group.projects.flatMap((project) => ["--project", project])];
const child = spawn(process.execPath, [playwrightCliPath, ...args], {
cwd: workingDirectory,
env: {
...process.env,
PLAYWRIGHT_BASE_URL: "",
PLAYWRIGHT_DEV_PORT: String(devPort),
PLAYWRIGHT_WORKERS: String(group.workers),
PLAYWRIGHT_ARTIFACT_NAMESPACE: artifactNamespace,
PLAYWRIGHT_REPORTER_MODE: "line-html",
PLAYWRIGHT: "1",
},
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
});
activeChildren.add(child);
prefixStream(child.stdout, group.name);
prefixStream(child.stderr, `${group.name}:err`);
return new Promise((resolve) => {
child.on("close", (code) => {
activeChildren.delete(child);
resolve({
name: group.name,
code: code ?? 1,
artifactNamespace,
projects: group.projects,
port: devPort,
workers: group.workers,
});
});
});
}
async function writeCombinedReportIndex(results) {
await fs.rm(reportIndexDirectory, { recursive: true, force: true });
await fs.mkdir(reportIndexDirectory, { recursive: true });
const rows = results
.map((result) => {
const status = result.code === 0 ? "passed" : "failed";
const statusColor = result.code === 0 ? "#166534" : "#991b1b";
const reportHref = `../${result.artifactNamespace}/report/index.html`;
return `
<tr>
<td>${result.name}</td>
<td>${result.projects.join(", ")}</td>
<td>${result.port}</td>
<td>${result.workers}</td>
<td style="color: ${statusColor}; font-weight: 600;">${status}</td>
<td><a href="${reportHref}">Open report</a></td>
</tr>`;
})
.join("\n");
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Playwright Parallel Reports</title>
<style>
body { font-family: Segoe UI, Arial, sans-serif; margin: 32px; color: #111827; }
h1 { margin-bottom: 8px; }
p { color: #4b5563; }
table { border-collapse: collapse; width: 100%; margin-top: 24px; }
th, td { border: 1px solid #d1d5db; padding: 10px 12px; text-align: left; }
th { background: #f3f4f6; }
a { color: #1d4ed8; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>Playwright Parallel Reports</h1>
<p>Each child run used its own Vite port, dev-server pid file, and artifact directory.</p>
<table>
<thead>
<tr>
<th>Group</th>
<th>Projects</th>
<th>Port</th>
<th>Workers</th>
<th>Status</th>
<th>Report</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</body>
</html>`;
await fs.writeFile(path.join(reportIndexDirectory, "index.html"), html, "utf8");
}
async function shutdown(signal) {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
process.stderr.write(`Received ${signal}. Stopping parallel Playwright children...\n`);
await Promise.all([...activeChildren].map((child) => killProcessTree(child.pid)));
await Promise.all(groups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
}
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => {
void shutdown(signal).finally(() => {
process.exit(130);
});
});
}
async function main() {
const configuredGroups = resolveConfiguredGroups();
const totalWorkers = validateWorkerCap(configuredGroups);
console.log(
`Starting Playwright CI in parallel with ${
configuredGroups.length
} processes and ${totalWorkers} total worker(s): ${configuredGroups
.map((group) => `${group.name}=${group.workers}`)
.join(", ")}.`
);
await Promise.all(configuredGroups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
const results = await Promise.all(configuredGroups.map((group, index) => spawnGroup(group, index)));
await writeCombinedReportIndex(results);
await Promise.all(configuredGroups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
const failedRuns = results.filter((result) => result.code !== 0);
if (failedRuns.length > 0) {
console.error(
`Parallel Playwright CI failed for: ${failedRuns.map((result) => `${result.name} (${result.code})`).join(", ")}`
);
process.exit(1);
}
console.log(
`Parallel Playwright CI passed. Combined report index: ${path.relative(
workingDirectory,
path.join(reportIndexDirectory, "index.html")
)}`
);
}
await main();