mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
Playwright --shard distributes by count (per file with fullyParallel:false), blind to duration — measured skew on the 9-shard matrix: 24m47s worst vs 1m47s best (14x), putting E2E on the CI critical path (~25min of the 33min gate). - scripts/quality/balance-e2e-shards.mjs: LPT greedy (heaviest first into the lightest shard) over config/quality/e2e-timings.json; deterministic (weight desc, filename tiebreak); new specs get the median weight; the CLI self-verifies the shard union equals the discovered spec list and exits non-zero on ANY inconsistency (missing timings, lost spec) so the CI step falls back to plain --shard — never fewer specs than before. - config/quality/e2e-timings.json: relative weights seeded from spec LOC (proxy); replace with real per-file durations from a full run when convenient (documented in _meta). LOC-seeded packing already lands at 742-761 per shard (1.03x skew) vs the alphabetical round-robin that produced 14x. - ci.yml test-e2e: balanced list per shard with logged assignment + fallback. TDD: 5 unit tests (LPT invariants, determinism, completeness, median fallback, seed-vs-specs drift guard).
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import { test } from "node:test";
|
||
import assert from "node:assert/strict";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { lptAssign, weightItems } from "../../scripts/quality/balance-e2e-shards.mjs";
|
||
|
||
// WS4.1 (v3.8.49 quality plan) — the E2E matrix skew was 14× (24m47s vs 1m47s)
|
||
// because Playwright --shard distributes by count, not duration. These tests pin
|
||
// the LPT packing invariants; the hard one is COMPLETENESS (a lost spec would
|
||
// silently hollow the suite — the CLI self-checks it and falls back to --shard).
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||
|
||
test("lptAssign puts the heaviest item alone before doubling up lighter shards", () => {
|
||
const shards = lptAssign(
|
||
[
|
||
{ file: "huge.spec.ts", weight: 100 },
|
||
{ file: "a.spec.ts", weight: 30 },
|
||
{ file: "b.spec.ts", weight: 30 },
|
||
{ file: "c.spec.ts", weight: 30 },
|
||
],
|
||
2
|
||
);
|
||
assert.deepEqual(shards[0].files, ["huge.spec.ts"]);
|
||
assert.deepEqual(shards[1].files, ["a.spec.ts", "b.spec.ts", "c.spec.ts"]);
|
||
assert.equal(shards[0].total, 100);
|
||
assert.equal(shards[1].total, 90);
|
||
});
|
||
|
||
test("lptAssign is deterministic on equal weights (filename tiebreak)", () => {
|
||
const items = [
|
||
{ file: "b.spec.ts", weight: 10 },
|
||
{ file: "a.spec.ts", weight: 10 },
|
||
];
|
||
const s1 = lptAssign(items, 2);
|
||
const s2 = lptAssign([...items].reverse(), 2);
|
||
assert.deepEqual(s1, s2);
|
||
});
|
||
|
||
test("completeness: every file lands in exactly one shard", () => {
|
||
const items = Array.from({ length: 37 }, (_, i) => ({
|
||
file: `f${String(i).padStart(2, "0")}.spec.ts`,
|
||
weight: (i * 7) % 40,
|
||
}));
|
||
const shards = lptAssign(items, 9);
|
||
const union = shards.flatMap((s) => s.files).sort();
|
||
assert.deepEqual(union, items.map((i) => i.file).sort());
|
||
});
|
||
|
||
test("weightItems gives unknown/new specs the median weight, not an extreme", () => {
|
||
const items = weightItems(["new.spec.ts", "big.spec.ts", "small.spec.ts"], {
|
||
_meta: "x",
|
||
"big.spec.ts": 600,
|
||
"small.spec.ts": 20,
|
||
"other.spec.ts": 100,
|
||
});
|
||
const byFile = Object.fromEntries(items.map((i) => [i.file, i.weight]));
|
||
assert.equal(byFile["big.spec.ts"], 600);
|
||
assert.equal(byFile["small.spec.ts"], 20);
|
||
assert.equal(byFile["new.spec.ts"], 100); // median of [20,100,600]
|
||
});
|
||
|
||
test("the committed timings seed covers every current e2e spec (no drift)", () => {
|
||
const timings = JSON.parse(
|
||
fs.readFileSync(path.join(ROOT, "config", "quality", "e2e-timings.json"), "utf8")
|
||
);
|
||
const specs = fs
|
||
.readdirSync(path.join(ROOT, "tests", "e2e"))
|
||
.filter((f) => f.endsWith(".spec.ts"));
|
||
const missing = specs.filter((f) => !(f in timings));
|
||
// Missing entries are tolerated at runtime (median fallback) — this assert keeps
|
||
// the seed honest so balance quality does not silently rot as specs are added.
|
||
assert.deepEqual(missing, [], `add to config/quality/e2e-timings.json: ${missing.join(", ")}`);
|
||
});
|