mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
106 lines
4.9 KiB
Python
106 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
# MobileGL - scripts/ci/wait_boundary_negative_control.py
|
|
# Copyright (c) 2025-2026 MobileGL-Dev
|
|
# Licensed under the GNU Lesser General Public License v3.0:
|
|
# https://www.gnu.org/licenses/gpl-3.0.txt
|
|
# https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
# SPDX-License-Identifier: LGPL-3.0-only
|
|
# End of Source File Header
|
|
|
|
"""E1: green/red/restored-green proof of real ClientSession wait boundaries.
|
|
|
|
The peer holds apply until the actual transport parks or the emit call returns.
|
|
VERB_BARRIER=0 must fail only the two owed waits; ordinary run-ahead draws need
|
|
not fail. Each arm is checked from its own CTest JUnit testcase, never a global
|
|
grep, stale library log, arbitrary nonzero exit, or timing-dependent pixel.
|
|
"""
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
PREFIX = "RemoteWaitBoundaryControl."
|
|
CASES = {
|
|
"AppliedClassWaitsWithRunAheadCap": ("ApplierReset", 1, True,
|
|
"E1 wait boundary: ApplierReset returned before apply"),
|
|
"MissingServerCapKeepsClearLockstep": ("Clear", 0, True,
|
|
"E1 wait boundary: Clear without server cap returned before apply"),
|
|
"ServerCapAllowsClearToRunAhead": ("Clear", 1, False, ""),
|
|
}
|
|
|
|
|
|
def own_negative(text, expected_observation, diagnostic):
|
|
return expected_observation in text and diagnostic in text
|
|
|
|
|
|
def inspect(path, selected, barrier, negative, rc):
|
|
root = ET.parse(path).getroot()
|
|
cases = root.findall(".//testcase")
|
|
names = [case.get("name") for case in cases]
|
|
if len(names) != len(set(names)) or set(names) != set(selected):
|
|
raise ValueError("selected testcase names are missing, duplicated or unexpected")
|
|
for case in cases:
|
|
name = case.get("name")
|
|
if case.find("skipped") is not None or case.get("status") in {"notrun", "disabled"}:
|
|
raise ValueError(f"{name}: selected testcase skipped or did not run")
|
|
failed = case.find("failure") is not None or case.find("error") is not None
|
|
op, cap, waits, diagnostic = CASES[name.removeprefix(PREFIX)]
|
|
waited = int(waits and barrier == 1)
|
|
observation = (f"E1 observation: op={op} cap={cap} barrier={barrier} parked={waited} "
|
|
f"applied_before_return={waited} emitted=1 peer_seen=1")
|
|
text = "\n".join(case.itertext())
|
|
if negative:
|
|
if not failed or rc == 0:
|
|
raise ValueError(f"{name}: disabled barrier did not fail the owed wait")
|
|
if not own_negative(text, observation, diagnostic):
|
|
raise ValueError(f"{name}: red lacks its own observed wait-boundary failure")
|
|
elif failed or rc != 0 or observation not in text:
|
|
raise ValueError(f"{name}: baseline/restored arm did not execute its green boundary proof")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--ctest", default=os.environ.get("CTEST", "ctest"))
|
|
parser.add_argument("--out", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
args.out.mkdir(parents=True, exist_ok=True)
|
|
all_cases = [PREFIX + name for name in CASES]
|
|
negative_cases = [PREFIX + name for name, (_, _, waits, _) in CASES.items() if waits]
|
|
ok = True
|
|
for arm, barrier, negative, selected in (
|
|
("baseline", 1, False, all_cases),
|
|
("control", 0, True, negative_cases),
|
|
("restored", 1, False, all_cases),
|
|
):
|
|
# Never run a negative arm against a failed baseline. A failed negative
|
|
# still proceeds through restoration and must not consume stale XML.
|
|
if arm == "control" and not ok:
|
|
continue
|
|
result = args.out / f"e1-{arm}.xml"
|
|
result.unlink(missing_ok=True)
|
|
env = os.environ.copy()
|
|
env["MOBILEGL_IPC_VERB_BARRIER"] = str(barrier)
|
|
selector = "^(" + "|".join(re.escape(name) for name in selected) + ")$"
|
|
command = [args.ctest, "-L", "unit", "-R", selector, "--no-tests=error",
|
|
"--output-on-failure", "--output-junit", str(result)]
|
|
try:
|
|
run = subprocess.run(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
text=True, timeout=90)
|
|
(args.out / f"e1-{arm}.out").write_text(run.stdout, encoding="utf-8")
|
|
print(run.stdout, end="", flush=True)
|
|
inspect(result, selected, barrier, negative, run.returncode)
|
|
print(f"E1 {arm}: {'EXPECTED_RED' if negative else 'GREEN'} ({len(selected)} cases)", flush=True)
|
|
except (OSError, ValueError, ET.ParseError, subprocess.TimeoutExpired) as exc:
|
|
print(f"E1 FAILED: {arm}: {exc}", file=sys.stderr, flush=True)
|
|
ok = False
|
|
if ok:
|
|
print("negative control E1: owed waits failed by their own observed boundary assertions; baseline and restoration green")
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|