|
61 | 61 | * |
62 | 62 | * A file-sharded package (the CLI, cut into 3 slices on 3 shards whenever a |
63 | 63 | * run's own list makes it the serial floor) arrives as three partial windows |
64 | | - * on three shards. They are SUMMED across shards (mergeCaptures, which marks a |
| 64 | + * on three shards. They are SUMMED across shards (foldCaptures, which marks a |
65 | 65 | * set missing a part as incomplete) and the sum is graded against the whole |
66 | 66 | * package's ceiling, because that is the quantity the ceiling was measured in: |
67 | 67 | * the generator records a sliced package as the per-run SUM of its slices, and |
@@ -101,7 +101,6 @@ import process from 'node:process'; |
101 | 101 |
|
102 | 102 | import { isEntrypoint } from './invoked-as.mjs'; |
103 | 103 | import { escapeWorkflowCommandMessage } from './partition-test-shards.mjs'; |
104 | | -import { mergeCaptures } from './report-test-timings.mjs'; |
105 | 104 |
|
106 | 105 | const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); |
107 | 106 | const DATASET_PATH = path.join(REPO_ROOT, 'scripts', 'test-shard-timings.json'); |
@@ -214,7 +213,7 @@ export function readCeilingTable(dataset) { |
214 | 213 | // ── Grading ─────────────────────────────────────────────────────────────── |
215 | 214 |
|
216 | 215 | /** |
217 | | - * Grade one run. `merged` is mergeCaptures()' result: every executed package's |
| 216 | + * Grade one run. `merged` is foldCaptures()' result: every executed package's |
218 | 217 | * window, a sliced package already summed across shards with `complete` saying |
219 | 218 | * whether every part turned up, and every file line. |
220 | 219 | * |
@@ -421,6 +420,52 @@ export function renderCeilingVerdict(report, label) { |
421 | 420 |
|
422 | 421 | // ── Reading the shard captures ──────────────────────────────────────────── |
423 | 422 |
|
| 423 | +/** The capture schema `report-test-timings.mjs --capture` writes; any other is refused. */ |
| 424 | +export const CAPTURE_SCHEMA = 'test-timing-capture/1'; |
| 425 | + |
| 426 | +/** |
| 427 | + * Fold the per-shard captures into one run: every executed package's window, |
| 428 | + * a file-sliced package SUMMED across the shards that ran its parts and marked |
| 429 | + * `complete` only when every part turned up, and every file line. |
| 430 | + * |
| 431 | + * The same fold `report-test-timings.mjs` makes for its table, kept here rather |
| 432 | + * than imported: that module is a gate file only through its own self-test, and |
| 433 | + * a gate family importing it would silently stop inheriting its declared |
| 434 | + * populations in the dispatch-gates derivation (its promotion invariant reds on |
| 435 | + * exactly that). The windows themselves are not re-read here -- each capture's |
| 436 | + * package rows are `samplesFromSummary`'s, taken on the shard. |
| 437 | + */ |
| 438 | +export function foldCaptures(captures) { |
| 439 | + const files = []; |
| 440 | + const byName = new Map(); |
| 441 | + const problems = []; |
| 442 | + const shards = []; |
| 443 | + for (const capture of captures) { |
| 444 | + if (capture?.schema !== CAPTURE_SCHEMA) { |
| 445 | + problems.push(`a capture declares schema ${JSON.stringify(capture?.schema ?? null)}, not ${CAPTURE_SCHEMA}`); |
| 446 | + continue; |
| 447 | + } |
| 448 | + if (capture.shard) shards.push(capture.shard); |
| 449 | + files.push(...(capture.files ?? [])); |
| 450 | + for (const p of capture.problems ?? []) problems.push(p); |
| 451 | + for (const row of capture.packages ?? []) { |
| 452 | + const acc = byName.get(row.pkg) ?? { pkg: row.pkg, seconds: 0, sliceCount: null, parts: [] }; |
| 453 | + acc.seconds += row.seconds; |
| 454 | + if (row.sliceCount) { |
| 455 | + acc.sliceCount = row.sliceCount; |
| 456 | + acc.parts.push(...(row.parts ?? [])); |
| 457 | + } |
| 458 | + byName.set(row.pkg, acc); |
| 459 | + } |
| 460 | + } |
| 461 | + const packages = [...byName.values()].map((row) => { |
| 462 | + const parts = [...new Set(row.parts)].sort((a, b) => a - b); |
| 463 | + return { ...row, parts, complete: row.sliceCount === null || parts.length === row.sliceCount }; |
| 464 | + }); |
| 465 | + return { files, packages, problems, shards: shards.sort() }; |
| 466 | +} |
| 467 | + |
| 468 | + |
424 | 469 | /** Every capture JSON under `dir`, recursively; unreadable files become problems. */ |
425 | 470 | export function readCaptures(dir) { |
426 | 471 | const captures = []; |
@@ -472,7 +517,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ |
472 | 517 | 'provisional prints "no ceiling: provisional" and is not red': 3, |
473 | 518 | 'a package absent from the dataset is red; an uncapped one is not': 4, |
474 | 519 | 'no ceiling table yet is NOT MEASURED, never red': 5, |
475 | | - 'file-sliced packages are summed across shards': 5, |
| 520 | + 'file-sliced packages are summed across shards': 6, |
476 | 521 | 'a ruled raise lifts a ceiling, and only a well-formed one does': 7, |
477 | 522 | 'a malformed ceiling table is refused, never read as a pass': 4, |
478 | 523 | }); |
@@ -529,7 +574,7 @@ function selfTest() { |
529 | 574 | ...extra, |
530 | 575 | }); |
531 | 576 | const run = (captures, ds = dataset(), raises = {}) => { |
532 | | - const report = gradeCeilings({ merged: mergeCaptures(captures), dataset: ds, raises }); |
| 577 | + const report = gradeCeilings({ merged: foldCaptures(captures), dataset: ds, raises }); |
533 | 578 | return { report, ...renderCeilingVerdict(report, 'Test Core') }; |
534 | 579 | }; |
535 | 580 | const status = (r, name) => r.report.rows.find((row) => row.name === name)?.status; |
@@ -661,6 +706,14 @@ function selfTest() { |
661 | 706 | const r = run([capture('2/6', [['cli', 10, [1, 3]]]), capture('3/6', [['a', 1]])]); |
662 | 707 | if (!r.err[0].includes('cli: NOT MEASURED') || !r.err[0].includes('slices 1 of 3')) throw new Error(`slices: the partial line is wrong:\n${r.err[0]}`); |
663 | 708 | }); |
| 709 | + check(() => { |
| 710 | + // A capture in a shape this fold does not know contributes nothing and is |
| 711 | + // named, never read as a set of windows. |
| 712 | + const folded = foldCaptures([{ ...capture('2/6', [['cli', 600, [1, 3]]]), schema: 'test-timing-capture/2' }, capture('3/6', [['a', 1]])]); |
| 713 | + if (folded.packages.some((p) => p.pkg === 'cli') || !folded.problems.some((p) => p.includes('test-timing-capture/2'))) { |
| 714 | + throw new Error(`slices: an unknown capture schema was folded in: ${JSON.stringify(folded)}`); |
| 715 | + } |
| 716 | + }); |
664 | 717 |
|
665 | 718 | battery('a ruled raise lifts a ceiling, and only a well-formed one does'); |
666 | 719 | check(() => { |
@@ -777,7 +830,7 @@ function main() { |
777 | 830 | } |
778 | 831 |
|
779 | 832 | const { captures, problems: readProblems } = readCaptures(capturesDir); |
780 | | - const merged = mergeCaptures(captures); |
| 833 | + const merged = foldCaptures(captures); |
781 | 834 | const dataset = JSON.parse(readFileSync(DATASET_PATH, 'utf8')); |
782 | 835 | const report = gradeCeilings({ merged, dataset }); |
783 | 836 | const rendered = renderCeilingVerdict(report, label); |
|
0 commit comments