Files
horizon-airdef/tools/test_collision_parity.py
Ludovic Marie 5034e4557d parity: clear rust-<name>-parity outputs after a pass, reuse oracles on CI
Disk: clear_large_outputs only looked at .local directories named after the
harness, so the 174 harnesses writing to rust-<name>-parity kept every dump
(86 GB in one runner checkout, 13 GB for state_provider alone). It now
clears those too, keeps vanilla outputs only up to 256 MB (larger ones cost
more disk than rerunning their oracle), and handles shard names.
collision, world and geometry dropped one mkdtemp run directory per run
(11 GB): each run now removes the earlier ones.

Time: harnesses inherited the runner's stdin, a pipe on GitHub's runner, and
parity_shim never reuses an oracle run whose stdin is a pipe, so oracle reuse
never worked on CI. Harnesses now get /dev/null.

CI: parity.yml falls back to the head's own selection when the names main's
`--list` printed don't match its summary count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 00:44:22 +01:00

88 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Validate registry-backed cached collisions against the pinned vanilla binaries."""
import argparse
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import tempfile
try:
from . import lab, test_rust_parity
except ImportError:
import lab
import test_rust_parity
def counts(output):
marker = "HORIZON_COLLISION_RESULT "
found = [json.loads(line.split(marker, 1)[1]) for line in output.splitlines() if marker in line]
if len(found) != 1:
raise AssertionError(f"Missing collision counts: {found}")
result = found[0]
if result["registry_states"] != 35723 or result["cached_states"] + result["uncached_states"] != result["registry_states"] or result["queries"] != result["cached_states"] * 6 or result["unique_shapes"] <= 0:
raise AssertionError(f"Incomplete collision coverage: {result}")
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--prepare", action="store_true")
args = parser.parse_args()
directory = lab.LOCAL / "rust-collision-parity"
directory.mkdir(parents=True, exist_ok=True)
report_path = directory / "report.json"
report_path.unlink(missing_ok=True)
if args.prepare:
lab.prepare()
lock, jars, libraries = test_rust_parity.verify_artifacts()
for earlier in directory.glob("run-*"): # one run per run used to stay behind: 5 GB here after a few weeks
shutil.rmtree(earlier, ignore_errors=True)
base = Path(tempfile.mkdtemp(prefix="run-", dir=directory))
oracle = lab.ROOT / "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCollisionOracle.java"
sources = [oracle, Path(__file__).resolve(), lab.ROOT / "tools/test_rust_parity.py", lab.ROOT / "Cargo.toml", lab.ROOT / "Cargo.lock", lab.ROOT / "crates/horizon-core/Cargo.toml", lab.ROOT / "crates/horizon-world/Cargo.toml", *sorted((lab.ROOT / "crates/horizon-core/src").rglob("*.rs")), *sorted((lab.ROOT / "crates/horizon-world/src").rglob("*.rs")), lab.ROOT / "crates/horizon-world/examples/probe/collision_fixture.rs"]
tested_sources = test_rust_parity.source_hashes(sources)
classes = base / "classes"
classes.mkdir()
dependency_path = os.pathsep.join(map(str, libraries))
test_rust_parity.run(["javac", "--release", "25", "-proc:none", "-cp", os.pathsep.join([str(jars["server"]), dependency_path]), "-d", str(classes), str(oracle)])
def java(side, mode):
result = subprocess.run(["java", "-Xmx1G", "-cp", os.pathsep.join([str(classes), str(jars[side]), dependency_path]), "dev.horizonlab.parity.VanillaCollisionOracle", mode, str(base)], cwd=lab.ROOT, capture_output=True, text=True, timeout=300)
(base / f"{side}-{mode}.log").write_text(result.stdout + result.stderr, encoding="utf-8")
result.check_returncode()
return result.stdout
expected = counts(java("server", "generate"))
test_rust_parity.run(["cargo", "build", "--release", "-p", "horizon-world", "--example", "world_probe"])
metadata = json.loads(subprocess.check_output(["cargo", "metadata", "--no-deps", "--format-version", "1"], cwd=lab.ROOT, text=True))
binary = Path(metadata["target_directory"]) / "release/examples" / ("world_probe.exe" if os.name=="nt" else "world_probe")
actual = counts(subprocess.check_output([str(binary), "collision_fixture", str(base)], cwd=lab.ROOT, text=True))
if actual != expected:
raise AssertionError(f"Collision coverage differs: vanilla={expected}, Rust={actual}")
canonical = {"empty_states":0,"block_states":0,"other_cached_states":0}
for line in (base / "rust.txt").read_text().splitlines():
if line == 'canonical true false': canonical['empty_states'] += 1
elif line == 'canonical false true': canonical['block_states'] += 1
elif line == 'canonical false false': canonical['other_cached_states'] += 1
if sum(canonical.values()) != expected['cached_states']: raise AssertionError('incomplete canonical identity comparison')
references = []
for side, jar in jars.items():
java(side, side)
reference = base / f"vanilla-{side}.txt"
matched = test_rust_parity.compare(reference, base / "rust.txt")
references.append({"side": side, "jar_sha256": lab.digest(jar, "sha256"), "output_sha256": lab.digest(reference, "sha256"), "matched_output_lines": matched, **expected})
print(f"{side}: {matched:,} cached collision output lines match", flush=True)
test_rust_parity.ensure_sources_unchanged(sources, tested_sources)
report = {"schema": 1, "minecraft": lock["minecraft"]["version"], "manifest_sha1": lock["minecraft"]["manifest_sha1"], "platform": platform.platform(),
"java": subprocess.check_output(["java", "--version"], text=True).splitlines()[0], "rustc": subprocess.check_output(["rustc", "--version"], text=True).strip(),
"inputs_sha256": lab.digest(base / "inputs.txt", "sha256"), "rust_output_sha256": lab.digest(base / "rust.txt", "sha256"), "sources_sha256": tested_sources, "references": references, "canonical_identities":canonical,
"scope": "Pinned no-context cached collision branch only. Registry identity validation, resident section lookup, grids, native canonical empty/block identities distinct from equal geometry, discrete extrema including empty shapes, greedy boxes, bounds, translated collide, ray hits, closest points and moved cell lookup. Uncached and entity-context behavior have separate comparison suites."}
lab.write_json(report_path, report)
lab.write_json(base / "report.json", report)
print(f"Cached collision comparison passed: {report_path}")
if __name__ == "__main__":
main()