874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
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())
|