diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46a4ebbd..7f1a78ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1557,6 +1557,9 @@ jobs: timeout-minutes: 120 needs: [build-guard, build-linux-split, build-linux] if: ${{ !cancelled() && needs.build-linux-split.result == 'success' && (needs.build-guard.outputs.verdict != 'identical' || needs.build-linux.result == 'success') }} + # The skip census reads whether 'Default-knob resolution check' proved the IPC defaults. + outputs: + defaults-on: ${{ steps.resolution.outputs.defaults-on }} # Short-lived test processes skip the glslang prewarm, as in the test job. env: @@ -4067,6 +4070,7 @@ jobs: name: skip census runs-on: ubuntu-26.04 needs: + - build-guard - test - integration - integration-verify @@ -4084,10 +4088,25 @@ jobs: with: pattern: junit-* path: junit + # The optional rows run on some paths only, and on the path this run took they are required: + # without a reused build (any build-guard verdict but identical) integration-split runs its + # unit lane and Transport A/B, and with one A/B still runs when the IPC defaults were not + # proven. - name: Skip census + env: + GUARD_VERDICT: ${{ needs.build-guard.outputs.verdict }} + SPLIT_DEFAULTS_ON: ${{ needs.integration-split.outputs.defaults-on }} run: | + required=() + if [ "${GUARD_VERDICT}" != identical ]; then + required+=(--require junit-integration-split/unit-split) + fi + if [ "${GUARD_VERDICT}" != identical ] || [ "${SPLIT_DEFAULTS_ON}" != true ]; then + required+=(--require junit-integration-split/arm-inproc --require junit-integration-split/arm-monolith) + fi + echo "build-guard verdict '${GUARD_VERDICT:-none}', split IPC defaults proven '${SPLIT_DEFAULTS_ON:-no}': ${required[*]:-no optional row required}" python3 scripts/ci/skip_census.py check junit \ - --baseline scripts/data/skip_census_baseline.json --report skip-census.json + --baseline scripts/data/skip_census_baseline.json --report skip-census.json "${required[@]}" - name: Upload census if: ${{ always() }} uses: actions/upload-artifact@v7 diff --git a/docs/ci/ci-cleanup-log.md b/docs/ci/ci-cleanup-log.md index eaddb4e6..58af3f36 100644 --- a/docs/ci/ci-cleanup-log.md +++ b/docs/ci/ci-cleanup-log.md @@ -47,8 +47,8 @@ them wrong. | 2026-10-17 | the pre-flip arm of `Strict split lane` (red-by-design while `kMGPipeP5eRunAheadReady` was false) and the "GREEN with rows outstanding" rule of `Dual-block census lane` | Test / `integration-split-strict` | dead: the constant is true, and `dualblock-expected-fatals.txt` has no rows, so neither branch could run | the hard-green arm and its two-sided marker census, and the empty-census rule (rows == 0 requires ctest success), both kept | | 2026-10-17 | `scripts/ci/tcp_log_forwarding_control.py` | none | no caller outside the archived planning notes | none needed | | 2026-10-17 | the compile of `build-linux-split` on the disaggregation branches (when `build-guard` proves the configurations identical) | Test / `build-linux-split` | it built the library `build-linux` builds there, from a configuration that differs only by the benchmarks (211 paired runs never disagreed); it delayed every split consumer by up to 9 minutes | the `build-guard` job configures both lines on one runner and compares them with `build_fingerprint.py` (cache, compile commands, link statements, CTest files; benchmarks excluded; one shared FetchContent directory, a failed configure or one killed after 10 minutes retried once) and records its image and compiler; only the verdict `identical` lets the consumers take `build-linux`'s artifacts, and only a Release one, while `different` (annotated as a warning) and `unknown` (a configure that failed twice, any error, too little evidence; a notice) keep both builds and the full split lanes; the guard job's own failure never fails the run; `build-linux` runs the link ratchet on every disaggregation build (artifact `link-ratchet-primary`) and integration-split reds on it when it takes that build; dev runs no guard and always builds | -| 2026-10-17 | `Transport A/B (monolith vs inproc)` (skip-census lanes `arm-monolith`, `arm-inproc` now optional) | Test / `integration-split` | with `build-linux` reused it repeats the integration job's two legs on the same library (79 paired runs, no one-sided red) | the integration legs; `Default-knob resolution check` (disaggregation branches) reads 'Split lanes' own run and proves, from every passed entry's client log, strict=0, role-split-state=1 and run-ahead=1 unless pinned (`split_log_paths.py ipc-config`); A/B still runs when a default differs or the build is not reused | -| 2026-10-17 | `Unit tests on the split runtime` (skip-census lane `unit-split` now optional) | Test / `integration-split` | with `build-linux` reused the test job runs the same unit lane on the same build (182 paired runs; the one recurring one-sided red was a fixed test race) | the test job, which now carries `MOBILEGL_ITEST_REQUIRE_GPU=1` and `libegl-mesa0` on the three branches; the strict unit step is unchanged | +| 2026-10-17 | `Transport A/B (monolith vs inproc)` (skip-census lanes `arm-monolith`, `arm-inproc` now optional) | Test / `integration-split` | with `build-linux` reused it repeats the integration job's two legs on the same library (79 paired runs, no one-sided red) | the integration legs; `Default-knob resolution check` (disaggregation branches) reads 'Split lanes' own run and proves, from every passed entry's client log, strict=0, role-split-state=1 and run-ahead=1 unless pinned (`split_log_paths.py ipc-config`); A/B still runs when a default differs or the build is not reused, and the skip census then requires the two arm rows | +| 2026-10-17 | `Unit tests on the split runtime` (skip-census lane `unit-split` now optional) | Test / `integration-split` | with `build-linux` reused the test job runs the same unit lane on the same build (182 paired runs; the one recurring one-sided red was a fixed test race) | the test job, which now carries `MOBILEGL_ITEST_REQUIRE_GPU=1` and `libegl-mesa0` on the three branches; the strict unit step is unchanged; without a reused build the skip census requires `unit-split` | | 2026-10-17 | the integration-gpu entries that pin their own `MOBILEGL_TRANSPORT` (804 inproc, 1446 spawn, 131 monolith) and the 12 `TcpServer` fixture entries only they need, in the inproc-acceptance leg (both passes) | Test / `integration` (inproc-acceptance) | their pin overrides the only variable the two legs differ in, so they ran identically in both (173 paired runs; the one one-sided red was a port bind fixed by cde6512b) | the monolith-control leg, which runs them on every branch; `transport_pinned.py` proves per run that the trimmed selection lost exactly that population and that the monolith-control leg ran every one; when it cannot, the leg runs everything under the JUnit stems `integration-all` and `integration-staged-copy-all`; skip census: the monolith-control lanes list their own names, the inproc-acceptance lanes drop the pinned ones, and two optional `-all` rows (the full names, the old skip lists) stand in for them on that fallback | | 2026-10-17 | `Strict split lane` on the disaggregation branches (dev keeps it) | Test / `integration-split-strict` | with the role-split state defaulting on, its environment equals the dual-block census lane's, whose entries are a superset (31 paired runs after the flip, never red only in the strict lane) | the dual-block step's first run leaves the knob at its default, proves per entry from its client log that strict, the dual block and run-ahead resolved on (`ipc-config`), writes the integration-split part of its JUnit as `strict-split.xml` (every name exactly once, `junit_tally.py --select-from`), and makes the strict lane's checks on it (hard green, two-sided marker census, StrictArming ran); a default that changed reruns the census with the knob set | | 2026-10-17 | `Magma split lane (strict, dual-block)` on the disaggregation branches when the dual-block step proved its defaults | Test / `integration-split-strict` | the dual-block census run covers the `integration-magma-split` label in the same environment | the dual-block census run | diff --git a/scripts/ci/skip_census.py b/scripts/ci/skip_census.py index dba6da21..2dfd456d 100644 --- a/scripts/ci/skip_census.py +++ b/scripts/ci/skip_census.py @@ -26,13 +26,15 @@ into the tree (scripts/data/skip_census_baseline.json): a build-equality guard falls back); its absence is a note, and when it runs it is checked like any other lane; * an optional row that also carries "instead_of": "" is that lane's whole-run fallback: - when it ran, the lane it stands in for may be absent (a note), and is required otherwise. + when it ran, the lane it stands in for may be absent (a note), and is required otherwise; + * `check --require LANE` names an optional lane that the run took the path for, so its + absence is red in that run. A deliberate retirement edits the baseline in the same commit and names the cases in the commit message (PLAN-P13 ยง4.2). Usage: - skip_census.py check --baseline [--report ] + skip_census.py check --baseline [--report ] [--require LANE]... skip_census.py write --baseline skip_census.py --self-test holds one sub-directory per lane (actions/download-artifact with a pattern lays @@ -144,8 +146,10 @@ def keep_optional_rows(written, previous): return dict(sorted(written.items())) -def compare(lanes, baseline): +def compare(lanes, baseline, required=()): red, info = [], [] + for lane in sorted(set(required) - set(baseline)): + red.append(f'lane {lane}: required by this run, but the baseline has no such row') stood_in = {row['instead_of']: lane for lane, row in baseline.items() if row.get('optional') and row.get('instead_of') and lane in lanes} for lane, known in sorted(baseline.items()): @@ -154,9 +158,13 @@ def compare(lanes, baseline): if lane in stood_in: info.append(f'lane {lane}: not run ({stood_in[lane]} ran instead)') continue - if known.get('optional'): + if known.get('optional') and lane not in required: info.append(f"lane {lane}: not run (optional: {known['optional']})") continue + if known.get('optional'): + red.append(f"lane {lane}: no JUnit in this run, which took the path that runs it " + f"({known['optional']})") + continue red.append(f'lane {lane}: no JUnit at all in this run (the job did not run or did not upload)') continue allowed = set(known['skipped']) @@ -188,6 +196,8 @@ def run(argv): parser.add_argument('junit_dir') parser.add_argument('--baseline', required=True, type=pathlib.Path) parser.add_argument('--report', type=pathlib.Path) + parser.add_argument('--require', action='append', default=[], metavar='LANE', + help='check: an optional lane this run took the path for, so it must be present') args = parser.parse_args(argv) lanes = collect(args.junit_dir) if not lanes: @@ -207,7 +217,7 @@ def run(argv): if not args.baseline.exists(): print(f'::error::skip census: no baseline at {args.baseline}; write one from a full run') return 1 - red, info = compare(lanes, resolve_baseline(json.loads(args.baseline.read_text()))) + red, info = compare(lanes, resolve_baseline(json.loads(args.baseline.read_text())), args.require) for line in info: print(f'note: {line}') for line in red: @@ -268,6 +278,15 @@ def self_test(): junit(both / 'junit-other' / 'other.xml', [('A', 'skip')]) assert quiet(['check', str(both), '--baseline', str(optional)]) == 1, 'a present optional lane is checked' assert quiet(['check', str(lost), '--baseline', str(optional)]) == 1, 'a plain lane is still required' + assert quiet(['check', str(good), '--baseline', str(optional), '--require', 'junit-other/other']) == 1, \ + 'an optional lane the run took the path for is required' + present = tmp / 'present' + junit(present / 'junit-unit' / 'unit.xml', [('A', 'pass'), ('B', 'skip'), ('C', 'pass')]) + junit(present / 'junit-other' / 'other.xml', [('A', 'pass')]) + assert quiet(['check', str(present), '--baseline', str(optional), '--require', 'junit-other/other']) == 0, \ + 'a required optional lane that ran is checked like any other' + assert quiet(['check', str(good), '--baseline', str(optional), '--require', 'junit-no/such']) == 1, \ + 'a required lane the baseline does not know is red' kept = keep_optional_rows(to_baseline(collect(good)), rows) assert kept['junit-other/other'] == rows['junit-other/other'], 'write keeps an absent optional row' # A whole-run fallback lane stands in for a trimmed one: the leg ran everything under its