Files
horizon-airdef/tools/test_generation_tick_parity.py
Ludovic Marie 9edcbc2559 Give parity harness javac runs the oracle source path
Oracles reference shared helper oracles (heightmap, level shape...) that
javac could not find when only the entry files were listed. Also let
chunkgen_bench list unsupported features (HORIZON_LIST_UNSUPPORTED=1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:33:12 +02:00

125 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""Compare proto-chunk ticks and world-generation tick routing with vanilla."""
import argparse
from collections import Counter
import json
import os
from pathlib import Path
import platform
import random
import subprocess
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab, test_rust_parity as base
def corpus():
extremes = [-9223372036854775808, -2147483648, -1, 0, 1, 2147483647, 9223372036854775807]
for time in extremes:
yield 'reset'
yield 'load -1 -1 4 0 -1 64 -1 -2147483648 -100 0 -1 64 -1 2147483647 100 1 -1 64 -1 -1 0 2 -2 65 -2 12 3'
yield 'query 0 -1 64 -1'
yield 'snapshot -1 -1'
yield f'schedule 0 -1 64 -1 {time} -3 {time}'
yield f'schedule 3 -3 66 -3 {time} 2 {time}'
yield f'dump -1 -1 {time}'
yield 'snapshot_dump'
yield f'promote -1 -1 {time}'
yield f'dump -1 -1 {time}'
yield f'schedule 4 -4 67 -4 {time} -2 {time}'
yield f'dump -1 -1 {time}'
yield 'snapshot_dump'
for kind in ['load', 'level']:
yield 'reset'
# Deliberately load a position outside the owning container; routing is
# determined by each request's position, not by entries in saved lists.
yield f'{kind} 0 0 2 0 16 64 0 7 0 0 16 64 0 8 -3'
yield 'query 0 16 64 0'
yield 'dump 0 0 0'
yield 'dump 1 0 0'
yield 'fail 1'
yield 'query 0 0 64 0'
yield 'schedule 1 0 64 0 0 0 0'
yield 'will 1 0 64 0'
yield 'count'
yield 'fail 0'
yield 'query 1 0 64 0'
yield 'unpack 0 0 9223372036854775807'
yield 'dump 0 0 -9223372036854775808'
positions = [[-2147483648, -2147483648, 2147483647], [2147483647, 2147483647, -2147483648],
[0, 0, 0], [67108864, 4096, -67108864], [-1, -1, -1], [15, 319, 15], [16, -64, 16]]
for priority in [-2147483648, -4, -3, -2, -1, 0, 1, 2, 3, 4, 2147483647]:
yield 'reset'
for i, p in enumerate(positions):
xyz = ' '.join(map(str, p))
yield f'schedule {i} {xyz} -9223372036854775808 {priority} 9223372036854775807'
yield f'schedule {i} {xyz} 9223372036854775807 {-priority if priority != -2147483648 else 0} -9223372036854775808'
yield f'query {i} {xyz}'
yield f'will {i} {xyz}'
yield f'dump {p[0] >> 4} {p[2] >> 4} 12345'
rng = random.Random(0x484f52495a4f4e)
for trial in range(12):
yield 'reset'
for cx in range(-2, 3):
command = 'load' if (cx + trial) % 2 == 0 else 'level'
rows = []
for i in range(24):
row = [rng.randrange(8), cx*16+rng.randrange(4), rng.randrange(-3, 4), rng.randrange(4), rng.choice([-2147483648, -1, 0, 1, 2147483647]), rng.randrange(-8, 9)]
rows.append(' '.join(map(str, row)))
if i % 3 == 0: rows.append(rows[-1])
yield f'{command} {cx} 0 {len(rows)} ' + ' '.join(rows)
for step in range(700):
cx = rng.randrange(-2, 3); p = [cx*16+rng.randrange(4), rng.randrange(-3, 4), rng.randrange(4)]
xyz = ' '.join(map(str, p)); typ = rng.randrange(8)
if step % 71 == 0:
yield 'fail 1'; yield f'query {typ} {xyz}'; yield f'will {typ} {xyz}'; yield 'count'; yield 'fail 0'
if step % 79 == 0: yield f'promote {cx} 0 {rng.choice(extremes)}'
if step % 83 == 0: yield f'snapshot {cx} 0'
if step % 89 == 0: yield 'snapshot_dump'
if step % 101 == 0: yield f'remove {cx} 0'
yield f'schedule {typ} {xyz} {rng.choice(extremes)} {rng.randrange(-8, 9)} {rng.choice(extremes)}'
yield f'query {typ} {xyz}'
if step % 23 == 0: yield f'dump {cx} 0 {rng.choice(extremes)}'
for cx in range(-2, 3): yield f'dump {cx} 0 {rng.choice(extremes)}'
yield 'snapshot_dump'
def main():
parser = argparse.ArgumentParser(description=__doc__); parser.add_argument('--prepare', action='store_true'); args = parser.parse_args()
build = lab.LOCAL / 'rust-generation-tick-parity'; build.mkdir(parents=True, exist_ok=True)
report_path = build / 'report.json'; report_path.unlink(missing_ok=True)
if args.prepare: lab.prepare()
lock, jars, libraries = base.verify_artifacts()
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGenerationTickOracle.java'
sources = [oracle, Path(__file__), lab.ROOT / 'tools/test_rust_parity.py', lab.ROOT / 'tools/lab.py', lab.ROOT / 'Cargo.toml', lab.ROOT / 'Cargo.lock', lab.ROOT / 'crates/horizon-core/Cargo.toml',
*sorted((lab.ROOT / 'crates/horizon-core/src').rglob('*.rs')), lab.ROOT / 'crates/horizon-core/examples/generation_tick_fixture.rs']
tested_sources = base.source_hashes(sources)
commands = list(corpus()); inputs = build / 'inputs.txt'; inputs.write_text('\n'.join(commands) + '\n')
tested_fixtures = base.source_hashes([inputs]); classes = build / 'classes'; classes.mkdir(exist_ok=True)
dependencies = os.pathsep.join(map(str, libraries))
def run(command, name, **kwargs):
with (build / (name + '.log')).open('w') as log:
subprocess.run(list(map(str, command)), cwd=lab.ROOT, check=True, timeout=3600, stderr=log, **({'stdout': log} | kwargs))
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none','-sourcepath',str(lab.ROOT/'mods/horizon/src/test/java'), '-cp', str(jars['server']) + os.pathsep + dependencies, '-d', classes, oracle], 'compile-java')
run(['cargo', 'build', '--release', '-p', 'horizon-core', '--example', 'generation_tick_fixture'], 'compile-rust')
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' / ('generation_tick_fixture.exe' if os.name == 'nt' else 'generation_tick_fixture')
with inputs.open('rb') as source, (build / 'rust.txt').open('wb') as output: run([binary], 'rust', stdin=source, stdout=output)
references = []
for side, jar in jars.items():
run(['java', '-Xmx512M', '-cp', os.pathsep.join([str(classes), str(jar), dependencies]), 'dev.horizonlab.parity.VanillaGenerationTickOracle', inputs, build / (side + '.txt')], side)
matched = base.compare(build / (side + '.txt'), build / 'rust.txt')
references.append({'side': side, 'jar_sha256': lab.digest(jar, 'sha256'), 'output_sha256': lab.digest(build / (side + '.txt'), 'sha256'), 'matched_output_lines': matched})
print(f'{side}: {matched:,} generation-tick lines match Rust', flush=True)
base.ensure_sources_unchanged(sources, tested_sources); base.ensure_sources_unchanged([inputs], tested_fixtures)
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(),
'commands': dict(Counter(command.split()[0] for command in commands)), 'sources_sha256': tested_sources, 'fixtures_sha256': tested_fixtures,
'rust_output_sha256': lab.digest(build / 'rust.txt', 'sha256'), 'references': references,
'scope': 'Native ProtoChunkTicks load/schedule deduplication, insertion order, zero delay for scheduled requests, preserved saved delays/priorities, time-independent packing and independent scheduledTicks snapshots. Native WorldGenTickAccess lookup ordering, routing to mixed proto/level containers, lookup failures, false willTickThisTick and zero count without lookups. Proto-to-level promotion and unpacked save behavior, position/type identities, priorities, duplicates, unloaded containers and integer/time extremes are compared. Rust exposes pack as a borrowed immutable slice; mutation of the native returned list is outside the API. Actual block/fluid execution and feature-world scheduling integration remain pending.'}
lab.write_json(report_path, report); print(f'Scoped generation-tick parity passed: {report_path}')
if __name__ == '__main__': main()