Files
horizon-airdef/tools/test_stored_effect_attributes_parity.py
T
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

114 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""Compare actual attribute map/supplier allocation, effect modifiers and live dirty queues."""
import argparse
import json
import os
from pathlib import Path
import platform
import subprocess
import random
import struct
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab
import test_rust_parity as base
def corpus():
rng = random.Random(0x53544f5241545452)
def bits(v): return struct.pack('>d', float(v)).hex()
values = [-float('inf'), -1e30, -1, -0., 0., .1, 1, 2, 20, 1e16, float('inf'), float('nan')]
def op(kind, **kw):
r = dict(op=kind, key=0, instance=0, template=0, target=0, amplifier=0, flag=False, value=bits(0), fail=-1, mutate=-1, mutations=[])
r.update(kw); return r
def hashes(mode, n):
def unmix(x):
h = ((x ^ (x >> 16)) * pow(0x9e3779b9, -1, 2**32)) & 0xffffffff
return h if h < 0x80000000 else h - 2**32
if mode == 0: return [0] * n
if mode == 1: return [unmix(31)] * n
if mode == 2: return [unmix(127 + i % 4) for i in range(n)]
if mode == 3: return list(range(n))
return [rng.randrange(-2**31, 2**31) for _ in range(n)]
def fixture(n, mode, ops, null=False):
templates = [dict(id='horizon:m'+str(i), amount=bits(values[i % len(values)]), operation=i % 3) for i in range(n)]
definitions = [op('define', key=i, template=i) for i in range(n)]
if null: definitions.append(op('define', key=-1))
return dict(hashes=hashes(mode,n), defaults=[bits(values[(i+6) % len(values)]) for i in range(n)],
bases=[None if i % 5 == 4 else bits(values[(i+8) % len(values)]) for i in range(n)],
syncable=[i % 2 == 0 for i in range(n)], templates=templates, definitions=definitions,
ops=[op('get')] + ops)
for mode in range(5):
for n in [1, 2, 7, 24, 25, 49, 97]:
for null in [False, True]:
ops = [op('get',key=i) for i in range(n)] + [op('get',key=i) for i in range(n)]
ops += [op('add'),op('value'),op('queue-clear'),op('queue-clear',target=1),op('add',amplifier=2),op('value'),op('remove'),op('remove')]
ops += [op('clear'),op('get',key=0),op('modified',instance=0),op('put',key=-1,instance=0),op('get',key=-1),op('add',amplifier=-1)]
yield fixture(n,mode,ops,null)
actions = [[op('put',instance=0)], [op('put',key=-1,instance=0)], [op('put',instance=-1)],
[op('clear')], [op('queue-clear')], [op('queue-clear',target=1)],
[op('queue-add',instance=-1)], [op('queue-add',target=1,instance=-1)],
[op('queue-drop',instance=0)], [op('syncable',flag=False)], [op('syncable',flag=True)],
[op('default',value=bits(float('nan')))], [op('default',value=bits(-0.))]]
for n in [1,7,25]:
for mode in [0,1,4]:
for mutations in actions + [[op('put',key=i,instance=0) for i in range(n)]]:
for at in [0,1,2,3]:
yield fixture(n,mode,[op('drop'),op('get',mutate=at,mutations=mutations),op('lookup'),op('get'),op('add'),op('remove')])
for fail in range(8):
for kind in ['get','add','remove','modified']:
for sync in [False,True]:
yield fixture(7,1,[op('syncable',flag=sync),op('drop'),op(kind,fail=fail),op('lookup'),op('get'),op('value'),op('add'),op('remove')])
yield fixture(1,0,[op('modified',instance=-1),op('queue-clear'),op('put',instance=-1),op('lookup'),op('get'),op('get',key=-1)])
for _ in range(200):
n=rng.choice([1,7,24,25,49])
def raw():
return op(rng.choice(['put','drop','clear','queue-add','queue-drop','queue-clear','syncable','default']),
key=rng.randrange(n),instance=rng.choice([-1,0]),target=rng.randrange(2),flag=bool(rng.randrange(2)),value=bits(rng.choice(values)))
ops=[]
for __ in range(35):
if rng.randrange(4)==0:ops.append(raw());continue
ops.append(op(rng.choice(['get','get','add','remove','lookup','modified','value']),key=rng.randrange(-1,n),
amplifier=rng.choice([-2**31,-1,0,1,255,2**31-1]),instance=0,
fail=rng.choice([-1,-1,-1,0,1,2]),mutate=rng.choice([-1,-1,0,1,2,3]),
mutations=[raw() for ___ in range(rng.randrange(4))]))
yield fixture(n,rng.randrange(5),ops,bool(rng.randrange(2)))
def main():
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--prepare',action='store_true');args=parser.parse_args()
build=lab.LOCAL/'rust-stored-effect-attributes-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/VanillaStoredEffectAttributesOracle.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',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/stored_effect_attributes_fixture.rs']
tested_sources=base.source_hashes(sources)
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','-sourcepath',str(lab.ROOT/'mods/horizon/src/test/java'),'-cp',str(jars['server'])+os.pathsep+dependency_path,'-d',classes,oracle],'compile-java')
def java(side,mode,path):
run(['java','-Xmx512M','-cp',os.pathsep.join([str(classes),str(jars[side]),dependency_path]),'dev.horizonlab.parity.VanillaStoredEffectAttributesOracle',mode,path],side+'-'+mode)
fixtures=list(corpus());inputs=build/'inputs.txt';inputs.write_text(''.join(json.dumps(value,separators=(',',':'))+'\n' for value in fixtures),encoding='utf-8')
fixture_paths=[inputs];tested_fixtures=base.source_hashes(fixture_paths)
run(['cargo','build','--release','-p','horizon-world','--example','stored_effect_attributes_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'/('stored_effect_attributes_fixture.exe' if os.name=='nt' else 'stored_effect_attributes_fixture')
references=[]
for side,jar in jars.items():
java(side,side,build)
enriched=build/f'inputs-{side}.txt';fixture_paths.append(enriched);tested_fixtures.update(base.source_hashes([enriched]))
run([binary,build,side],'rust-'+side)
reference=build/f'{side}.txt';candidate=build/f'rust-{side}.txt';matched=base.compare(reference,candidate)
references.append({'side':side,'jar_sha256':lab.digest(jar,'sha256'),'output_sha256':lab.digest(reference,'sha256'),'matched_output_lines':matched,'rust_output_sha256':lab.digest(candidate,'sha256')})
print(f'{side}: {matched:,} stored effect attribute 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),'fastutil_sha256':{str(p.relative_to(lab.ROOT)):lab.digest(p,'sha256') for p in libraries if 'fastutil' in str(p)},'sources_sha256':tested_sources,'fixtures_sha256':tested_fixtures,'references':references,'scope':'Actual MobEffect modifier application bound to actual AttributeMap.getInstance, AttributeSupplier.createInstance and AttributeInstance replacement/modifier storage. Compares lazy creation, failed creation retained in update queues before map insertion, null cached entries, map key aliasing, native identity-hash update/sync set order, captured instance ownership after map clearing, dirty callback mutation/failure and value/cache/permanent state. Native instance identity hashes are exported per JVM for Rust replay. Supplier prototypes use real basic attributes and base values; isClientSyncable/getDefaultValue are controlled callbacks. Registered definitions, entity notification binding, bulk map methods and full gameplay/platform builds remain pending.'}
lab.write_json(report_path,report);print(f'Scoped stored effect attribute parity passed: {report_path}')
if __name__=='__main__':main()