[MG_IntegrationTest, CI] (P6/t6): integration-spawn, and the set identity made structural instead of maintained

Exit gates 9.2 and 9.3. Until now nothing ran the integration suite across a
process boundary at all - the spawn shape was proven only by the retrace lane,
which exercises one trace per case and no scenario assertions.

integration-spawn: 102 cases, 102 green.

THE SET IDENTITY IS THE POINT, AND IT IS NOT MAINTAINED BY HAND. 9.2 asks that
the spawn lane's case-name set equal the split lane's. Registering the spawn
arm as a second copy of each block would satisfy that today and drift the first
time somebody adds a scenario to one of them - and a spawn lane covering LESS
goes green while proving less, with nothing about a smaller green lane looking
wrong. The eleven split-environment blocks become eleven calls to one macro
that emits BOTH arms from one filter, so a case added to either is added to
both or to neither.

scripts/ci/spawn_lane_parity.py asserts it anyway, because a macro is one edit
from a hand-written block. It compares SETS rather than counts - a count can
agree while the membership does not - and carries exactly one sanctioned
asymmetry, named in the file: integration-split also labels the PUSH-MONOLITH
arm, whose scenario is monolith-only by construction. R-16: introduce a
split-only block and it reds naming the case and the macro to use.

THREE CASES SKIP UNDER SPAWN, BY NAME. CtWireScenario reads PipeApplier's
in-process tallies through ServerVerbs(); under spawn those live in the SERVER's
process and this one's copy is 0 forever, so the assertions were failing about
somebody else's memory. That is a harness limit, not a product defect - the
records themselves cross, which the retrace lane proves. They are SKIPPED and
not deregistered: a case dropped from the registration would make the two sets
differ silently, which is the drift the macro exists to prevent. What retires
the skip is cross-process telemetry, which is also what S8's SessionFaultCount
needs.

TWO REAL REGRESSIONS THE EXISTING GATES CAUGHT, both from this refactor:

  * the macro dropped the per-block TEST_LIST variables, which were what
    SplitLogPaths fed to give every entry a PRIVATE MOBILEGL_LOG_FILE_PATH.
    SplitLogPaths.PrivateAndDistinct went red naming the first orphan. The
    macro accumulates list NAMES now, so a block added later is covered without
    anyone remembering to extend a list - and the names have to be unique,
    because gtest_discover_tests' TEST_LIST is populated when CTEST runs, not
    at configure time, so one shared variable held only the last block's
    entries. That matters more on the spawn arm: there TWO PROCESSES append to
    the path, so a shared file interleaves two sessions rather than merely
    being ambiguous.

  * the configure_file that substitutes those names ran BEFORE the F1 and Ct
    blocks registered, so it captured neither. Same gate, naming
    F1WireScenario.ClearBufferfiPixels. It now runs last.

junit_tally gains --require-spawn-ran beside --require-split-ran: the prefix IS
the arm, and a shared flag would let one lane tally the other's entries and call
itself run. Verified to discriminate - the spawn junit reads 96/0/5 under its
own flag and 0/0/0 under the split flag, which correctly fails.

Two death tests follow the ADOPT_TIER message the census commit retagged with
Fatal{UnimplementedAdoptTier}; their regexes asserted the old wording.

