mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user