#!/usr/bin/env python3 """Run the vanilla differential harnesses (tools/test_*_parity.py) quickly. python3 tools/parity.py # harnesses whose inputs changed, or that never passed python3 tools/parity.py --all # every harness, from scratch (the audit) python3 tools/parity.py --fresh ... # don't reuse vanilla oracle results python3 tools/parity.py food light # these harnesses (names without test_/_parity.py) python3 tools/parity.py --failed # the ones that failed last time python3 tools/parity.py --changed origin/main # those a branch's changes can affect (pull requests) python3 tools/parity.py --changed-files F # the same, for paths listed in F (a copy without Git history) python3 tools/parity.py --list # what would run, without running it python3 tools/parity.py --status # last result of every harness Harnesses in parity_hosts.json's "named_only" run only when named or with --all (the nightly audit): no automatic selection picks them. Each harness runs unmodified, in parallel, under a tracer that records what it read: its Python modules, Java oracle sources, other repository files, and the Rust sources of every probe it executed (from Cargo's dep-info). A harness reruns only when one of those files, the pinned vanilla artifacts or the toolchains change; otherwise its last pass stands. Vanilla oracles are reused too: tools/parity_shim.py stands in for java and javac and replays an oracle run whose inputs are unchanged, so a Rust-only change costs only the Rust side. A harness that fails while reusing oracle results is rerun fresh, and stops reusing them if it then passes. --all and --fresh never reuse them. A harness listed in parity_hosts.json's "known_flaky" (each with its reason) that fails runs once more from scratch instead, and a pass then counts. Every retry is printed as a RETRIED line in the summary and in the CI job's summary. Fix the cause, then take the harness off the list: nothing else gets this retry. Harness probes build into .local/parity-target with release optimisations but without LTO and with 16 codegen units (`PROFILE` below), which builds in seconds instead of minutes. Rust does not let those settings change results: there is no fast-math, and overflow checks stay as in release. Probes built in the dev profile are optimised too, keeping their overflow checks. The probes the selected harnesses built last time are built before they start, so parallel harnesses don't queue on Cargo's lock, with each harness's own package and feature flags so that its Cargo run then finds them fresh. """ import argparse import hashlib import json import os from pathlib import Path, PurePosixPath import re import shutil import signal import socket import subprocess import sys import time try: from . import parity_reach except ImportError: import parity_reach ROOT = Path(__file__).resolve().parents[1] LOCAL = ROOT / ".local" STATE_DIR = LOCAL / "parity-runner" STATE = STATE_DIR / "state.json" TARGET = LOCAL / "parity-target" PROFILE = { "CARGO_PROFILE_RELEASE_LTO": "false", "CARGO_PROFILE_RELEASE_CODEGEN_UNITS": "16", "CARGO_PROFILE_RELEASE_INCREMENTAL": "true", # Probes built in the dev profile keep its overflow checks and debug # assertions, but run optimised. "CARGO_PROFILE_DEV_OPT_LEVEL": "3", } SHIM = STATE_DIR / "shim" # Results shared by every worktree using the same vanilla data (.local/minecraft # links to a shared directory in parallel worktrees): passes keyed by content, # so a harness whose inputs match one that passed elsewhere doesn't run again. SHARED = Path(os.environ.get("PARITY_SHARED") or (LOCAL / "minecraft").resolve().parent / "parity-shared") MACHINE_JOBS = int(os.environ.get("PARITY_MACHINE_JOBS") or max(1, (os.cpu_count() or 4) // 2)) HOST = socket.gethostname().split(".")[0] DEFAULT_MEMORY = 3 << 30 # assumed peak for a harness that never ran here FAILURE_TAILS, FAILURE_TAIL_LINES = 10, 30 # the summary prints the end of the first failures' logs def harness_settings(path): """PARITY_SHARDS and PARITY_RUST_SCOPE, read from a harness's source without running it.""" import ast text = Path(path).read_text(encoding="utf-8") settings = {} if "PARITY_" in text: for node in ast.parse(text).body: if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) \ and node.targets[0].id in ("PARITY_SHARDS", "PARITY_RUST_SCOPE"): settings[node.targets[0].id] = ast.literal_eval(node.value) return settings SHARDS = {} # "datafix#2" -> "2/8": the share of a sharded harness each entry runs (PARITY_SHARD) def harnesses(): """Harness names and scripts. A harness declaring PARITY_SHARDS = N runs as N entries, NAME#1 to NAME#N.""" found = {} for path in sorted((ROOT / "tools").glob("test_*_parity.py")): name = path.name[len("test_"):-len("_parity.py")] count = harness_settings(path).get("PARITY_SHARDS", 1) if count > 1: for index in range(1, count + 1): found[f"{name}#{index}"] = path SHARDS[f"{name}#{index}"] = f"{index}/{count}" else: found[name] = path return found def base_name(name): """The harness a shard belongs to: datafix#2 -> datafix.""" return name.split("#")[0] def load_state(): return json.loads(STATE.read_text(encoding="utf-8")) if STATE.exists() else {"harnesses": {}, "hashes": {}} def save_state(state): STATE_DIR.mkdir(parents=True, exist_ok=True) temporary = STATE.with_suffix(".tmp") temporary.write_text(json.dumps(state, indent=1, sort_keys=True)) temporary.replace(STATE) class Hasher: """Content hashes, reused while a file's size and mtime stay the same.""" def __init__(self, cache): self.cache = cache def __call__(self, path): real, _, keep = str(path).partition(KEEP) try: stat = os.stat(real) except FileNotFoundError: return "missing" stamp = [stat.st_size, stat.st_mtime_ns] cached = self.cache.get(str(path)) if cached and cached[0] == stamp: return cached[1] if keep: digest = hashlib.sha256(crate_root_without_modules(Path(real).read_text(encoding="utf-8"), set(keep.split(","))).encode()).hexdigest() else: with open(real, "rb") as stream: digest = hashlib.file_digest(stream, "sha256").hexdigest() self.cache[str(path)] = [stamp, digest] return digest # A narrowed crate root: "crates/c/src/lib.rs#keep:a,b" stands for lib.rs without its # declarations of modules other than a and b (see narrow_rust_sources). KEEP = "#keep:" MODULE_DECLARATION = re.compile(r"^\s*(?:pub(?:\([^)]*\))?\s+)?mod\s+(\w+)\s*;\s*$") def crate_root_without_modules(text, keep): """A crate root's text without the `mod x;` lines (and their attributes and doc comments) for modules not in keep.""" lines, pending = [], [] for line in text.splitlines(): stripped = line.strip() if stripped.startswith(("#[", "///", "//!")) and not stripped.startswith("#!["): pending.append(line) continue declared = MODULE_DECLARATION.match(line) if declared and declared.group(1) not in keep: pending = [] continue lines += pending + [line] pending = [] return "\n".join(lines + pending) def environment_key(hasher): """Everything outside the recorded dependencies that can change a result.""" def version(command): try: result = subprocess.run(command, capture_output=True, text=True, cwd=ROOT) return (result.stdout + result.stderr).strip() except FileNotFoundError: return "missing" lock = json.loads((ROOT / "research-lock.json").read_text(encoding="utf-8")) version_dir = LOCAL / "minecraft" / lock["minecraft"]["version"] parts = [version(["rustc", "-V"]), version(["java", "-version"]), version(["javac", "-version"]), json.dumps(PROFILE, sort_keys=True), hasher(ROOT / "research-lock.json"), hasher(ROOT / "Cargo.lock"), hasher(version_dir / "prepared.json"), hasher(version_dir / "manifest.json")] return hashlib.sha256("\n".join(parts).encode()).hexdigest() def key(record, hasher, environment): digest = hashlib.sha256(environment.encode()) for path in sorted(record["dependencies"]): digest.update(f"{path}\0{hasher(ROOT / path)}\n".encode()) return digest.hexdigest() def inside(path, root): try: Path(path).resolve().relative_to(root) return True except (ValueError, OSError): return False def dep_info(binary): """Source files Cargo recorded for a built binary (its .d file, spaces escaped as "\\ ").""" info = Path(str(binary) + ".d") if not info.exists(): return [] sources = [] for line in info.read_text(encoding="utf-8").splitlines(): _, separator, rest = line.partition(": ") if separator: sources += [part.replace("\\ ", " ") for part in re.split(r"(?", " ") parameters = set() if header.startswith("<"): # generic parameters depth, start = 0, 1 for index, c in enumerate(header): if depth == 1 and c in ",>": parameter = re.match(r"\s*(?:const\s+)?([A-Za-z_]\w*)", header[start:index]) if parameter: parameters.add(parameter.group(1)) start = index + 1 depth += c == "<" depth -= c == ">" if depth == 0: header = header[index + 1:] break names = set() for part in re.split(r"\bfor\b", header, maxsplit=1): path = re.sub(r"<.*", "", part.strip().lstrip("!&").replace("dyn ", "").strip(), flags=re.DOTALL).strip() segments = [segment for segment in path.split("::") if segment] if not segments: continue head, last = segments[0], segments[-1] if not re.fullmatch(r"[A-Za-z_]\w*", last): names.update(set(re.findall(r"[A-Za-z_]\w*", part)) - {"mut", "const"}) # a tuple, slice or pointer type continue if len(segments) > 1 and head not in ("crate", "self", "super") and head not in workspace: continue # qualified by an external crate if len(segments) == 1: if re.search(rf"\b(?:struct|enum|trait|union)\s+{last}\b", text): continue # defined in this file (an import of the same name would not compile) imported = re.search(rf"\buse\s+(\w+)[^;]*\b{last}\b[^;]*;", text) if imported and imported.group(1) not in ("crate", "self", "super", *workspace): continue # imported from an external crate names.add(last) return names - parameters def narrow_rust_sources(sources, scope): """Drop the workspace Rust sources a harness's declared PARITY_RUST_SCOPE proves it can't reach. The scope lists source prefixes (crates/horizon-world/src/nbt.rs, crates/horizon-world/src/datafix/). It applies only if it is closed: no file in it names a module, crate item or macro outside it (crate::, super::, other workspace crates), declares a module it doesn't cover, or includes source; and no file outside it implements anything for a type it defines. Crate roots stay as dependencies without their declarations of unreachable modules, so adding a module elsewhere doesn't count. Probe entry points (examples, bins) are always kept. Returns the kept sources and None, or all of them and the reason the scope isn't closed.""" crates = {path.parent.name.replace("-", "_"): path.parent for path in (ROOT / "crates").glob("*/Cargo.toml")} def scoped(path): return any(path == prefix or (prefix.endswith("/") and path.startswith(prefix)) for prefix in scope) rust = [path for path in sources if path.startswith("crates/") and path.endswith(".rs")] entry = [path for path in rust if "/examples/" in path or "/src/bin/" in path or path.endswith("/src/main.rs")] inner = [path for path in rust if scoped(path)] outside = [path for path in rust if path not in inner and path not in entry and not path.endswith("/src/lib.rs")] def module_file(crate_dir, parts): """The file of the deepest module named by parts, and how many parts it covers.""" for count in range(len(parts), 0, -1): for candidate in (crate_dir / "src" / Path(*parts[:count - 1]) / f"{parts[count - 1]}.rs", crate_dir / "src" / Path(*parts[:count]) / "mod.rs"): if candidate.is_file(): return candidate.relative_to(ROOT).as_posix(), count return None, 0 def module_path(path): parts = list(PurePosixPath(path).parts[3:-1] + (PurePosixPath(path).with_suffix("").name,)) return parts[:-1] if parts[-1] in ("mod", "lib") else parts defined_types, used_macros, kept_modules = set(), set(), {} for path in inner + entry: text = rust_code((ROOT / path).read_text(encoding="utf-8", errors="replace")) crate_name = PurePosixPath(path).parts[1].replace("-", "_") nesting = inline_modules(text) if re.search(r"\binclude!\s*\(", text): return sources, f"{path} includes source" used_macros.update(RUST_MACRO_USE.findall(text)) if path in inner: defined_types.update(RUST_TYPE.findall(text)) kept_modules.setdefault(crate_name, set()).add(module_path(path)[0] if module_path(path) else "") for line in text.splitlines(): declared = MODULE_DECLARATION.match(line) if declared: child, _ = module_file(crates[crate_name], module_path(path) + [declared.group(1)]) if child and not scoped(child): return sources, f"{path} declares module {declared.group(1)} outside the scope" for match in RUST_PATH.finditer(text): head, rest = match.groups() parts = rest.split("::") if head in crates: # a workspace crate base, crate_name = [], head elif head.startswith(("crate", "self", "super")) and path in inner: own, crate_name = module_path(path) + nesting(match.start()), PurePosixPath(path).parts[1].replace("-", "_") ups = head.count("super") if head != "crate" and ups <= len(nesting(match.start())): continue # within this file base = [] if head == "crate" else own[:len(own) - ups] else: continue # an external crate, a local name or a probe's own module target, covered = module_file(crates[crate_name], base + parts) if target is None: # An item of the crate root: fine if defined there (the narrowed root keeps it), not if re-exported. root = crates[crate_name] / "src" / "lib.rs" item = (base + parts)[0] defined_in_root = root.is_file() and re.search( rf"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:const|static|fn|struct|enum|type|trait|union)\s+{item}\b", root.read_text(encoding="utf-8"), re.MULTILINE) if not defined_in_root: return sources, f"{path} names {head}::{rest}, which the crate root doesn't define itself" continue if not scoped(target): return sources, f"{path} names {head}::{rest} ({target})" for path in outside: text = rust_code((ROOT / path).read_text(encoding="utf-8", errors="replace")) defined = set(RUST_MACRO_DEFINITION.findall(text)) & used_macros if defined: return sources, f"the scope uses macro {sorted(defined)[0]}! defined in {path}" for header in re.findall(r"^\s*(?:unsafe\s+)?impl\b([^;{]*)", text, re.MULTILINE): touched = impl_targets(header, text, set(crates)) & defined_types if touched: return sources, f"{path} implements something for {sorted(touched)[0]}, defined in the scope" kept = [path for path in sources if path not in outside and not path.endswith("/src/lib.rs")] for path in sources: if path.endswith("/src/lib.rs"): modules = kept_modules.get(PurePosixPath(path).parts[1].replace("-", "_")) or {"-"} # "-": no module of it is reachable kept.append(f"{path}{KEEP}{','.join(sorted(modules))}") return sorted(kept), None PROBE_SOURCE = re.compile(r"/examples/probe/(?:(support)/)?(\w+)\.rs$") # Each crate's probe example (crates//examples/probe) and its target name. Until 2026-10-24 they were all # `probe`, and Cargo copies every crate's examples into one /examples/: a harness could run the binary # another harness had just copied there. PROBE_TARGETS = {"horizon-client": "client_probe", "horizon-protocol": "protocol_probe", "horizon-world": "world_probe"} OLD_PROBE_SOURCE = re.compile(r"crates/([\w-]+)/examples/probe[/.]") def probes_run(commands): """The probes a harness ran through a crate's probe binary: `world_probe ...`, or `cargo run --example world_probe -- ...`.""" names = set() for arguments in commands: program = Path(arguments[0]).name.removesuffix(".exe") if arguments else "" if program in PROBE_TARGETS.values() and len(arguments) > 1: names.add(arguments[1]) elif program == "cargo" and "--" in arguments and len(arguments) > arguments.index("--") + 1 \ and any(kind == "example" and name in PROBE_TARGETS.values() for kind, name, *_ in artifacts_of(arguments)): names.add(arguments[arguments.index("--") + 1]) return names def recorded_artifacts(record): """A harness record's artifacts, with the shared `probe` target of records from before 2026-10-24 renamed to its crate's own (PROBE_TARGETS): the crate the record's `-p` selection names, else the one whose probe sources the harness depended on. The prebuild would otherwise drop them as targets that no longer exist.""" artifacts = [] for artifact in record.get("artifacts", []): if artifact[:2] != ["example", "probe"]: artifacts.append(artifact) continue selection = artifact[3] if len(artifact) > 3 else [] crates = [value for flag, value in zip(selection, selection[1:]) if flag == "-p" and value in PROBE_TARGETS] \ or sorted({match.group(1) for path in record.get("dependencies", []) if (match := OLD_PROBE_SOURCE.match(path)) and match.group(1) in PROBE_TARGETS}) artifacts += [["example", PROBE_TARGETS[crate], *artifact[2:]] for crate in crates] or [artifact] return artifacts def only_probes_run(paths, names): """A probe binary's dep-info lists every probe in it. Keep those the harness ran, the probes they include! or name (crate::) and the shared support modules they name (support::), so that editing one probe reruns only its own harnesses. Files in a probe's own subdirectory and dispatch.rs, which runs them, are always kept; main.rs, which only lists them, is left out, so adding a probe reruns nothing.""" if not names: return paths names, used = {*names, "dispatch"}, "" while True: ran = [path for path in paths if (match := PROBE_SOURCE.search(path)) and not match.group(1) and match.group(2) in names] used = "".join((ROOT / path).read_text(encoding="utf-8", errors="replace") for path in ran) included = {*re.findall(r'include!\("(\w+)\.rs"\)', used), *re.findall(r"\bcrate::(\w+)", used)} - names if not included: break names |= included return [path for path in paths if not (match := PROBE_SOURCE.search(path)) or (f"support::{match.group(2)}" in used if match.group(1) else match.group(2) in names)] def dependencies(trace, scope=None): """Repository files a traced harness depended on, relative to ROOT, and the probes it built. With a PARITY_RUST_SCOPE, Rust sources it can't reach are left out (see narrow_rust_sources).""" found = set(trace["modules"]) found.update(trace["read"]) artifacts = [] for arguments in trace["commands"]: program = Path(arguments[0]).name if arguments else "" if program == "cargo" and ("build" in arguments or "run" in arguments): artifacts += artifacts_of(arguments) if arguments and inside(arguments[0], TARGET): found.update(dep_info(Path(arguments[0]))) if program == "javac": found.update(java_sources(arguments)) for argument in arguments[1:]: if isinstance(argument, str) and os.path.isfile(argument): found.add(argument) for kind, name, profile, *_ in artifacts: if name: found.update(dep_info(artifact_path(kind, name, profile))) kept = set() for path in found: path = Path(path) if os.path.isabs(path) else ROOT / path if inside(path, ROOT) and not inside(path, LOCAL) and not inside(path, ROOT / "target") and path.is_file() \ and "__pycache__" not in path.parts: # bytecode caches vary; their sources are dependencies already kept.add(str(path.resolve().relative_to(ROOT))) unique = [] for artifact in artifacts: if artifact not in unique: unique.append(artifact) kept = only_probes_run(sorted(kept), probes_run(trace["commands"])) kept, note = narrow_rust_sources(kept, scope) if scope else (kept, None) if note: print(f"note: PARITY_RUST_SCOPE not applied: {note}", flush=True) return kept, unique def java_sources(arguments): """Java files javac compiled: the named ones, plus those it pulled from -sourcepath into -d.""" sources = [argument for argument in arguments if str(argument).endswith(".java")] if "-d" in arguments and "-sourcepath" in arguments: output = Path(arguments[arguments.index("-d") + 1]) source_roots = arguments[arguments.index("-sourcepath") + 1].split(os.pathsep) for compiled in output.rglob("*.class") if output.is_dir() else []: relative = compiled.relative_to(output).with_suffix("") top = relative.with_name(relative.name.split("$")[0] + ".java") for root in source_roots: if (Path(root) / top).is_file(): sources.append(str(Path(root) / top)) return sources def run_environment(reuse_oracles=False): environment = dict(os.environ, CARGO_TARGET_DIR=str(TARGET), PYTHONDONTWRITEBYTECODE="1", HORIZON_NO_AUDIO="1", **PROFILE) environment.pop("CARGO_BUILD_JOBS", None) if reuse_oracles: # tools/parity_shim.py stands in for java and javac and replays unchanged oracle runs. SHIM.mkdir(parents=True, exist_ok=True) for name in ("java", "javac"): link = SHIM / name if not link.is_symlink(): link.symlink_to(ROOT / "tools" / "parity_shim.py") environment.update(PARITY_SHIM_REAL_PATH=os.environ.get("PATH", ""), PARITY_SHIM_CACHE=str(STATE_DIR / "oracle-cache"), PATH=f"{SHIM}{os.pathsep}{os.environ.get('PATH', '')}") return environment def default_build_jobs(): """Cargo jobs for the probe build: PARITY_BUILD_JOBS, else every thread, within $FLEET_LOAD_JOBS when set.""" if os.environ.get("PARITY_BUILD_JOBS"): return int(os.environ["PARITY_BUILD_JOBS"]) jobs = os.cpu_count() or 8 if os.environ.get("FLEET_LOAD_JOBS", "").isdigit(): jobs = min(jobs, max(1, int(os.environ["FLEET_LOAD_JOBS"]))) return jobs def target_packages(): """{(kind, name): package} for the workspace's examples and binaries.""" result = subprocess.run(["cargo", "metadata", "--no-deps", "--format-version", "1"], cwd=ROOT, capture_output=True, text=True) if result.returncode: return {} return {(kind, target["name"]): package["name"] for package in json.loads(result.stdout)["packages"] for target in package["targets"] for kind in target["kind"] if kind in ("example", "bin")} def prebuild_commands(artifacts, packages=None): """The Cargo runs that build the recorded probes exactly as their harnesses do: one per profile, package and feature selection, and whether examples are among the command's targets (see artifacts_of). packages: target_packages(). Records of targets that no longer exist (a probe renamed or deleted since a harness ran it) are left out, since one unknown target fails its whole Cargo run. A [kind, name, profile] record from before selections were kept is taken as built with `-p` and its own crate, as nearly every harness does; the harness's next run records the real one.""" groups = {} for artifact in artifacts: if packages and artifact[1] and tuple(artifact[:2]) not in packages: continue if len(artifact) == 3 and (packages or {}).get(tuple(artifact[:2])): kind, name, profile = artifact artifact = [kind, name, profile, ["-p", packages[(kind, name)]], kind == "example"] if len(artifact) < 5: continue kind, name, profile, selection, examples = artifact targets = groups.setdefault((profile, tuple(selection), examples), []) target = [f"--{kind}", name] if name else [f"--{kind}"] if target not in targets: targets.append(target) return [["cargo", "build", "--locked", "--profile", profile, *selection, *(part for target in targets for part in target)] for (profile, selection, _), targets in sorted(groups.items())] def prebuild(artifacts, jobs): """Build the recorded probes before the harnesses start, so they don't serialise on Cargo's lock.""" environment = run_environment() environment["CARGO_BUILD_JOBS"] = str(jobs) errors = "" packages = target_packages() for command in prebuild_commands(artifacts, packages): result = subprocess.run(command, cwd=ROOT, env=environment, capture_output=True, text=True) if result.returncode: errors += result.stderr return not errors, errors LARGE_OUTPUT = 64 << 20 # Vanilla outputs above this are deleted after a pass too: rerunning those few oracles (12 harnesses on the Mac # mini's runner) costs less than the 26 GB they held in every checkout; smaller ones stay for oracle reuse. KEPT_ORACLE_OUTPUT = 256 << 20 OWN_DIRECTORIES = {"parity-target", "parity-runner", "rea", "logs", "gate"} def outputs_listing(name, fresh=False): path = STATE_DIR / "oracle-outputs" / f"{name}.txt" path.parent.mkdir(parents=True, exist_ok=True) if fresh: path.write_text("") return path def clear_large_outputs(name, trace): """After a pass, delete the large files a harness regenerates on every run. Harness outputs reach gigabytes and used to stay in every worktree. Vanilla oracle outputs under KEPT_ORACLE_OUTPUT are kept, since reusing an oracle run needs them in place; everything else large under the harness's .local directories (`...` or `rust--parity`) is rebuilt on its next run.""" local = LOCAL.resolve() keep = {os.path.realpath(line) for line in outputs_listing(name).read_text(encoding="utf-8").splitlines() if line} own = name.split("#")[0].replace("_", "-") directories = set() for command in trace["commands"]: for argument in command[1:]: path = Path(os.path.realpath(argument if os.path.isabs(argument) else ROOT / argument)) try: top = path.relative_to(local).parts[0] except (ValueError, IndexError): continue # Only the harness's own directory: another harness running beside it may be writing # large files in a directory this one merely read (decorated_region compiles its oracle # into chunk-generation-parity while chunk_generation writes its dumps there). if top not in OWN_DIRECTORIES and (top.startswith(own) or top == f"rust-{own}-parity"): directories.add(local / top) for directory in directories: for file in (directory.rglob("*") if directory.is_dir() else []): try: size = file.stat().st_size if file.is_file() and not file.is_symlink() else 0 if size >= LARGE_OUTPUT and (str(file) not in keep or size > KEPT_ORACLE_OUTPUT): file.unlink() except OSError: pass def publish(name, record): """Share a pass, so any worktree with the same inputs can reuse it.""" if record["status"] != "pass" or "key" not in record: return for path, data in ((SHARED / "passes" / name / f"{record['key']}.json", record), (SHARED / "dependencies" / f"{name}.json", {k: record[k] for k in ("dependencies", "artifacts")})): path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_name(f"{path.name}.{os.getpid()}.tmp") temporary.write_text(json.dumps(data)) temporary.replace(path) def machine_slot(): """One of MACHINE_JOBS lock files shared by every runner on this machine, or None if all are taken.""" import fcntl slots = SHARED / "slots" slots.mkdir(parents=True, exist_ok=True) for index in range(MACHINE_JOBS): handle = open(slots / f"{index}.lock", "w") try: fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) return handle except OSError: handle.close() return None def memory_available(): """Bytes the system can give harnesses now: MemAvailable on Linux, free and reclaimable pages on macOS.""" meminfo = Path("/proc/meminfo") if meminfo.exists(): for line in meminfo.read_text(encoding="utf-8").splitlines(): if line.startswith("MemAvailable:"): return int(line.split()[1]) * 1024 try: text = subprocess.run(["vm_stat"], capture_output=True, text=True).stdout page = int(re.search(r"page size of (\d+) bytes", text).group(1)) pages = sum(int(re.search(rf"^{label}:\s+(\d+)", text, re.MULTILINE).group(1)) for label in ("Pages free", "Pages inactive", "Pages speculative", "Pages purgeable")) return pages * page except (OSError, AttributeError, ValueError): return 16 << 30 REMOTE_LOCAL_JOBS = 3 # harnesses at once here while other machines take the rest REMOTE_LOCAL_PEAK = 800 << 20 # heavier harnesses are left to them MEMORY_RESERVE = 2 << 30 # what admission leaves free here, for the person at the screen def default_local_jobs(remote=False): """Harnesses at once on this machine: PARITY_LOCAL_JOBS, else a third of its threads (3 when other machines help), within the fleet's budget ($FLEET_LOAD_JOBS).""" if os.environ.get("PARITY_LOCAL_JOBS"): return int(os.environ["PARITY_LOCAL_JOBS"]) jobs = REMOTE_LOCAL_JOBS if remote else max(1, (os.cpu_count() or 4) // 3) if os.environ.get("FLEET_LOAD_JOBS", "").isdigit(): jobs = min(jobs, max(1, int(os.environ["FLEET_LOAD_JOBS"]))) return jobs def outcome(name, script, passed, elapsed, trace_path, hasher=None): """The record of a finished harness, from its trace. With a hasher, also the content hash of each dependency.""" record = {"status": "pass" if passed else "fail", "seconds": round(elapsed, 1), "finished": time.strftime("%Y-%m-%dT%H:%M:%S"), "host": HOST} try: trace = json.loads(Path(trace_path).read_text(encoding="utf-8")) except (OSError, ValueError): trace = None # killed before writing it, or the disk was full: the pass can't be keyed if trace: record["dependencies"], record["artifacts"] = dependencies(trace, harness_settings(script).get("PARITY_RUST_SCOPE")) record["peak_bytes"] = trace["peak_bytes"] if passed: clear_large_outputs(name, trace) if hasher is not None: record["hashes"] = {path: hasher(ROOT / path) for path in record["dependencies"]} else: record["dependencies"], record["artifacts"] = [], [] return record class LocalExecutor: """Runs harnesses on this machine, within its job count, memory budget and the machine-wide slots.""" def __init__(self, jobs, memory_budget, timeout, max_peak=None): self.name, self.jobs, self.memory_budget, self.timeout = HOST, jobs, memory_budget, timeout self.max_peak = max_peak # set while other machines can take heavier harnesses self.running = {} self.solo = set() # harnesses that run alone here (solo_harnesses) self.dead = False # this machine never drops out, unlike a remote host self.available, self.measured = 0, 0.0 (STATE_DIR / "logs").mkdir(parents=True, exist_ok=True) (STATE_DIR / "traces").mkdir(parents=True, exist_ok=True) def free(self): if any(harness_of(label) in self.solo for label in self.running): return 0 return self.jobs - len(self.running) def accepts(self, name): # A solo harness waits until nothing else runs here; the others fill the slots meanwhile, so solo ones # run last, alone. return name not in self.solo or not self.running def fits(self, expected, only_here=True): """Admission by recorded peak, within the budget and leaving MEMORY_RESERVE of what is free now. only_here: no other machine can take this harness.""" if self.max_peak is not None and expected > self.max_peak and not only_here: return False if time.monotonic() - self.measured > 1: self.available, self.measured = memory_available(), time.monotonic() if not self.running and (self.max_peak is None or only_here): return True # alone, it must run anyway return sum(item["expected"] for item in self.running.values()) + expected <= self.memory_budget \ and self.available - expected >= MEMORY_RESERVE def start(self, name, script, reuse, expected, shard=None): self.measured = 0.0 # measure again before the next start slot = machine_slot() if slot is None: return False # other runners on this machine hold every slot trace = STATE_DIR / "traces" / f"{name}.json" trace.unlink(missing_ok=True) log = (STATE_DIR / "logs" / f"{name}.log").open("w") environment = run_environment(reuse) # A game the harness starts logs into HORIZON_HOME, which is a real player's folder when it is unset # (lab.game_env keeps this one). game_home = STATE_DIR / "game-home" / name shutil.rmtree(game_home, ignore_errors=True) game_home.mkdir(parents=True) environment.setdefault("HORIZON_HOME", str(game_home)) environment["PARITY_SHIM_OUTPUTS"] = str(outputs_listing(name, fresh=True)) environment.pop("PARITY_SHARD", None) if shard: environment["PARITY_SHARD"] = shard # stdin from /dev/null: tools/parity_shim.py never reuses an oracle run that reads a pipe, and CI runners # (and --worker's ssh) give the runner one, which kept oracle reuse off on CI. process = subprocess.Popen([sys.executable, str(Path(__file__)), "--trace", str(trace), str(script)], cwd=ROOT, env=environment, stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, start_new_session=True) self.running[name] = {"process": process, "started": time.monotonic(), "log": log, "expected": expected, "slot": slot, "script": script} return True def poll(self): """(name, script, passed, seconds, trace path) of each harness that finished or timed out.""" finished = [] for name, item in list(self.running.items()): process, elapsed = item["process"], time.monotonic() - item["started"] if process.poll() is None and elapsed < self.timeout: continue if process.poll() is None: os.killpg(process.pid, 9) process.wait() print(f"--- {name}: timed out after {self.timeout}s", file=item["log"]) item["log"].close() item["slot"].close() del self.running[name] finished.append((name, item["script"], process.returncode == 0, elapsed, STATE_DIR / "traces" / f"{name}.json")) return finished def stop(self): for item in self.running.values(): try: os.killpg(item["process"].pid, 9) except ProcessLookupError: pass def host_config(): """tools/parity_hosts.json: the machines harnesses can run on, and the harnesses that must stay local.""" path = ROOT / "tools" / "parity_hosts.json" return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {"default": [], "hosts": {}, "local_only": {}} class RemoteExecutor: """Runs harnesses on another machine through `parity.py --worker` over ssh. The worktree (minus .git, .local, target and ignored files) is mirrored to /remote/ there; its .local links to the vanilla data in /shared-local. The worker builds the probes and runs each harness it is sent under the same tracer; results come back with the content hash of every dependency, so the pass is keyed on exactly what ran.""" def __init__(self, name, config, artifacts, local_only, timeout): import threading self.name, self.config, self.local_only, self.timeout = name, config, set(local_only), timeout self.jobs, self.memory_budget, self.ready, self.dead = 0, 0, False, False self.running, self.events, self.backoff = {}, [], 0.0 self.lock = threading.Lock() self.process = None self.mirror = f"{config['root']}/remote/{HOST}{str(ROOT).replace('/', '-')}" threading.Thread(target=self._connect, args=(artifacts,), daemon=True).start() def _log(self, text): print(f"[{self.name}] {text}", flush=True) def _connect(self, artifacts): import shlex ssh = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=30", self.config["ssh"]] started = time.monotonic() shared = f"{self.config['root']}/shared-local" mkdir = subprocess.run([*ssh, f"mkdir -p {shlex.quote(self.mirror)}"], capture_output=True, text=True) if mkdir.returncode: self._log(f"unreachable, running everything here: {mkdir.stderr.strip()[-300:]}") self.dead = True return sync = subprocess.run( ["rsync", "-a", "--delete", "--filter=:- .gitignore", "--exclude=/.git", "--exclude=/.local", "--exclude=/target", "--exclude=__pycache__", "-e", " ".join(ssh[:-1]), f"--rsync-path={self.config.get('rsync', 'rsync')}", f"{ROOT}/", f"{self.config['ssh']}:{self.mirror}/"], capture_output=True, text=True) if sync.returncode: self._log(f"sync failed, running everything here: {sync.stderr.strip()[-300:]}") self.dead = True return environment = " ".join(f"{key}={shlex.quote(str(value))}" for key, value in self.config.get("environment", {}).items()) command = (f"{self.config.get('setup', 'true')} && cd {shlex.quote(self.mirror)} && " f"exec nice -n {int(self.config.get('nice', 0))} env PARITY_MACHINE_JOBS={self.config.get('jobs', 8)} " f"{environment} {self.config.get('python', 'python3')} tools/parity.py --worker {shlex.quote(shared)}") self.process = subprocess.Popen([*ssh, f"{self.config.get('shell', 'sh -c')} {shlex.quote(command)}"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1) self._send({"prebuild": artifacts, "timeout": self.timeout}) self._log(f"synced in {time.monotonic() - started:.0f}s, building probes...") for line in self.process.stdout: try: message = json.loads(line) except ValueError: continue with self.lock: if "hello" in message: hello = message["hello"] self.jobs = min(self.config.get("jobs", hello["jobs"]), hello["jobs"]) self.memory_budget = hello["memory"] self.ready = True if not hello["built"]: self._log("probe build failed; harnesses will report their own build errors:\n" + hello["errors"][-2000:]) self._log(f"ready after {time.monotonic() - started:.0f}s: {self.jobs} harnesses at once, " f"{self.memory_budget / (1 << 30):.0f} GB") else: self.events.append(message) with self.lock: self.dead = True def _send(self, message): try: self.process.stdin.write(json.dumps(message) + "\n") self.process.stdin.flush() except (OSError, ValueError, AttributeError): self.dead = True def free(self): if not self.ready or self.dead or time.monotonic() < self.backoff: return 0 return self.jobs - len(self.running) def accepts(self, name): return base_name(name) not in self.local_only and base_name(name) not in self.config.get("exclude", {}) def fits(self, expected, only_here=False): return not self.running or sum(self.running.values()) + expected <= self.memory_budget def start(self, name, script, reuse, expected, shard=None): self.running[name] = expected self._send({"run": name, "script": str(Path(script).relative_to(ROOT)), "reuse": reuse, "expected": expected, "shard": shard}) return True def poll(self): """Finished harnesses, as (name, record or None): None means it must run elsewhere.""" with self.lock: events, self.events = self.events, [] dead = self.dead finished = [] for message in events: name = message.get("done") or message.get("rejected") if name not in self.running: continue del self.running[name] if "rejected" in message: self.backoff = time.monotonic() + 5 # other runners hold the remote machine's slots finished.append((name, None)) else: (STATE_DIR / "logs").mkdir(parents=True, exist_ok=True) (STATE_DIR / "logs" / f"{name}.log").write_text(message.get("log", "")) finished.append((name, message["record"])) if dead and self.running: self._log("connection lost; its harnesses run elsewhere") finished += [(name, None) for name in self.running] self.running = {} return finished def stop(self): if self.process and self.process.poll() is None: try: self.process.stdin.close() except OSError: pass try: self.process.wait(timeout=30) except subprocess.TimeoutExpired: self.process.kill() def key_from(record, hashes, environment): """key() over the content hashes a remote machine recorded for each dependency.""" return key(record, lambda path: hashes.get(str(Path(path).relative_to(ROOT)), "missing"), environment) def harness_of(label): """The harness a queue entry runs: food~2 is the second round of food.""" return label.split("~")[0] def keep_first_log(label): """Move a harness's log aside before its retry writes a new one; returns where it went.""" log = STATE_DIR / "logs" / f"{label}.log" kept = log.with_name(f"{label}.first-run.log") try: log.replace(kept) except OSError: return log # a remote host's run: its log stays on that machine return kept def run(selected, state, hasher, environment_digest, executors, reuse_oracles, rounds=None, known_flaky=None): """Run the selected harnesses on the executors. rounds: how many times each must run (default once); its runs must agree, and a harness that both passes and fails is reported as flaky and recorded as failing. known_flaky ({name: reason}, tools/parity_hosts.json): a harness listed there that fails its single round runs once more, from scratch; its record keeps `retried` (the first run's log is kept beside its own).""" records = state["harnesses"] rounds = rounds or {} known_flaky = known_flaky or {} second_chance = set() # known flaky harnesses that already failed once in this run # Longest first, so the slow harnesses don't start last; a harness's later rounds after its first. queue = sorted((name if index == 1 else f"{name}~{index}" for name in selected for index in range(1, rounds.get(name, 1) + 1)), key=lambda label: (-records.get(harness_of(label), {}).get("seconds", 1e9), label)) results, retried, busy, done = {}, set(), {}, {} home = {} # where a harness's first round ran: its later rounds run there too, reusing its oracle results started_all = time.monotonic() local = executors[-1] while queue or busy: # Remote machines first: they take the longest harnesses and leave this one lighter. for executor in executors: while queue and executor.free() > 0: # Rounds of one harness share its output directories: never two at once on one machine. here = {harness_of(label) for label, (other, _) in busy.items() if other is executor} pick = next((label for label in queue if harness_of(label) not in here and executor.accepts(harness_of(label)) and (label == harness_of(label) or home.get(harness_of(label), executor) in (executor, None) or home[harness_of(label)].dead) and executor.fits(records.get(harness_of(label), {}).get("peak_bytes", DEFAULT_MEMORY), not any(other.accepts(harness_of(label)) and not other.dead for other in executors[:-1]))), None) if pick is None: break name = harness_of(pick) # A retry runs the oracle again too: it may be the side that failed (mob_ai's heap). reuse = reuse_oracles and records.get(name, {}).get("oracle_reuse", True) and pick not in retried \ and name not in second_chance if not executor.start(pick, selected[name], reuse, records.get(name, {}).get("peak_bytes", DEFAULT_MEMORY), SHARDS.get(name)): break queue.remove(pick) busy[pick] = (executor, reuse) home.setdefault(name, executor) if executors[:-1] and all(executor.dead for executor in executors[:-1]) and local.max_peak is not None: # Every other machine is gone: this one runs everything. local.max_peak, local.jobs = None, max(local.jobs, default_local_jobs()) time.sleep(0.2) finished = [] for executor in executors: if isinstance(executor, LocalExecutor): for label, script, passed, elapsed, trace in executor.poll(): finished.append((label, outcome(label, script, passed, elapsed, trace))) else: finished += executor.poll() for label, record in finished: executor, reused = busy.pop(label) name = harness_of(label) if record is None: queue.insert(0, label) continue passed = record["status"] == "pass" flaky_retry = rounds.get(name, 1) == 1 and base_name(name) in known_flaky if not passed and reused and label == name and records.get(name, {}).get("status") != "fail" \ and not flaky_retry: # A new failure might come from a reused oracle result: run it again from scratch. Later rounds reuse # what the first just validated, so their failures count (a flaky harness). A known flaky harness # gets its own retry below instead, which keeps reusing oracle results afterwards. retried.add(label) queue.insert(0, label) print(f"retry {label} without reused oracle results", flush=True) continue # A harness that only passes fresh no longer reuses oracle results. record["oracle_reuse"] = records.get(name, {}).get("oracle_reuse", True) and not (passed and label in retried) hashes = record.pop("hashes", None) if record["dependencies"]: record["key"] = key_from(record, hashes, environment_digest) if hashes is not None else \ key(record, hasher, environment_digest) where = "" if executor is local else f" on {executor.name}" tail = "" if passed else f" ({STATE_DIR / 'logs' / (label + '.log')})" print(f"{'PASS' if passed else 'FAIL'} {label} {record['seconds']:.0f}s{where}{tail}", flush=True) done.setdefault(name, []).append(record) if len(done[name]) < rounds.get(name, 1): continue runs = done.pop(name) if runs[0]["status"] != "pass" and flaky_retry and name not in second_chance: second_chance.add(name) first_log = keep_first_log(label) print(f"RETRIED {name}: failed, and it is a known flaky harness ({known_flaky[base_name(name)]}): " f"running it once more (first run's log: {first_log})", flush=True) queue.insert(0, label) continue record = dict(runs[0], status="pass" if all(run["status"] == "pass" for run in runs) else "fail", rounds=len(runs)) if name in second_chance: record["retried"] = "known flaky" elif label in retried: record["retried"] = "reused oracle results" if len({run["status"] for run in runs}) > 1: record["flaky"] = [run["status"] for run in runs] print(f"FLAKY {name}: {', '.join(record['flaky'])} in {len(runs)} runs", flush=True) records[name] = record results[name] = record save_state(state) publish(name, record) return results, time.monotonic() - started_all def exit_on_termination(): """SIGTERM and SIGHUP (a cancelled CI job, a dropped ssh) end this process through its finally blocks, which stop the running harnesses. Each runs in its own session, so without that they, and the vanilla servers they started, outlive it (orphans of days were found on the Mac mini's runners, 2026-10-26).""" for number in (signal.SIGTERM, signal.SIGHUP): signal.signal(number, lambda received, _frame: sys.exit(128 + received)) def worker_main(shared_local): """`parity.py --worker SHARED_LOCAL`: run harnesses sent as JSON lines on stdin, answer on stdout.""" import queue as queues import threading global SHARED LOCAL.mkdir(exist_ok=True) shared_local = Path.home() / Path(shared_local).expanduser() # relative paths are from the home directory for entry in sorted(shared_local.iterdir()) if shared_local.is_dir() else []: if entry.is_dir() and entry.name not in {"parity-shared", "datafix"} and not (LOCAL / entry.name).exists(): (LOCAL / entry.name).symlink_to(entry) if not os.environ.get("PARITY_SHARED"): SHARED = (LOCAL / "minecraft").resolve().parent / "parity-shared" inbox = queues.Queue() def read(): for line in sys.stdin: inbox.put(json.loads(line)) inbox.put(None) threading.Thread(target=read, daemon=True).start() channel = sys.stdout sys.stdout = sys.stderr # nothing else may write to the protocol channel def send(message): channel.write(json.dumps(message) + "\n") channel.flush() first = inbox.get() if first is None: return 0 state = load_state() hasher = Hasher(state["hashes"]) built, errors = prebuild(first["prebuild"], default_build_jobs()) if first["prebuild"] else (True, "") executor = LocalExecutor(MACHINE_JOBS, int(memory_available() * 0.75), first["timeout"]) send({"hello": {"jobs": MACHINE_JOBS, "memory": executor.memory_budget, "built": built, "errors": errors[-4000:]}}) try: return serve(executor, inbox, queues.Empty, send, state, hasher) finally: executor.stop() def serve(executor, inbox, empty, send, state, hasher): """The worker's loop: start what the coordinating runner sends, report what finished, until its stdin ends.""" while True: try: message = inbox.get(timeout=0.2) except empty: message = False if message is None: save_state(state) return 0 if message: # Other runners may share this machine: take a slot only while memory allows. reserve = message["expected"] + (4 << 30) if (executor.running and memory_available() < reserve) or \ not executor.start(message["run"], ROOT / message["script"], message["reuse"], message["expected"], message.get("shard")): send({"rejected": message["run"]}) for name, script, passed, elapsed, trace in executor.poll(): record = outcome(name, script, passed, elapsed, trace, hasher) log = (STATE_DIR / "logs" / f"{name}.log").read_bytes()[-(8 << 20):].decode(errors="replace") send({"done": name, "record": record, "log": log}) save_state(state) def stale(name, state, hasher, environment): record = state["harnesses"].get(name) if record and record.get("status") == "pass" and "key" in record and key(record, hasher, environment) == record["key"]: return False # Passed elsewhere with the same inputs? Try this worktree's and the shared dependency lists. candidates = [record] if record and record.get("dependencies") else [] template = SHARED / "dependencies" / f"{name}.json" if template.exists(): candidates.append(json.loads(template.read_text(encoding="utf-8"))) for candidate in candidates: shared = SHARED / "passes" / name / f"{key(candidate, hasher, environment)}.json" if shared.exists(): state["harnesses"][name] = dict(json.loads(shared.read_text(encoding="utf-8")), shared=True) return False return True def trace_main(output, script, arguments): """Run one harness and record what it read and executed.""" import atexit import resource import runpy read, commands = set(), [] def audit(event, details): if event == "open": path, mode = details[0], details[1] if isinstance(path, (str, bytes, os.PathLike)) and (mode is None or "r" in str(mode) and "+" not in str(mode)): read.add(os.fsdecode(path)) elif event == "subprocess.Popen": arguments_ = details[1] arguments_ = [arguments_] if isinstance(arguments_, (str, bytes, os.PathLike)) else list(arguments_ or []) commands.append([os.fsdecode(a) if isinstance(a, (bytes, os.PathLike)) else str(a) for a in arguments_]) def write(): modules = [getattr(module, "__file__", None) for module in list(sys.modules.values())] peak = max(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss, resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss) peak //= 1024 if sys.platform == "darwin" else 1 # bytes on macOS, KiB elsewhere Path(output).write_text(json.dumps({ "modules": sorted({m for m in modules if m and inside(m, ROOT)}), "read": sorted(path for path in read if os.path.isabs(path) or not path.startswith("<")), "commands": commands, "peak_bytes": peak * 1024, })) atexit.register(write) sys.addaudithook(audit) sys.argv = [script, *arguments] sys.path.insert(0, str(Path(script).parent)) runpy.run_path(script, run_name="__main__") # Files every harness's result depends on (part of environment_key): a change to one affects them all. SHARED_INPUTS = {"research-lock.json", "Cargo.lock"} # Manifest sections that say what a crate is and where its targets are, not what it builds with. MANIFEST_SECTION_KEPT = re.compile(r"\[\[?(?:package|lib|bin|example|test|bench|lints|badges)\b[^\]]*\]\]?$") WORKSPACE_CRATE = r"(horizon[\w-]*|cargo-horizon-mod|xtask)" # A workspace crate named as a dependency (by path, or from the workspace's own table) without choosing its features, # or listed as a member. WORKSPACE_LINE = re.compile(WORKSPACE_CRATE + r'\s*=\s*(?:\{(?![^}]*\bfeatures\b)[^}]*\b(?:path|workspace)\s*=[^}]*\}|true)' r'|' + WORKSPACE_CRATE + r'\.workspace\s*=\s*true|"crates/[^"]+",?') # A workspace crate named as a dependency with some of its features turned on. WORKSPACE_FEATURES = re.compile(WORKSPACE_CRATE + r'\s*=\s*\{(?=[^}]*\b(?:path|workspace)\s*=)(?![^}]*default-features)' r'[^}]*\bfeatures\s*=\s*\[([^\]]*)\][^}]*\}') def git_show(base, path): """path's content at commit base, or None where it didn't exist.""" result = subprocess.run(["git", "show", f"{base}:{path}"], cwd=ROOT, capture_output=True, text=True) return result.stdout if result.returncode == 0 else None def manifest_lines(text): """A Cargo.toml's lines that decide what is built: (section, line) pairs, without its package's identity and targets, comments, or workspace crates (whose code counts through its own sources).""" lines, section = set(), "" name = re.search(r'^\[package\][^\[]*?^name\s*=\s*"([^"]+)"', text or "", re.M | re.S) for raw in (text or "").splitlines(): line = re.sub(r"\s+", " ", raw.split("#", 1)[0]).strip() if not line: continue if line.startswith("["): # A crate's features are its own: the same line in two crates means two things. section = f"[features] of {name.group(1) if name else '?'}" if line == "[features]" else line continue if MANIFEST_SECTION_KEPT.match(section) or WORKSPACE_LINE.fullmatch(line) or \ section == "[workspace]" and (line.startswith("members =") or line.startswith('"') or line == "]"): continue lines.add((section, line)) return lines def lock_packages(text): """Cargo.lock's packages from a registry or git (with their versions and checksums); workspace crates' entries only list what their manifests say.""" return {block.strip() for block in (text or "").split("[[package]]") if "\nsource = " in block} def workspace_only_manifests(changed, base): """The changed Cargo.lock and Cargo.toml files whose changes only add, remove or move workspace crates (a crate split): no external package, version, feature, or build setting changed across them. Their crates' code counts through its own sources, so these files don't make every harness linking the crates rerun.""" manifests = sorted(path for path in changed if path == "Cargo.lock" or path.endswith("Cargo.toml")) if not manifests or base is None: return set() if "Cargo.lock" in manifests and \ lock_packages(git_show(base, "Cargo.lock")) != lock_packages((ROOT / "Cargo.lock").read_text(encoding="utf-8")): return set() tomls = [path for path in manifests if path != "Cargo.lock"] before = set().union(*(manifest_lines(git_show(base, path)) for path in tomls)) if tomls else set() after = set().union(*(manifest_lines((ROOT / path).read_text(encoding="utf-8") if (ROOT / path).exists() else None) for path in tomls)) if tomls else set() # Workspace crates' features that a changed line defines or turns on: they change what compiles of that crate's # sources, which is fine only where every source naming them changed too (a split: the crate's files are new). gated = set() for section, line in before ^ after: wanted = WORKSPACE_FEATURES.fullmatch(line) if wanted: gated |= {(wanted.group(1), feature) for feature in re.findall(r'"([^"]+)"', wanted.group(2))} elif section.startswith("[features] of ") and re.fullmatch(r'[\w-]+ = \[(?: ?"[\w-]+",?)*\s*\]', line): gated.add((section[len("[features] of "):], line.split(" ", 1)[0])) else: return set() # an external package, a feature reaching one ("dep:", "crate/feature"), a build setting return set(manifests) if all(feature_sources_changed(crate, feature, changed) for crate, feature in gated) else set() def feature_sources_changed(crate, feature, changed): """Whether every Rust source of workspace crate `crate` that names `feature`, or a feature it turns on, is in changed (False where the crate isn't found).""" listed = subprocess.run(["git", "ls-files", "*Cargo.toml"], cwd=ROOT, capture_output=True, text=True) manifests = [ROOT / path for path in listed.stdout.split()] if listed.returncode == 0 else list(ROOT.rglob("Cargo.toml")) for manifest in manifests: text = manifest.read_text(encoding="utf-8") if manifest.exists() else "" if re.search(r'^\[package\][^\[]*?^name\s*=\s*"' + re.escape(crate) + '"', text, re.M | re.S): break else: return False table = re.search(r"^\[features\]\n(.*?)(?=^\[|\Z)", text, re.M | re.S) implies = {name: re.findall(r'"([^"]+)"', value) for name, value in re.findall(r'^([\w-]+)\s*=\s*\[([^\]]*)\]', table.group(1) if table else "", re.M)} names, todo = set(), [feature] while todo: name = todo.pop() if name not in names: names.add(name) todo += [implied for implied in implies.get(name, []) if "/" not in implied and ":" not in implied] named = re.compile(r'feature\s*=\s*"(?:' + "|".join(map(re.escape, sorted(names))) + r')"') return all(path.relative_to(ROOT).as_posix() in changed for path in manifest.parent.rglob("*.rs") if named.search(path.read_text(encoding="utf-8", errors="replace"))) def changed_files(ref): """Files changed between the merge base of HEAD and ref and HEAD (committed or not), or None without a merge base.""" def git(*arguments): result = subprocess.run(["git", *arguments], cwd=ROOT, capture_output=True, text=True) return result.stdout.split("\n") if result.returncode == 0 else None base = git("merge-base", "HEAD", ref) if not base or not base[0]: return None return set(filter(None, git("diff", "--name-only", base[0]) or [])) | \ set(filter(None, git("ls-files", "--others", "--exclude-standard") or [])) def changed_harnesses(found, records, ref): """Harnesses new or changed since the merge base of HEAD and ref: their script, or a dependency of theirs that is an oracle (Java under mods/horizon/src/test) or a probe (a crate's example or binary), changed or was added.""" changed = changed_files(ref) if changed is None: print(f"note: no merge base with {ref}; the flake check repeats nothing", flush=True) return set() found_changed = set() for name, script in found.items(): specific = [path for path in records.get(name, {}).get("dependencies", []) if path.startswith("mods/horizon/src/test/") or "/examples/" in path or "/src/bin/" in path] if Path(script).relative_to(ROOT).as_posix() in changed or any(path in changed for path in specific): found_changed.add(name) return found_changed def affected_harnesses(found, records, changed, reach=True, base=None): """Harnesses the changed files can affect: their script or a recorded dependency (here, or in the shared dependency lists) changed, a file every harness depends on changed, or nothing is recorded about them yet. With reach, a harness's workspace Rust sources count only if its probes can reach them (tools/parity_reach.py); a harness whose probes can't be placed keeps them all. A changed file counts unless, compared with base (a commit), only declarations of modules the harness doesn't reach changed (tools/parity_reach.py).""" if changed & SHARED_INPUTS: return set(found) graph, stop = None, frozenset() # Parses are keyed by content, so every worktree sharing the vanilla data shares them. cache_path = (SHARED if SHARED.is_dir() else STATE_DIR) / "reach-cache.json" if reach and any(path.startswith("crates/") for path in changed): try: cache = json.loads(cache_path.read_text(encoding="utf-8")) except (OSError, ValueError): cache = {} graph = parity_reach.SourceGraph(ROOT, rust_code, cache) # A changed file that differs in more than its `mod` declarations counts as soon as a harness reaches it, # so the search for that harness can stop there. stop = frozenset(path for path in changed if parity_reach.is_workspace_rust(path) and parity_reach.declarations_changed(ROOT, path, (), base, True)) budget, started, over = float(os.environ.get("PARITY_REACH_BUDGET") or 180), time.monotonic(), False affected = set() for name, script in found.items(): record = records.get(name) or {} dependencies = record.get("dependencies") template = SHARED / "dependencies" / f"{name}.json" if dependencies is None and template.exists(): record = json.loads(template.read_text(encoding="utf-8")) dependencies = record.get("dependencies") if dependencies is None or Path(script).relative_to(ROOT).as_posix() in changed: affected.add(name) continue plain = {path.split(KEEP)[0] for path in dependencies} hit = (plain & changed) | parity_reach.unrecorded(ROOT, plain, changed) if not hit: continue if graph is None or any(not path.startswith("crates/") for path in hit): affected.add(name) # no narrowing, or a file narrowing always keeps (tools/, Java oracles) continue if not over and time.monotonic() - started > budget: over = True print(f"note: choosing harnesses took over {budget:.0f} s (PARITY_REACH_BUDGET): the rest count every " "source their probes link", flush=True) if over: affected.add(name) continue narrowed = parity_reach.narrow(dependencies, recorded_artifacts(record), graph, changed, stop) if any(path.split(KEEP)[0] in changed and (children is None or parity_reach.declarations_changed( ROOT, path.split(KEEP)[0], children, base, whole)) for path, (whole, children) in narrowed.items()): affected.add(name) if graph is not None: saved = graph.saved_cache() if len(cache) + len(saved) < 4 * max(len(saved), 1): saved = {**cache, **saved} # other worktrees' versions of the files too, within bounds cache_path.parent.mkdir(parents=True, exist_ok=True) temporary = cache_path.with_suffix(f".{os.getpid()}.tmp") temporary.write_text(json.dumps(saved), encoding="utf-8") temporary.replace(cache_path) return affected def platform_skips(config, host=None): """Harnesses tools/parity_hosts.json marks as giving different results on this platform, and those this host's entry excludes (playhorizon's CI runner runs its own pull request jobs here, not only workers sent to it): {name: reason}.""" skips = {} if sys.platform == "linux" else dict(config.get("linux_only", {})) skips.update(config.get("hosts", {}).get(HOST if host is None else host, {}).get("exclude", {})) return skips def known_flaky(config): """Harnesses tools/parity_hosts.json lists as failing now and then for a cause not fixed yet: one that fails runs once more, and passing then counts, reported as RETRIED. {name: reason}""" return dict(config.get("known_flaky", {})) def solo_harnesses(config): """Harnesses tools/parity_hosts.json's "solo" lists ({name: reason}: those driving game windows, whose frames are drawn on the CPU on Linux runners) run alone on a machine with SOLO_CPUS CPUs or fewer for this process (playhorizon's runner has 4): beside other harnesses there they lose ticks between control requests. The set, or an empty one on a bigger machine. PARITY_SOLO=1 or 0 forces it on or off.""" forced = os.environ.get("PARITY_SOLO") cpus = len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else os.cpu_count() or 1 if forced == "0" or forced != "1" and cpus > SOLO_CPUS: return set() return set(config.get("solo", {})) SOLO_CPUS = 4 def named_only(config): """Harnesses tools/parity_hosts.json keeps out of every automatic selection (--changed, --failed, the default, --flake-check): they run only when named or with --all, the nightly audit. {name: reason}""" return dict(config.get("named_only", {})) def report_retries(results, flaky): """Say loudly which harnesses needed their one retry (a known flaky one, or one that failed with reused oracle results), here and in the CI job's summary: a pass on the retry counts, so this is where flakes stay visible.""" def why(name, record): if record["retried"] == "known flaky": return (f"known flaky: {flaky.get(base_name(name), '?')}; first run's log: " f"{STATE_DIR / 'logs' / (name + '.first-run.log')}") return "its first run reused oracle results; it no longer reuses them" if record["status"] == "pass" else \ "its first run reused oracle results" lines = [f"RETRIED {name}: failed, then {'passed' if record['status'] == 'pass' else 'failed again'} on its one " f"retry ({why(name, record)})" for name, record in sorted(results.items()) if record.get("retried")] for line in lines: print(line, flush=True) if lines and os.environ.get("GITHUB_STEP_SUMMARY"): with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary: summary.write("### Harnesses that failed once, then ran again\n" + "".join(f"- {line}\n" for line in lines)) def finished_run(failed): """Record that this run completed, for tools/gate.py: a crashed runner never writes it.""" STATE_DIR.mkdir(parents=True, exist_ok=True) (STATE_DIR / "last-run.json").write_text(json.dumps({"run": os.environ.get("PARITY_RUN_ID"), "failed": failed})) def main(): if len(sys.argv) > 1 and sys.argv[1] == "--trace": return trace_main(sys.argv[2], sys.argv[3], sys.argv[4:]) exit_on_termination() if len(sys.argv) > 1 and sys.argv[1] == "--worker": return worker_main(sys.argv[2]) parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("names", nargs="*", help="harness names, e.g. food or block_update") parser.add_argument("--all", action="store_true", help="run every harness from scratch, ignoring earlier passes") parser.add_argument("--fresh", action="store_true", help="run the vanilla oracles even if their inputs are unchanged") parser.add_argument("--failed", action="store_true", help="rerun the harnesses that failed last time") parser.add_argument("--list", action="store_true", help="print what would run, then stop") parser.add_argument("--status", action="store_true", help="print each harness's last result") parser.add_argument("-j", "--jobs", type=int, help="harnesses at once on this machine (default: PARITY_LOCAL_JOBS, else 3 with other hosts or a " "third of its threads, within $FLEET_LOAD_JOBS); runners here share PARITY_MACHINE_JOBS slots") parser.add_argument("--hosts", default=os.environ.get("PARITY_HOSTS"), help="comma-separated machines from tools/parity_hosts.json to run harnesses on as well " "(default: PARITY_HOSTS, else the file's default; '' for this machine only)") parser.add_argument("--timeout", type=int, default=3600, help="seconds per harness") parser.add_argument("--repeat", type=int, default=1, help="run each selected harness this many times; its runs " "must agree (oracle results are reused after the first unless --fresh)") parser.add_argument("--flake-check", metavar="REF", help="harnesses new or changed since the merge base with REF " "must pass --flake-rounds times in a row (tools/gate.py)") parser.add_argument("--flake-rounds", type=int, default=3, help="runs required by --flake-check (default 3)") parser.add_argument("--changed", metavar="REF", help="only harnesses the changes since the merge base with REF can " "affect (and that have not passed with their current inputs): the pull-request check") parser.add_argument("--by-crate", action="store_true", help="with --changed: count every Rust source a probe " "links, not only those it can reach (tools/parity_reach.py)") parser.add_argument("--changed-files", metavar="FILE", help="like --changed, for the changed paths listed in FILE, " "one per line: for a copy without Git history (tools/pr_ready.py on another machine)") args = parser.parse_args() found = harnesses() state = load_state() hasher = Hasher(state["hashes"]) environment = environment_key(hasher) records = state["harnesses"] # A sharded harness's name stands for all its shards. args.names = [entry for name in args.names for entry in ([name] if name in found else [shard for shard in found if base_name(shard) == name] or [name])] unknown = [name for name in args.names if name not in found] if unknown: parser.error("unknown harnesses: " + ", ".join(unknown)) if args.status: for name in found: record = records.get(name) status = "never run" if not record else record["status"] + (" (stale)" if stale(name, state, hasher, environment) and record["status"] == "pass" else "") seconds = f"{record['seconds']:>7.0f}s" if record else "" print(f"{name:<40} {status:<15} {seconds}") save_state(state) return 0 if args.names: chosen = args.names elif args.all: chosen = list(found) elif args.failed: chosen = [name for name in found if records.get(name, {}).get("status") == "fail"] elif args.changed or args.changed_files: if args.changed_files: changed, since = set(Path(args.changed_files).read_text(encoding="utf-8").split("\n")) - {""}, "" else: changed, since = changed_files(args.changed), f" since the merge base with {args.changed}" if changed is None: parser.error(f"no merge base with {args.changed}") base = None if args.changed: merge_base = subprocess.run(["git", "merge-base", "HEAD", args.changed], cwd=ROOT, capture_output=True, text=True) base = merge_base.stdout.strip() or None moved = workspace_only_manifests(changed, base) if not args.by_crate else set() if moved: print(f"{', '.join(sorted(moved))}: only workspace crates added, removed or moved; their sources count " "instead", flush=True) affected = affected_harnesses(found, records, changed - moved, reach=not args.by_crate, base=base) chosen = [name for name in found if name in affected and stale(name, state, hasher, environment)] print(f"{len(changed)} files changed{since}: {len(affected)} harnesses " f"depend on them, {len(chosen)} without a pass for their current inputs.", flush=True) else: chosen = [name for name in found if stale(name, state, hasher, environment)] skips = platform_skips(host_config()) for name in [name for name in chosen if base_name(name) in skips and not args.names]: print(f"skipped on {HOST} ({sys.platform}): {name}: {skips[base_name(name)]}", flush=True) chosen.remove(name) manual = {} if args.names or args.all else named_only(host_config()) for name in [name for name in chosen if base_name(name) in manual]: print(f"not selected (runs when named, and nightly): {name}: {manual[base_name(name)]}", flush=True) chosen.remove(name) rounds = {name: args.repeat for name in chosen} if args.repeat > 1 else {} if args.flake_check: changed = {name for name in changed_harnesses(found, records, args.flake_check) if base_name(name) not in manual} for name in sorted(changed): # A pass counts for a changed harness only if it held over enough runs. if name not in chosen and records.get(name, {}).get("rounds", 1) < args.flake_rounds: chosen.append(name) if name in chosen: rounds[name] = max(rounds.get(name, 1), args.flake_rounds) if rounds: print(f"Flake check: {', '.join(sorted(rounds))} must pass {args.flake_rounds} times.", flush=True) selected = {name: found[name] for name in chosen} if args.list or not selected: for name in selected: print(name) print(f"{len(selected)} of {len(found)} harnesses would run" if args.list else "Nothing to run: every harness passed with its current inputs.") save_state(state) if not args.list: finished_run([]) return 0 # The probes the selected harnesses built last time, here or (for those that never ran here) in another # worktree. A harness with no record builds its own: building every probe for it would be ~275 binaries. artifacts = [] for name in selected: record = records.get(name) if not record or "artifacts" not in record: template = SHARED / "dependencies" / f"{name}.json" record = json.loads(template.read_text(encoding="utf-8")) if template.exists() else {} for artifact in recorded_artifacts(record): if artifact not in artifacts: artifacts.append(artifact) config = host_config() hosts = [host for host in (config.get("default", []) if args.hosts is None else args.hosts.split(",")) if host and host != HOST] unknown = [host for host in hosts if host not in config["hosts"]] if unknown: parser.error("unknown hosts (see tools/parity_hosts.json): " + ", ".join(unknown)) jobs = default_local_jobs(remote=bool(hosts)) if args.jobs is None else max(0, args.jobs) def elsewhere(name): return base_name(name) not in config.get("local_only", {}) and \ any(base_name(name) not in config["hosts"][host].get("exclude", {}) for host in hosts) if jobs == 0 and not all(elsewhere(name) for name in selected): jobs = 1 # harnesses that can only run here still need a slot remotes = [RemoteExecutor(host, config["hosts"][host], artifacts, config.get("local_only", {}), args.timeout) for host in hosts] local = None try: if jobs: print("Building probes...", flush=True) started = time.monotonic() built, errors = prebuild(artifacts, default_build_jobs()) if not built: print(errors[-4000:]) print("Probe build failed; harnesses will report their own build errors.", flush=True) print(f"Probes built in {time.monotonic() - started:.0f}s.", flush=True) where = "".join(f", plus {host}" for host in hosts) print(f"Running {len(selected)} harnesses, {jobs} at a time here{where}...", flush=True) reuse_oracles = not (args.fresh or args.all) local = LocalExecutor(jobs, int(memory_available() * 0.75), args.timeout, REMOTE_LOCAL_PEAK if remotes and args.jobs is None else None) local.solo = solo_harnesses(config) if local.solo & {harness_of(name) for name in selected}: print(f"Alone on this machine ({SOLO_CPUS} CPUs or fewer), after the others: " f"{', '.join(sorted(local.solo & {harness_of(name) for name in selected}))}", flush=True) results, elapsed = run(selected, state, hasher, environment_key(hasher), [*remotes, local], reuse_oracles, rounds, known_flaky(config)) finally: for executor in [*remotes, *([local] if local else [])]: executor.stop() save_state(state) failed = sorted(name for name, record in results.items() if record["status"] != "pass") finished_run(failed) print(f"\n{len(results) - len(failed)} passed, {len(failed)} failed in {elapsed / 60:.1f} min.") report_retries(results, known_flaky(config)) for index, name in enumerate(failed): log = STATE_DIR / 'logs' / (name + '.log') print(f" {name}: {log}") # The end of a harness's output says why it failed, and CI shows only this output. if index < FAILURE_TAILS: try: lines = log.read_text(encoding='utf-8', errors='replace').splitlines()[-FAILURE_TAIL_LINES:] except OSError: lines = [] for line in lines: print(f" {line[:300]}") return 1 if failed else 0 if __name__ == "__main__": sys.exit(main())