Files
SashegDev 874b6f20c2 Клиент: начальный импорт кодовой базы
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
2026-10-29 22:04:28 +03:00

1695 lines
87 KiB
Python

#!/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"(?<!\\) ", rest.strip()) if part]
return sources
def artifact_path(kind, name, profile):
directory = "debug" if profile == "dev" else profile
return TARGET / directory / ("examples" if kind == "example" else "") / name
SELECTION_FLAGS = {"-p", "--package", "-F", "--features"} # followed by a value
SELECTION_SWITCHES = {"--workspace", "--all", "--all-features", "--no-default-features"}
def artifacts_of(arguments):
"""The [kind, name, profile, selection, examples] targets a cargo build/run command line names.
kind is example, bin, or examples/bins (every one, name ""). selection is the command's package and feature
flags, and examples whether the command builds any example (which turns on dev-dependencies): together they
decide how Cargo unifies features, so the probe prebuild must repeat them exactly. A `--workspace` build of
the same example gives its dependencies other features, and the harness's own `-p` build then compiles
everything again."""
arguments = arguments[:arguments.index("--")] if "--" in arguments else arguments
profile = "release" if "--release" in arguments else "dev"
selection = []
found = []
index = 1
while index < len(arguments):
argument = arguments[index]
flag, _, value = argument.partition("=")
if flag in SELECTION_FLAGS | {"--profile", "--example", "--bin"} and not value and index + 1 < len(arguments):
value = arguments[index + 1]
index += 1
if flag == "--profile":
profile = value
elif flag in SELECTION_FLAGS:
selection += ["-p" if flag == "--package" else "--features" if flag == "-F" else flag, value]
elif flag in SELECTION_SWITCHES:
selection.append("--workspace" if flag == "--all" else flag)
elif flag in ("--example", "--bin"):
found.append([flag[2:], value])
elif flag in ("--examples", "--bins"):
found.append([flag[2:], ""])
index += 1
examples = any(kind in ("example", "examples") for kind, _ in found)
return [[kind, name, profile, selection, examples] for kind, name in found]
RUST_PATH = re.compile(r"(?<![\w:])((?:crate|self|super)(?:::super)*|[a-z_][a-z0-9_]*)::((?:[A-Za-z_]\w*::)*[A-Za-z_]\w*)")
RUST_TYPE = re.compile(r"\b(?:struct|enum|trait|union|type)\s+([A-Za-z_]\w*)")
RUST_MACRO_USE = re.compile(r"\b([A-Za-z_]\w*)!\s*[(\[{]")
RUST_MACRO_DEFINITION = re.compile(r"macro_rules!\s+([A-Za-z_]\w*)")
RUST_LITERAL_START = re.compile(r"//|/\*|(?<![\w])b?r#*\"|\"|'")
RUST_CODE_CACHE = {}
def rust_code(text):
"""Rust source with comments, string and character literals blanked out (newlines kept)."""
cached = RUST_CODE_CACHE.get(text)
if cached is not None:
return cached
out, i, n = [], 0, len(text)
def blank(chunk):
return re.sub(r"[^\n]", " ", chunk)
while i < n:
match = RUST_LITERAL_START.search(text, i)
if not match:
out.append(text[i:])
break
out.append(text[i:match.start()])
i, token = match.start(), match.group()
if token == "//":
end = text.find("\n", i)
end = n if end < 0 else end
elif token == "/*":
depth, end = 1, i + 2
while end < n and depth:
nested = re.compile(r"/\*|\*/").search(text, end)
if not nested:
end = n
break
depth += 1 if nested.group() == "/*" else -1
end = nested.end()
elif token.endswith('"') and "r" in token:
closing = '"' + "#" * token.count("#")
end = text.find(closing, match.end())
end = n if end < 0 else end + len(closing)
elif token == '"':
literal = re.compile(r'"(?:\\.|[^"\\])*"', re.DOTALL).match(text, i)
end = literal.end() if literal else n
else: # ' starts a character literal or a lifetime
literal = re.compile(r"'(?:\\(?:u\{[0-9a-fA-F]*\}|x..|.)|[^\\'\n])'").match(text, i)
if not literal:
out.append("'")
i += 1
continue
end = literal.end()
out.append(blank(text[i:end]))
i = end
result = "".join(out)
RUST_CODE_CACHE[text] = result
return result
def inline_modules(code):
"""For blanked Rust code: a function giving the inline modules (`mod x { }`) open at an offset."""
import bisect
marks, stack, depth = [0], [], 0
states = [()]
opening = {match.end() - 1: match.group(1) for match in re.finditer(r"\bmod\s+(\w+)\s*\{", code)}
for index, c in enumerate(code):
if c == "{":
depth += 1
if index in opening:
stack.append((opening[index], depth))
marks.append(index); states.append(tuple(name for name, _ in stack))
elif c == "}":
if stack and stack[-1][1] == depth:
stack.pop()
marks.append(index); states.append(tuple(name for name, _ in stack))
depth -= 1
return lambda offset: list(states[bisect.bisect_right(marks, offset) - 1])
def impl_targets(header, text, workspace):
"""Names an impl header (after `impl`) may implement for or with that could be the scope's own types.
Only the trait and the self type count, by their last path segment; a path
starting with an external crate (std::error::Error) can't name a scope type,
nor can a bare name the file imports from an external crate or defines itself."""
header = re.split(r"\bwhere\b", header)[0].strip().replace("->", " ")
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/<crate>/examples/probe) and its target name. Until 2026-10-24 they were all
# `probe`, and Cargo copies every crate's examples into one <profile>/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 <name> ...`, or
`cargo run --example world_probe -- <name> ...`."""
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::<probe>) and the shared support modules they name (support::<module>), 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 (`<name>...` or `rust-<name>-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
<root>/remote/<this host><this path> there; its .local links to the vanilla
data in <root>/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())