Files
horizon-airdef/tools/test_biome_temperature_parity.py
T
Ludovic Marie 0b581f9ffe Give each crate's probe example its own target name
horizon-client, horizon-world and horizon-protocol each had an example
named `probe`. Cargo copies every crate's examples into one
<profile>/examples/ directory, so parallel parity harnesses sharing a
target dir could run another crate's probe (CI parity run 40055656303:
command_parse ran the horizon-world dispatcher). The examples are now
client_probe, world_probe and protocol_probe ([[example]] in each
Cargo.toml; the files stay where they are), and every harness builds
and runs its own crate's name.

tools/parity.py recognises the new names when it counts the probes a
harness ran, and renames the old `probe` in recorded artifacts to the
crate's target (from the record's -p, else its probe sources), so the
prebuild still builds them instead of dropping them as unknown.

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

106 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Compare biome temperature sampling, precipitation and cache behavior with pinned vanilla."""
import argparse
import json
import os
from pathlib import Path
import platform
import random
import subprocess
import zipfile
try:
from . import lab,test_rust_parity as base
except ImportError:
import lab
import test_rust_parity as base
def corpus():
rng=random.Random(0x484f52495a4f4e54454d50)
bases=[0,0x80000000,1,0x80000001,0x3dcccccc,0x3dcccccd,0x3dccccce,0x3e199999,0x3e19999a,0x3e19999b,0x3e4ccccd,0x3f000000,0x3f800000,0xbf800000,0x7f7fffff,0xff7fffff,0x7f800000,0xff800000,0x7fc00000,0xffc00001]
bases += [rng.getrandbits(32) for _ in range(28)]
edges=[-2**31,-33554433,-33554432,-16777217,-16777216,-4097,-4096,-64,-1,0,1,63,64,79,80,81,4095,4096,16777216,16777217,33554431,33554432,2**31-1]
wrap=lambda value:(value+2**31)%2**32-2**31
points=[[0,y,0,63] for y in [-64,0,63,79,80,81,82,319,4096]]
points += [[rng.choice(edges),rng.choice(edges),rng.choice(edges),rng.choice(edges)] for _ in range(96)]
points += [[rng.randrange(-30000000,30000001),rng.randrange(-64,321),rng.randrange(-30000000,30000001),63] for _ in range(96)]
# Reuse packed keys while varying the source coordinates and sea level.
for x,y,z,sea in list(points[:32]):
points += [[x,y,z,wrap(sea+500)],[wrap(x+2**26),wrap(y+4096),wrap(z-2**26),sea]]
for base in bases:
for modifier in ['NONE','FROZEN']:
for precipitation in [False,True]:
ops=[['query',*p] for p in points]
ops += [['fill',1100,-10000,63],['dump'],['query',-8901,1099&255,1099*31,-64],['query',-10000,0,0,63],['dump']]
yield {'base':f'{base:x}','modifier':modifier,'precipitation':precipitation,'ops':ops}
for base in [0x3e4ccccd,0x7fc00000,0xffc00001]:
for modifier in ['NONE','FROZEN']:
for first in [0,1023]:
ops=[['fill',1024,0,63],['dump'],['query',first,first&255,first*31,-64],['dump'],['fill',1,5000,63],['dump']]
child=[['dump'],['query',first,first&255,first*31,-1000],['query',0,4096,0,0],['dump']]
ops += [['thread',child],['dump'],['query',first,first&255,first*31,-64],['dump']]
yield {'base':f'{base:x}','modifier':modifier,'precipitation':True,'ops':ops}
def biome_resources(jar):
prefix='data/minecraft/worldgen/biome/'
with zipfile.ZipFile(jar) as archive:
return {'minecraft:'+name[len(prefix):-5]:archive.read(name).decode('utf-8') for name in sorted(archive.namelist()) if name.startswith(prefix) and name.endswith('.json')}
def codec_corpus(biomes):
cases=list(biomes.values())+['null','false','0','[]','{}']
template={'has_precipitation':True,'temperature':0.5,'downfall':0.5,'temperature_modifier':'none'}
tokens=['null','true','false','0','-0.0','0.1','0.15','0.2','1','-1','1.000000059604644775390625','1.000000059604644775390626','1e40','-1e40','1e1000','-1e1000','1e-1000','"none"','"frozen"','"NONE"','"minecraft:none"','"NaN"','"Infinity"','"0.1"','[]','{}']
for key in template:
cases.append(json.dumps({k:v for k,v in template.items() if k!=key}))
for token in tokens:
cases.append('{'+','.join(json.dumps(k)+':'+(token if k==key else json.dumps(v)) for k,v in template.items())+'}')
cases += [json.dumps(dict(template,unknown={'anything':True})),
'{"has_precipitation":true,"temperature":0.2,"temperature":-0.0,"downfall":0.0}',
'{"has_precipitation":true,"temperature":0.2,"temperature":null,"downfall":0.0}']
operations=[['query',0,y,0,63] for y in [-64,0,63,79,80,81,319]]+[['query',12345,100,-9876,63],['query',12345,100,-9876,-1000],['dump']]
for value in cases:yield {'json':value,'ops':operations}
def main():
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument("--prepare",action="store_true");args=parser.parse_args()
build=lab.LOCAL/"rust-biome-temperature-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()
biomes=biome_resources(jars['server'])
if not biomes or biomes!=biome_resources(jars['client']):raise ValueError('client/server biome resources differ')
(build/'biomes.json').write_text(json.dumps(biomes,separators=(',',':')),encoding='utf-8')
oracle=lab.ROOT/"mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBiomeTemperatureOracle.java"
sources=[oracle,Path(__file__),lab.ROOT/"tools/test_worldgen_parity.py",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",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/biome_temperature_fixture.rs"]
tested_sources=base.source_hashes(sources)
fixtures=[*corpus(),*codec_corpus(biomes)];inputs=build/"inputs.txt";inputs.write_text("".join(json.dumps(f,separators=(",",":"))+"\n" for f in fixtures),encoding="utf-8")
fixture_paths=[inputs,build/"biomes.json"];tested_fixtures=base.source_hashes(fixture_paths);classes=build/"classes";classes.mkdir(exist_ok=True)
def run(command,name):
with (build/(name+".log")).open("w",encoding="utf-8") as log:base.run(list(map(str,command)),stdout=log,stderr=subprocess.STDOUT)
dependency_path=os.pathsep.join(map(str,libraries))
run(["javac","--release",lock["minecraft"]["java_major"],"-proc:none","-cp",str(jars["server"])+os.pathsep+dependency_path,"-d",classes,oracle],"compile-java")
run(["cargo","build","--release","-p","horizon-world","--example","world_probe"],"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"/("world_probe.exe" if os.name=="nt" else "world_probe")
run([binary,"biome_temperature_fixture",build],"rust")
references=[]
for side,jar in jars.items():
reference=build/f"{side}.txt"
run(["java","-Xmx512M","-cp",os.pathsep.join([str(classes),str(jar),dependency_path]),"dev.horizonlab.parity.VanillaBiomeTemperatureOracle",side,build],side)
matched=base.compare(reference,build/"rust.txt")
references.append({"side":side,"jar_sha256":lab.digest(jar,"sha256"),"output_sha256":lab.digest(reference,"sha256"),"matched_output_lines":matched})
print(f"{side}: {matched:,} biome-temperature output lines match Rust",flush=True)
base.ensure_sources_unchanged(sources,tested_sources);base.ensure_sources_unchanged(fixture_paths,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(),
"fixtures":len(fixtures),"biome_resources":len(biomes),"codec_inputs":sum("json" in f for f in fixtures),"operations":sum(len(f["ops"]) for f in fixtures),"thread_isolation_fixtures":sum(any(op[0]=="thread" for op in f["ops"]) for f in fixtures),
"sources_sha256":tested_sources,"fixtures_sha256":tested_fixtures,"rust_output_sha256":lab.digest(build/"rust.txt","sha256"),"references":references,
"scope":"Actual Biome temperature methods, warm/cold/iceberg-melt predicates and precipitation selection, with both temperature modifiers and arbitrary base float bits. Native private climate/biome constructors provide explicit temperature inputs; unrelated biome fields are null and never queried. Fixed seeded simplex/stack sources, float/double conversion and arithmetic order, height adjustment and sea-level overflow, packed-position aliases, repeated queries with changed sea levels, cache capacity/order/content, NaN recomputation and eviction, and actual native thread-local isolation versus independently owned Rust workers. ClimateSettings map-codec parsing additionally covers all independently authenticated bundled biome definitions, direct float conversion, nonfinite overflow, modifier defaults/null handling, field types, duplicates and missing fields. Full biome data codecs, climate encoding, complete weather placement/freezing, material condition integration and automatic migration of shared biome instances between threads remain outside this scope."}
lab.write_json(report_path,report);print(f"Scoped biome-temperature parity passed: {report_path}")
if __name__=="__main__":main()