Files
Ludovic MarieandClaude Opus 5.5 a4355c7c9b Catch flaky harnesses: --repeat, and three runs in the gate
parity.py --repeat N runs each harness N times; its runs must agree, else it
is reported FLAKY and recorded as failing. Later rounds run on the machine
that ran the first, reusing its oracle results, and never two at once there.
With --flake-check REF (passed by gate.py, merge base with parity-base; by
supervise.py integrate, the integration branch before the merge), harnesses
whose script, oracle or probe is new or changed must pass three runs in a
row; a pass records its rounds so it is not repeated again. gate.py matches
shards of a known failure by the harness's name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 17:10:08 +02:00

112 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""The check a branch must pass before it merges into the integration branch.
python3 tools/gate.py # everything
python3 tools/gate.py --no-parity # skip the vanilla harnesses
python3 tools/gate.py --quick # formatting, lints and Python tests only
python3 tools/gate.py --known-failures mob_ai,light # harnesses already failing on the base
Runs, in order, stopping at the first failure: the public-tree check,
rustfmt, clippy with warnings denied, the Rust tests, the Python tests, and
then every vanilla harness whose inputs changed (tools/parity.py); those whose
script, oracle or probe is new or changed since the merge base with
--flake-base must pass 3 runs in a row, which catches most flaky ones. Harnesses
listed with --known-failures may still fail (they fail on the integration
branch too); any other failing harness fails the gate. CI runs the
same Rust and Python checks; the harnesses are the part CI cannot run.
The result is written to .local/gate.json.
"""
import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
import time
import uuid
ROOT = Path(__file__).resolve().parents[1]
RESULT = ROOT / ".local" / "gate.json"
def steps(args):
yield "public tree", [sys.executable, "tools/check_public_tree.py"]
yield "rustfmt", ["cargo", "+stable", "fmt", "--all", "--", "--check"]
yield "clippy", ["cargo", "+stable", "clippy", "--locked", "--workspace", "--all-targets", "--", "-D", "warnings"]
if args.quick:
yield "python tests", [sys.executable, "-m", "unittest", "discover", "-s", "tests", "-q"]
return
# Clippy has already compiled the examples; building them again for the
# test run is most of the time and disk `cargo test --workspace` takes.
yield "rust tests", ["cargo", "+stable", "test", "--locked", "--workspace", "--lib", "--bins", "--tests"]
yield "doc tests", ["cargo", "+stable", "test", "--locked", "--workspace", "--doc"]
yield "python tests", [sys.executable, "-m", "unittest", "discover", "-s", "tests", "-q"]
if not args.no_parity:
command = [sys.executable, "tools/parity.py", *(["-j", str(args.jobs)] if args.jobs is not None else [])]
if args.flake_base:
command += ["--flake-check", args.flake_base]
yield "vanilla harnesses", command
def head():
return subprocess.run(["git", "rev-parse", "HEAD"], cwd=ROOT, capture_output=True, text=True).stdout.strip()
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--no-parity", action="store_true", help="skip the vanilla harnesses")
parser.add_argument("--quick", action="store_true", help="only formatting, lints and Python tests")
parser.add_argument("--known-failures", default="", help="comma-separated harnesses allowed to keep failing")
parser.add_argument("-j", "--jobs", type=int, help="harnesses at once on this machine (default: tools/parity.py's)")
parser.add_argument("--flake-base", default="parity-base", help="harnesses new or changed since the merge base "
"with this ref must pass 3 runs in a row ('' to skip; default: parity-base)")
args = parser.parse_args()
logs = ROOT / ".local" / "gate"
logs.mkdir(parents=True, exist_ok=True)
commit, dirty = head(), bool(subprocess.run(["git", "status", "--porcelain"], cwd=ROOT, capture_output=True, text=True).stdout.strip())
results = []
passed = True
for name, command in steps(args):
started = time.monotonic()
log = logs / (name.replace(" ", "-") + ".log")
print(f"{name}...", end=" ", flush=True)
if name == "vanilla harnesses":
# Stream the runner's own progress; it keeps per-harness logs.
run_id = uuid.uuid4().hex
subprocess.run(command, cwd=ROOT, env=dict(os.environ, PARITY_RUN_ID=run_id))
summary = ROOT / ".local/parity-runner/last-run.json"
finished = json.loads(summary.read_text()) if summary.exists() else {}
if finished.get("run") != run_id:
print("tools/parity.py stopped before finishing its run")
status = 1
else:
known = set(filter(None, args.known_failures.split(",")))
failing = set(finished["failed"])
new = sorted(name for name in failing if name not in known and name.split("#")[0] not in known) # shards
if failing:
print(f"Failing harnesses: {', '.join(sorted(failing))}; new: {', '.join(new) or 'none'}")
status = 1 if new else 0
else:
with log.open("w") as stream:
status = subprocess.run(command, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT).returncode
seconds = round(time.monotonic() - started)
results.append({"step": name, "passed": status == 0, "seconds": seconds})
if status:
passed = False
print(f"FAILED ({seconds}s)")
if name != "vanilla harnesses":
print("".join(log.read_text(errors="replace").splitlines(keepends=True)[-40:]))
print(f"Full log: {log}")
break
print(f"ok ({seconds}s)")
RESULT.write_text(json.dumps({"commit": commit, "dirty": dirty, "passed": passed, "quick": args.quick,
"parity": not (args.quick or args.no_parity), "steps": results,
"finished": time.strftime("%Y-%m-%dT%H:%M:%S")}, indent=1))
print("Gate passed." if passed else "Gate failed.")
return 0 if passed else 1
if __name__ == "__main__":
sys.exit(main())