Unit 2353 (one pre-existing environmental failure), integration-split 186/186,
integration-spawn 102/102, retrace 6/6, fatal census 0 unmarked, G1 0 symbols
and .text unchanged at 0xa52203.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 20:20:24 -04:00
co-authored by Claude Opus 5
parent e50463dc5c
commit 8cd00f42fd
7 changed files with 246 additions and 132 deletions
+17 -6
View File
@@ -20,10 +20,18 @@ import sys
import xml.etree.ElementTree as ET
def tally(path, split_only=False):
# P6 `t6`: ONE FLAG PER ARM, because the prefix IS the arm and a shared flag would let a lane
# tally the other lane's entries and call itself run. Exit gate 9.3 compares the two.
ARM_PREFIXES = {
'--require-split-ran': 'DirectGLES.Split.',
'--require-spawn-ran': 'DirectGLES.Spawn.',
}
def tally(path, prefix=None):
passed = failed = skipped = 0
for case in ET.parse(path).getroot().iter('testcase'):
if split_only and not case.get('name', '').startswith('DirectGLES.Split.'):
if prefix is not None and not case.get('name', '').startswith(prefix):
continue
if case.find('failure') is not None or case.find('error') is not None:
failed += 1
@@ -35,17 +43,20 @@ def tally(path, split_only=False):
def main():
if len(sys.argv) not in (2, 3) or (len(sys.argv) == 3 and sys.argv[2] != '--require-split-ran'):
print("usage: junit_tally.py <junit.xml> [--require-split-ran]", file=sys.stderr)
if len(sys.argv) not in (2, 3) or (len(sys.argv) == 3 and sys.argv[2] not in ARM_PREFIXES):
print("usage: junit_tally.py <junit.xml> [" + " | ".join(sorted(ARM_PREFIXES)) + "]",
file=sys.stderr)
return 2
try:
passed, failed, skipped = tally(sys.argv[1], split_only=len(sys.argv) == 3)
prefix = ARM_PREFIXES[sys.argv[2]] if len(sys.argv) == 3 else None
passed, failed, skipped = tally(sys.argv[1], prefix=prefix)
except Exception as exc: # a malformed file is not "zero of everything"
print(f"junit_tally: cannot parse {sys.argv[1]}: {exc}", file=sys.stderr)
return 1
print(f"{passed} {failed} {skipped}")
if len(sys.argv) == 3 and (passed == 0 or failed):
print('split baseline FAILED: no successful split runtime entries or an already-red selection', file=sys.stderr)
print(f'{prefix} baseline FAILED: no successful entries with that arm prefix, or an '
f'already-red selection', file=sys.stderr)
return 1
return 0
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Exit gate 9.2: integration-spawn's case-name set equals integration-split's.
WHY A GATE AND NOT A COMMENT. The two lanes are registered from one macro, so today they agree
by construction - but a macro is one edit away from a second hand-written block, and a spawn lane
covering LESS than the split lane goes green while proving less. Nothing about a smaller green
lane looks wrong. This asserts the sets rather than the counts, because a count can agree while
the membership does not.
ARM PREFIXES ARE STRIPPED BEFORE COMPARING. The lanes deliberately differ in name -
`DirectGLES.Split.X` against `DirectGLES.Spawn.X` - and the sub-lane tails (`F1.`, `Ct.`) are
part of neither the case nor the arm, so both come off.
ONE SANCTIONED ASYMMETRY, LISTED HERE AND NOWHERE ELSE. `integration-split` also labels the
PUSH-MONOLITH arm (the CMakeLists explains why: the label means "the set the disaggregation gate
runs", not "runs inproc"), and that arm's scenario is monolith-only by construction. It cannot
have a spawn counterpart, so it is named as an exception rather than allowed to widen the
tolerance for everything else.
"""
import argparse
import re
import subprocess
import sys
# Registered only on the push-monolith arm, which has no spawn counterpart by construction.
MONOLITH_ONLY = {"MonolithAttachmentClearScenario"}
CASE = re.compile(r"DirectGLES\.[A-Za-z0-9]+\.(?:F1\.|Ct\.)?([A-Za-z0-9_]+Scenario\.[A-Za-z0-9_]+)")
def lane_cases(build_dir, label):
out = subprocess.run(["ctest", "-N", "-L", label], cwd=build_dir,
capture_output=True, text=True).stdout
return {m.group(1) for m in CASE.finditer(out)}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("build_dir")
args = parser.parse_args()
split = lane_cases(args.build_dir, "integration-split")
spawn = lane_cases(args.build_dir, "integration-spawn")
if not split or not spawn:
print(f"::error::a lane is EMPTY (split={len(split)}, spawn={len(spawn)}). An empty set "
f"compares equal to nothing and would pass this gate silently, which is the one "
f"way it could stop meaning anything.", file=sys.stderr)
return 1
split_comparable = {c for c in split if c.split(".", 1)[0] not in MONOLITH_ONLY}
missing = sorted(split_comparable - spawn)
extra = sorted(spawn - split_comparable)
print(f"spawn-lane parity: split {len(split)} ({len(split_comparable)} comparable), "
f"spawn {len(spawn)}")
if missing:
print("::error::cases in integration-split with no integration-spawn counterpart: " +
", ".join(missing) + ". Exit gate 9.2 requires the sets to be equal - a spawn lane "
"that covers less goes green while proving less. Register through "
"mgl_itest_register_split_arms, which emits both arms from one filter.",
file=sys.stderr)
if extra:
print("::error::cases in integration-spawn that integration-split does not run: " +
", ".join(extra) + ". The split arm is the control; a case only the spawn arm runs "
"has no baseline to be compared against.", file=sys.stderr)
return 1 if (missing or extra) else 0
if __name__ == "__main__":
sys.exit(main())