mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
Run the sequential Electron E2E suite as three Playwright shards per OS (one runner each) and summarize all shards of an OS in one follow-up job. The semantic summary script accepts a directory of shard reports, merges them and refuses to write when a shard is missing, duplicated or malformed, or when an explicit input does not exist. Slowest shard per OS in the final run: ubuntu 12.5 min (was 26), macOS 13.7 min (was 34), Windows 24.5 min (was 35). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
149 lines
5.4 KiB
JavaScript
149 lines
5.4 KiB
JavaScript
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
/**
|
|
* Helpers for reading one or more Playwright JSON reports (`results.json`).
|
|
*
|
|
* The Electron E2E workflow runs the suite as several Playwright shards, one
|
|
* per runner. Each shard writes its own `results.json` that carries
|
|
* `config.shard = { current, total }` and only that shard's tests. A semantic
|
|
* summary is only honest when every shard is present exactly once, so callers
|
|
* verify the set before merging.
|
|
*/
|
|
|
|
export const playwrightReportFileName = 'results.json';
|
|
|
|
/** Recursively lists every `results.json` below `rootDir`, sorted by path. */
|
|
export function findPlaywrightJsonReports(rootDir) {
|
|
if (!existsSync(rootDir) || !statSync(rootDir).isDirectory()) {
|
|
return [];
|
|
}
|
|
const found = [];
|
|
for (const entry of readdirSync(rootDir)) {
|
|
const fullPath = path.join(rootDir, entry);
|
|
if (statSync(fullPath).isDirectory()) {
|
|
found.push(...findPlaywrightJsonReports(fullPath));
|
|
} else if (entry === playwrightReportFileName) {
|
|
found.push(fullPath);
|
|
}
|
|
}
|
|
return found.sort();
|
|
}
|
|
|
|
/**
|
|
* Parses each report and extracts its shard descriptor: `null` when the run
|
|
* was not sharded (`config.shard` absent or `null`). A descriptor that is
|
|
* present but malformed is kept as `malformedShard` so verification rejects
|
|
* it instead of mistaking a partial run for a complete unsharded one.
|
|
*/
|
|
export function loadPlaywrightReports(reportPaths) {
|
|
return reportPaths.map((reportPath) => {
|
|
const report = JSON.parse(readFileSync(reportPath, 'utf8'));
|
|
const shard = report.config?.shard ?? null;
|
|
const wellFormed =
|
|
shard !== null &&
|
|
Number.isInteger(shard.current) &&
|
|
Number.isInteger(shard.total);
|
|
return {
|
|
path: reportPath,
|
|
report,
|
|
shard: wellFormed ? { current: shard.current, total: shard.total } : null,
|
|
malformedShard: shard !== null && !wellFormed,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks that the loaded reports form exactly one complete run: either a
|
|
* single unsharded report, or every shard `1..total` exactly once.
|
|
*/
|
|
export function verifyShardReports(reports) {
|
|
const problems = [];
|
|
if (reports.length === 0) {
|
|
problems.push('no Playwright JSON reports were provided');
|
|
return { ok: false, problems };
|
|
}
|
|
|
|
const malformed = reports.filter((entry) => entry.malformedShard);
|
|
if (malformed.length > 0) {
|
|
problems.push(`malformed config.shard descriptor: ${describePaths(malformed)}`);
|
|
}
|
|
|
|
const unsharded = reports.filter(
|
|
(entry) => entry.shard === null && !entry.malformedShard
|
|
);
|
|
const sharded = reports.filter((entry) => entry.shard !== null);
|
|
|
|
if (unsharded.length > 0 && sharded.length > 0) {
|
|
problems.push(
|
|
`mixed sharded and unsharded reports: ${describePaths(unsharded)} carry no shard descriptor`
|
|
);
|
|
}
|
|
if (unsharded.length > 1) {
|
|
problems.push(
|
|
`${unsharded.length} unsharded reports would count every test more than once: ${describePaths(unsharded)}`
|
|
);
|
|
}
|
|
|
|
if (sharded.length > 0) {
|
|
const totals = new Set(sharded.map((entry) => entry.shard.total));
|
|
if (totals.size > 1) {
|
|
problems.push(
|
|
`shard totals disagree: ${Array.from(totals).sort().join(', ')}`
|
|
);
|
|
} else {
|
|
const [total] = totals;
|
|
const seen = new Map();
|
|
for (const entry of sharded) {
|
|
const list = seen.get(entry.shard.current) ?? [];
|
|
list.push(entry);
|
|
seen.set(entry.shard.current, list);
|
|
}
|
|
const missing = [];
|
|
for (let index = 1; index <= total; index += 1) {
|
|
if (!seen.has(index)) {
|
|
missing.push(`${index}/${total}`);
|
|
}
|
|
}
|
|
if (missing.length > 0) {
|
|
problems.push(`missing shards: ${missing.join(', ')}`);
|
|
}
|
|
for (const [current, entries] of seen) {
|
|
if (entries.length > 1) {
|
|
problems.push(
|
|
`shard ${current}/${total} appears ${entries.length} times: ${describePaths(entries)}`
|
|
);
|
|
}
|
|
if (current < 1 || current > total) {
|
|
problems.push(`shard ${current}/${total} is outside 1..${total}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { ok: problems.length === 0, problems };
|
|
}
|
|
|
|
/** Human-readable label for the summary, e.g. `3/3 shards (1/3, 2/3, 3/3)`. */
|
|
export function describeShardReports(reports) {
|
|
if (reports.length === 0) {
|
|
return 'none';
|
|
}
|
|
const sharded = reports.filter((entry) => entry.shard !== null);
|
|
if (sharded.length === 0) {
|
|
return reports.length === 1
|
|
? '1 report (unsharded)'
|
|
: `${reports.length} reports (unsharded)`;
|
|
}
|
|
const total = Math.max(...sharded.map((entry) => entry.shard.total));
|
|
const labels = sharded
|
|
.map((entry) => entry.shard)
|
|
.sort((left, right) => left.current - right.current)
|
|
.map((shard) => `${shard.current}/${shard.total}`);
|
|
return `${sharded.length}/${total} shards (${labels.join(', ')})`;
|
|
}
|
|
|
|
function describePaths(entries) {
|
|
return entries.map((entry) => entry.path).join(', ');
|
|
}
|