mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
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]>
112 lines
5.6 KiB
Python
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())
|