|
49 | 49 | // node scripts/ci/select-shard-timings-run.mjs --self-test |
50 | 50 | // |
51 | 51 | // `--candidates` needs GITHUB_TOKEN and GITHUB_REPOSITORY in the environment and |
52 | | -// prints a JSON array, newest first. `--check-coverage` exits non-zero when the |
53 | | -// refreshed dataset lost a package the committed one measured and the workspace |
54 | | -// still contains. |
| 52 | +// prints a JSON array, newest first. `--check-coverage` judges MEASURED UNION |
| 53 | +// CARRIED against the workspace and exits non-zero when a package that HAD a |
| 54 | +// measured weight has neither -- it names workspace packages that were never |
| 55 | +// measured too, but those are a report rather than a refusal, because the |
| 56 | +// partitioner already estimated them and this refresh did not change that. |
55 | 57 |
|
56 | 58 | import { readFileSync } from 'node:fs'; |
57 | 59 | import process from 'node:process'; |
@@ -162,23 +164,53 @@ export function runIsEligible({ jobs, artifacts }, shardCount = SHARD_COUNT) { |
162 | 164 | // direction this refuses in. Comparing against the live workspace is what keeps |
163 | 165 | // a package legitimately deleted from the monorepo from blocking every future |
164 | 166 | // refresh -- it is gone from `workspace`, so it is not required. |
| 167 | +// Coverage is judged on MEASURED UNION CARRIED, which after a `--merge-into` |
| 168 | +// pass is simply the refreshed dataset's own key set: the generator has already |
| 169 | +// folded in every package a cache HIT witnessed as unchanged, and refused to |
| 170 | +// fold in anything else. Two outcomes are reported separately because they are |
| 171 | +// different facts and only one of them is a regression: |
| 172 | +// |
| 173 | +// `lost` — the package HAD a measured weight, this refresh neither measured |
| 174 | +// it nor found a cache HIT to witness it, so it would drop to a |
| 175 | +// test-file-count ESTIMATE. That is the silent degradation this |
| 176 | +// whole lane exists to prevent, so it is a REFUSAL, by name. |
| 177 | +// |
| 178 | +// `neverMeasured` — the package is in the workspace and has no measured |
| 179 | +// weight before OR after: a new package, or one the dataset has |
| 180 | +// never covered. The partitioner already estimates it from its |
| 181 | +// test-file count and this refresh changed nothing about it, so it |
| 182 | +// cannot be a regression — but it is NAMED rather than passed over |
| 183 | +// in silence, because "estimated" must never be something a reader |
| 184 | +// has to infer from an absence. |
165 | 185 | export function coverageReport({ committed, refreshed, workspace, exclude = [] }) { |
166 | 186 | const excluded = new Set(exclude); |
167 | 187 | const inWorkspace = new Set(workspace); |
168 | | - const required = Object.keys(committed?.packages ?? {}).filter( |
| 188 | + const priorPackages = committed?.packages ?? {}; |
| 189 | + const covered = new Set(Object.keys(refreshed?.packages ?? {})); |
| 190 | + const carried = new Set(refreshed?.carriedOver ?? []); |
| 191 | + |
| 192 | + const required = Object.keys(priorPackages).filter( |
169 | 193 | (name) => inWorkspace.has(name) && !excluded.has(name) |
170 | 194 | ); |
171 | | - const measured = new Set(Object.keys(refreshed?.packages ?? {})); |
172 | | - const lost = required.filter((name) => !measured.has(name)).sort((a, b) => a.localeCompare(b, 'en')); |
173 | | - const gained = [...measured] |
174 | | - .filter((name) => !Object.hasOwn(committed?.packages ?? {}, name)) |
| 195 | + const lost = required.filter((name) => !covered.has(name)).sort((a, b) => a.localeCompare(b, 'en')); |
| 196 | + |
| 197 | + const neverMeasured = [...inWorkspace] |
| 198 | + .filter((name) => !excluded.has(name) && !covered.has(name) && !Object.hasOwn(priorPackages, name)) |
175 | 199 | .sort((a, b) => a.localeCompare(b, 'en')); |
| 200 | + |
| 201 | + const gained = [...covered] |
| 202 | + .filter((name) => !Object.hasOwn(priorPackages, name)) |
| 203 | + .sort((a, b) => a.localeCompare(b, 'en')); |
| 204 | + |
176 | 205 | return { |
177 | 206 | ok: lost.length === 0, |
178 | 207 | lost, |
| 208 | + neverMeasured, |
179 | 209 | gained, |
| 210 | + carriedCount: carried.size, |
| 211 | + freshCount: covered.size - carried.size, |
180 | 212 | requiredCount: required.length, |
181 | | - measuredCount: measured.size, |
| 213 | + measuredCount: covered.size, |
182 | 214 | }; |
183 | 215 | } |
184 | 216 |
|
@@ -265,7 +297,7 @@ export async function listCandidates({ |
265 | 297 | // stopped running, and the remedy is to find what stopped registering, never to |
266 | 298 | // lower the number. |
267 | 299 | const SELF_TEST_BATTERIES = Object.freeze({ |
268 | | - 'select-shard-timings-run self-test': 30, |
| 300 | + 'select-shard-timings-run self-test': 35, |
269 | 301 | }); |
270 | 302 | const SELF_TEST_BATTERY_FLOOR = 1; |
271 | 303 | const UNATTRIBUTED_BATTERY = '(no battery open)'; |
@@ -466,6 +498,66 @@ function selfTest() { |
466 | 498 | if (!r.ok || r.gained.join(',') !== 'd') throw new Error(`coverage: a newly measured package was not reported (${r.gained.join(',')})`); |
467 | 499 | }); |
468 | 500 |
|
| 501 | + // -- Coverage under the MERGE (#16464). After a `--merge-into` pass the |
| 502 | + // refreshed dataset already holds the carried weights, so coverage is |
| 503 | + // judged on measured UNION carried; what the cases below separate is the |
| 504 | + // two ways a package can be missing, because only one of them is a |
| 505 | + // regression. |
| 506 | + check(() => { |
| 507 | + // A carried package COUNTS as covered — it has a real weight, witnessed |
| 508 | + // unchanged by a cache hit — so a refresh that measured only `a` and |
| 509 | + // carried `b` and `c` is complete, not short. |
| 510 | + const r = coverageReport({ |
| 511 | + committed, |
| 512 | + refreshed: { packages: { a: 11, b: 20, c: 30 }, carriedOver: ['b', 'c'] }, |
| 513 | + workspace: ws, |
| 514 | + }); |
| 515 | + if (!r.ok) throw new Error(`coverage: carried packages were not counted as covered (${r.lost.join(', ')})`); |
| 516 | + if (r.carriedCount !== 2 || r.freshCount !== 1) { |
| 517 | + throw new Error(`coverage: the carried/fresh split is wrong (carried ${r.carriedCount}, fresh ${r.freshCount})`); |
| 518 | + } |
| 519 | + }); |
| 520 | + check(() => { |
| 521 | + // The regression that still refuses: `c` had a weight and is in NEITHER set. |
| 522 | + const r = coverageReport({ |
| 523 | + committed, |
| 524 | + refreshed: { packages: { a: 11, b: 20 }, carriedOver: ['b'] }, |
| 525 | + workspace: ws, |
| 526 | + }); |
| 527 | + if (r.ok) throw new Error('coverage: a package that lost its measured weight was accepted'); |
| 528 | + if (r.lost.join(',') !== 'c') throw new Error(`coverage: the lost package was not named (${r.lost.join(',')})`); |
| 529 | + }); |
| 530 | + check(() => { |
| 531 | + // A workspace package that NEVER had a weight is named but is not a |
| 532 | + // refusal: the partitioner already estimated it and this refresh changed |
| 533 | + // nothing about it. |
| 534 | + const r = coverageReport({ |
| 535 | + committed, |
| 536 | + refreshed: { packages: { a: 11, b: 20, c: 30 }, carriedOver: [] }, |
| 537 | + workspace: [...ws, 'brand-new'], |
| 538 | + }); |
| 539 | + if (!r.ok) throw new Error(`coverage: a never-measured package was treated as a regression (${r.lost.join(',')})`); |
| 540 | + if (r.neverMeasured.join(',') !== 'brand-new') { |
| 541 | + throw new Error(`coverage: the never-measured package was not named (${r.neverMeasured.join(',')})`); |
| 542 | + } |
| 543 | + }); |
| 544 | + check(() => { |
| 545 | + // …and it is not confused with a carried one. |
| 546 | + const r = coverageReport({ |
| 547 | + committed, |
| 548 | + refreshed: { packages: { a: 11, b: 20, c: 30 }, carriedOver: ['c'] }, |
| 549 | + workspace: [...ws, 'brand-new'], |
| 550 | + }); |
| 551 | + if (r.neverMeasured.includes('c') || r.carriedCount !== 1) { |
| 552 | + throw new Error(`coverage: carried and never-measured were conflated (never ${r.neverMeasured.join(',')}, carried ${r.carriedCount})`); |
| 553 | + } |
| 554 | + }); |
| 555 | + check(() => { |
| 556 | + // A dataset with no carriedOver key at all (a plain replace) still reads. |
| 557 | + const r = coverageReport({ committed, refreshed: { packages: { a: 1, b: 2, c: 3 } }, workspace: ws }); |
| 558 | + if (!r.ok || r.carriedCount !== 0) throw new Error('coverage: a dataset without carriedOver was misread'); |
| 559 | + }); |
| 560 | + |
469 | 561 | // -- The workspace reader refuses a shape it cannot trust, rather than |
470 | 562 | // returning an empty list that would make every package look deleted. |
471 | 563 | check(() => { |
@@ -546,20 +638,32 @@ async function main() { |
546 | 638 | const refreshed = readJson(value('--refreshed')); |
547 | 639 | const workspace = workspaceNames(readJson(value('--workspace'))); |
548 | 640 | const report = coverageReport({ committed, refreshed, workspace, exclude }); |
| 641 | + // Named whichever way the verdict goes: a package the partitioner estimates |
| 642 | + // must never be something a reader infers from an absence. |
| 643 | + if (report.neverMeasured.length > 0) { |
| 644 | + console.error( |
| 645 | + `select-shard-timings-run: ${report.neverMeasured.length} workspace package(s) have no measured ` + |
| 646 | + `weight before or after this refresh and are ESTIMATED by the partitioner from their ` + |
| 647 | + `test-file count: ${report.neverMeasured.join(', ')}. Not a regression — this refresh did not ` + |
| 648 | + 'change their standing — but they are named rather than passed over, because an estimate that ' + |
| 649 | + 'reads as a measurement is this dataset\'s signature hazard.' |
| 650 | + ); |
| 651 | + } |
549 | 652 | if (!report.ok) { |
550 | 653 | console.error( |
551 | | - `select-shard-timings-run: COVERAGE SHORTFALL -- ${report.lost.length} package(s) the committed ` + |
552 | | - 'dataset measured, and the workspace still contains, are NOT measured by the runs accumulated ' + |
553 | | - `so far: ${report.lost.join(', ')}. Every one of them would silently fall back to the ` + |
554 | | - 'test-file-count ESTIMATE, so what has been read so far is a warm cache rather than the ' + |
555 | | - 'workspace. Not a verdict on any one run: no single run measures everything (turbo caches per ' + |
556 | | - 'shard, and the generator refuses hits), so the caller adds the next older run and asks again.' |
| 654 | + `select-shard-timings-run: COVERAGE SHORTFALL -- ${report.lost.length} package(s) HAD a measured ` + |
| 655 | + 'weight and this refresh neither re-measured them nor found a turbo cache HIT to witness that ' + |
| 656 | + `they are unchanged: ${report.lost.join(', ')}. Each would drop to the test-file-count ` + |
| 657 | + 'ESTIMATE, which is the silent degradation this lane exists to prevent, so this is a refusal. ' + |
| 658 | + 'Not a verdict on any one run: no single run measures everything, so the caller adds the next ' + |
| 659 | + 'older run and asks again.' |
557 | 660 | ); |
558 | 661 | process.exit(1); |
559 | 662 | } |
560 | 663 | console.error( |
561 | | - `select-shard-timings-run: coverage OK -- ${report.measuredCount} package(s) measured, ` + |
562 | | - `${report.requiredCount} required, ${report.gained.length} newly measured` + |
| 664 | + `select-shard-timings-run: coverage OK -- ${report.measuredCount} package(s) covered ` + |
| 665 | + `(${report.freshCount} measured in these runs, ${report.carriedCount} carried on a cache-hit ` + |
| 666 | + `witness), ${report.requiredCount} required, ${report.gained.length} newly measured` + |
563 | 667 | `${report.gained.length > 0 ? ` (${report.gained.join(', ')})` : ''}.` |
564 | 668 | ); |
565 | 669 | return; |
|
0 commit comments