mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Merge pull request #11403 from TheMrZZ/fix/h065-shrieker-local-capture
Keep the client-level shrieker effect out of shared capture
This commit is contained in:
2 files changed
+395
-2
No files matched your search
@@ -1643,7 +1643,7 @@ impl Game<'_> {
|
||||
self.particles
|
||||
.add(&ParticleOptions::sculk_charge(roll), at, speed);
|
||||
}
|
||||
/// `LevelEvent` 3007: ten shrieks rising from the shrieker, and the shriek.
|
||||
/// `LevelEventHandler.levelEvent` 3007: ten shrieks and a local sound.
|
||||
pub(super) fn shriek_event(&mut self, pos: BlockPos) {
|
||||
let top = f64::from(pos.y) + 0.5;
|
||||
for i in 0..10 {
|
||||
@@ -1656,7 +1656,7 @@ impl Game<'_> {
|
||||
let state = self.world.block(pos.x, pos.y, pos.z);
|
||||
if self.state_property(state, "waterlogged") != Some("true") {
|
||||
let pitch = 0.6 + self.particles.random().next_float() * 0.4;
|
||||
self.audio.play(
|
||||
self.audio.play_unshared(
|
||||
"minecraft:block.sculk_shrieker.shriek",
|
||||
Some([f64::from(pos.x) + 0.5, top, f64::from(pos.z) + 0.5]),
|
||||
2.0,
|
||||
@@ -2575,3 +2575,148 @@ mod command_damage_listener_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod shriek_capture_tests {
|
||||
use super::*;
|
||||
use crate::game::{Audio, Options, Progress, Settings, load_resources};
|
||||
use horizon_client::chunk::Chunk;
|
||||
use horizon_core::{position::ChunkPos, random::LegacyRandom};
|
||||
use horizon_world::level::LevelEvent;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES and scratch homes/cwd"]
|
||||
fn level_event_shriek_keeps_local_play_and_random_but_not_shared_capture() {
|
||||
let root = std::path::PathBuf::from(
|
||||
std::env::var_os("HORIZON_TEST_RESOURCES").expect("prepared resources required"),
|
||||
);
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
root.clone(),
|
||||
None,
|
||||
&root,
|
||||
&settings,
|
||||
std::time::Instant::now(),
|
||||
false,
|
||||
true,
|
||||
&Progress::new(),
|
||||
&mut |_, _| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
|
||||
options.resources = root.clone();
|
||||
options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer;
|
||||
let mut game = Game::new(
|
||||
&loaded.resources,
|
||||
loaded.pack,
|
||||
loaded.gui.unwrap(),
|
||||
options,
|
||||
settings,
|
||||
Audio::silent(&root),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
game.build_registries();
|
||||
game.set_allow_commands(true);
|
||||
game.world
|
||||
.chunks
|
||||
.insert((0, 0), Arc::new(Chunk::empty(0, 0)));
|
||||
game.block_ticks
|
||||
.add_empty_chunk(ChunkPos::new(0, 0))
|
||||
.unwrap();
|
||||
assert!(game.run_command("forceload add 0 0"));
|
||||
assert!(game.run_command("setblock 4 200 4 sculk_shrieker[waterlogged=false] strict"));
|
||||
assert!(game.run_command("setblock 7 200 4 sculk_shrieker[waterlogged=true] strict"));
|
||||
assert_eq!(
|
||||
game.state_property(game.world.block(4, 200, 4), "waterlogged"),
|
||||
Some("false")
|
||||
);
|
||||
assert_eq!(
|
||||
game.state_property(game.world.block(7, 200, 4), "waterlogged"),
|
||||
Some("true")
|
||||
);
|
||||
game.particles.requested = Some(Vec::new());
|
||||
game.particles.seed_randoms(6500, 6501, 6502);
|
||||
let mut random = LegacyRandom::new(6500);
|
||||
let clock = game.world.game_time;
|
||||
let mut world_random = game.world.random.clone();
|
||||
let effect = "minecraft:block.sculk_shrieker.shriek";
|
||||
let before = "minecraft:block.note_block.pling";
|
||||
let after = "minecraft:block.amethyst_block.chime";
|
||||
game.audio.capture = Some(Vec::new());
|
||||
game.audio
|
||||
.play_on(4, before, Some([1.5, 200.0, 4.5]), 1.0, 1.0);
|
||||
game.level_events.push(LevelEvent::LevelEvent {
|
||||
id: 3007,
|
||||
pos: BlockPos::new(4, 200, 4),
|
||||
data: 0,
|
||||
});
|
||||
game.process_level_events().unwrap();
|
||||
assert_eq!(
|
||||
game.audio.heard.get(effect),
|
||||
Some(&1),
|
||||
"dry effect must still play locally"
|
||||
);
|
||||
random.next_float(); // Same single pitch draw after the particle requests.
|
||||
assert_eq!(
|
||||
game.particles.random().next_long(),
|
||||
random.next_long(),
|
||||
"dry random continuation"
|
||||
);
|
||||
game.level_events.push(LevelEvent::LevelEvent {
|
||||
id: 3007,
|
||||
pos: BlockPos::new(7, 200, 4),
|
||||
data: 0,
|
||||
});
|
||||
game.process_level_events().unwrap();
|
||||
assert_eq!(
|
||||
game.audio.heard.get(effect),
|
||||
Some(&1),
|
||||
"waterlogged effect stays silent"
|
||||
);
|
||||
assert_eq!(
|
||||
game.particles.random().next_long(),
|
||||
random.next_long(),
|
||||
"wet branch consumes no pitch draw"
|
||||
);
|
||||
let requests = game.particles.requested.as_ref().unwrap();
|
||||
assert_eq!(requests.len(), 20);
|
||||
for (i, (options, position, velocity)) in requests.iter().enumerate() {
|
||||
assert_eq!(options.ty, ParticleType::Shriek);
|
||||
assert_eq!(options.scale, ((i % 10) * 5) as f32);
|
||||
assert_eq!(*position, [if i < 10 { 4.5 } else { 7.5 }, 200.5, 4.5]);
|
||||
assert_eq!(*velocity, [0.0; 3]);
|
||||
}
|
||||
game.audio
|
||||
.play_on(4, after, Some([2.5, 200.0, 4.5]), 0.5, 1.25);
|
||||
assert_eq!(game.audio.heard.get(before), Some(&1));
|
||||
assert_eq!(game.audio.heard.get(after), Some(&1));
|
||||
assert_eq!(game.world.game_time, clock);
|
||||
assert_eq!(
|
||||
game.world.random.next_long(),
|
||||
world_random.next_long(),
|
||||
"level random continuation"
|
||||
);
|
||||
let shared = game
|
||||
.audio
|
||||
.capture
|
||||
.as_ref()
|
||||
.expect("shared capture restored");
|
||||
assert_eq!(
|
||||
shared.iter().map(|s| s.event.as_str()).collect::<Vec<_>>(),
|
||||
vec![before, after],
|
||||
"client-level shriek entered shared capture"
|
||||
);
|
||||
for (sound, (position, volume, pitch)) in shared.iter().zip([
|
||||
([1.5, 200.0, 4.5], 1.0, 1.0),
|
||||
([2.5, 200.0, 4.5], 0.5, 1.25),
|
||||
]) {
|
||||
assert_eq!(
|
||||
(sound.position, sound.volume, sound.pitch, sound.source),
|
||||
(position, volume, pitch, 4)
|
||||
);
|
||||
assert_eq!((sound.entity, sound.except_entity), (None, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A real client jump lands on a shrieker; retain full raw wire packets.
|
||||
|
||||
Only the shriek's shared-packet absence, admitted level event and explicit shared
|
||||
controls form the contract. Seeds, random pitch/order and unrelated counts stay raw.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import math
|
||||
import uuid
|
||||
import anvil_dump as nbt
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import net_capture
|
||||
import test_hostile_sound_categories_parity as sound
|
||||
import test_locator_bar_parity as net
|
||||
from test_dedicated_server_parity import Server, game_time
|
||||
|
||||
PLAYER = 'Alice'
|
||||
ACTOR = 'Bob'
|
||||
SHRIEK = 'minecraft:block.sculk_shrieker.shriek'
|
||||
CONTROL = 'minecraft:block.note_block.pling'
|
||||
FIELDS = ['event', 'source', 'position', 'volume_bits', 'pitch_bits']
|
||||
CASES = [('dry', False), ('waterlogged', True)]
|
||||
HELPERS = ['tools/anvil_dump.py','tools/net_capture.py','tools/test_hostile_sound_categories_parity.py',
|
||||
'tools/test_locator_bar_parity.py','tools/test_dedicated_server_parity.py',
|
||||
'mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java',
|
||||
'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaPacketCodecOracle.java']
|
||||
|
||||
def helper_hashes():
|
||||
return {n: hashlib.sha256((net.lab.ROOT/n).read_bytes()).hexdigest() for n in HELPERS}
|
||||
|
||||
|
||||
def program():
|
||||
return dict(cases=CASES, source=[8, 100, 5],
|
||||
observer=dict(name=PLAYER,position=[2.5,100.0,2.5]),
|
||||
actor=dict(name=ACTOR,start=[8.5,100.5,1.9],end=[8.5,100.5,5.5]),
|
||||
initialization='join both clients, then freeze and positively create objective after geometry setup',
|
||||
observation_ticks=12, action='existing client arc Bob -1, one world tick per received real movement packet',
|
||||
contract='actual stepOn 3007 at source, shared controls before/after; no separate shriek Sound packet',
|
||||
raw='all packets/counts/order/seeds/pitch/commands/bodies/absolute clocks',
|
||||
sound_emission_time=None)
|
||||
|
||||
|
||||
def run(label, wet, command, client_command, build, world):
|
||||
directory = build / label
|
||||
port = net.server_dir(directory, world)
|
||||
server = Server(command, directory, label)
|
||||
commands = sound.Commands(server, build / (label+'-commands.json'))
|
||||
listen = net.lab.free_port()
|
||||
capture = build / (label+'-packets.jsonl')
|
||||
client_log = (build / (label+'-client.log')).open('w')
|
||||
client = actor = stop = None
|
||||
actor_log=(build/(label+'-actor-client.log')).open('w')
|
||||
clocks = []
|
||||
def clock(stage, elapsed):
|
||||
value = game_time(server)
|
||||
clocks.append(dict(stage=stage, absolute=value, relative=value-start, expected=elapsed,
|
||||
recorded_at=sound.now(), sound_emission_time=None))
|
||||
assert value-start == elapsed, clocks[-1]
|
||||
try:
|
||||
server.wait_for(r'Done \(')
|
||||
stop = net_capture.start_background(listen, f'127.0.0.1:{port}', capture, full=True)
|
||||
client = net.Client(client_command, listen, build/(label+'-wire.txt'), client_log)
|
||||
for line in ['sounds on', 'events on', 'chat on', 'join Alice', 'fly Alice on']:
|
||||
client.tell(line)
|
||||
server.wait_for('Alice joined the game', timeout=120)
|
||||
client.sync([PLAYER])
|
||||
commands.reply('tp Alice 2.5 100 2.5 0 0')
|
||||
commands.passed('execute if entity @a[name=Alice,type=player,nbt={UUID:[I;'+sound.uuid_words(PLAYER)+'],Health:20f,Pos:[2.5d,100d,2.5d],playerGameType:1}]')
|
||||
commands.reply('data get entity Alice')
|
||||
actor=net.Client(client_command,listen,build/(label+'-actor-wire.txt'),actor_log)
|
||||
for line in ['chat on','join Bob','fly Bob on']:actor.tell(line)
|
||||
server.wait_for('Bob joined the game',timeout=120);actor.sync([ACTOR])
|
||||
commands.reply('tick freeze')
|
||||
commands.reply('tp Bob 8.5 100.5 1.9 0 0')
|
||||
commands.passed('execute if entity @a[name=Bob,type=player,nbt={UUID:[I;'+sound.uuid_words(ACTOR)+'],Health:20f,Pos:[8.5d,100.5d,1.9d],playerGameType:1}]')
|
||||
commands.reply('data get entity Bob')
|
||||
client.sync([PLAYER]);actor.sync([ACTOR])
|
||||
for z in range(0,6):
|
||||
commands.passed(f'execute if loaded 8 100 {z}')
|
||||
commands.passed(f'execute if block 8 99 {z} stone')
|
||||
commands.passed(f'execute if block 8 101 {z} air')
|
||||
for z in range(0,5):
|
||||
commands.reply(f'setblock 8 100 {z} stone_slab[type=bottom,waterlogged=false] strict')
|
||||
commands.passed(f'execute if block 8 100 {z} stone_slab[type=bottom,waterlogged=false]')
|
||||
water = str(wet).lower()
|
||||
commands.reply(f'setblock 8 100 5 sculk_shrieker[shrieking=false,can_summon=false,waterlogged={water}] strict')
|
||||
commands.passed(f'execute if block 8 100 5 sculk_shrieker[shrieking=false,can_summon=false,waterlogged={water}]')
|
||||
created=commands.reply('scoreboard objectives add h065 dummy')
|
||||
assert 'Created new objective' in created and 'h065' in created,created
|
||||
start = game_time(server)
|
||||
controls = []
|
||||
for phase in ['before','after']:
|
||||
client.tell('step '+phase);client.sync([PLAYER])
|
||||
if phase == 'after':
|
||||
client.tell('step actual-jump');client.sync([PLAYER])
|
||||
def movement_count():
|
||||
try:
|
||||
rows=net_capture.load(capture)
|
||||
except json.JSONDecodeError:
|
||||
return 0 # Proxy may still be appending the final complete raw line.
|
||||
return sum(r.get('dir')=='c2s' and r.get('state')=='play' and r.get('name')=='move_player_pos_rot' for r in rows)
|
||||
initial_moves=movement_count()
|
||||
actor.tell('step actual-jump');actor.sync([ACTOR])
|
||||
actor.tell('arc Bob -1')
|
||||
for tick in range(1,13):
|
||||
deadline=time.monotonic()+45
|
||||
while movement_count()<initial_moves+tick:
|
||||
assert time.monotonic()<deadline,('no actual movement input',tick)
|
||||
time.sleep(.02)
|
||||
# Observe real input and pass a server receipt barrier while
|
||||
# frozen, then advance exactly one measured world tick.
|
||||
commands.reply('time query gametime')
|
||||
commands.reply('tick step 1')
|
||||
while game_time(server)<start+tick:
|
||||
assert time.monotonic()<deadline,('step stalled',tick)
|
||||
time.sleep(.02)
|
||||
clock('jump-'+str(tick),tick)
|
||||
actor.sync([ACTOR]);client.sync([PLAYER])
|
||||
assert movement_count()==initial_moves+12,'unexpected movement count'
|
||||
commands.passed(f'execute if block 8 100 5 sculk_shrieker[shrieking=true,can_summon=false,waterlogged={water}]')
|
||||
commands.reply('data get entity Bob') # Full live projection remains a raw diagnostic.
|
||||
client.sync([PLAYER]);sound.dump(client,'actual-event')
|
||||
client.tell('step after');client.sync([PLAYER])
|
||||
for key,initial in [('result',31),('success',17)]:
|
||||
commands.reply(f'scoreboard players set {key} h065 {initial}')
|
||||
commands.reply('execute store result score result h065 store success score success h065 run playsound '+CONTROL+' block Alice 8.5 101 2.5 1 1')
|
||||
for key in ['result','success']:
|
||||
text=commands.reply(f'scoreboard players get {key} h065')
|
||||
assert f'{key} has 1 [h065]' in text,text
|
||||
deadline=time.monotonic()+30
|
||||
while True:
|
||||
client.sync([PLAYER]);sound.dump(client,phase)
|
||||
found=[r for r in sound.raw_sounds(build/(label+'-wire.txt')) if r['stage']==phase and r['event']==CONTROL]
|
||||
if found:break
|
||||
assert time.monotonic()<deadline,'shared control receipt missing'
|
||||
time.sleep(.02)
|
||||
assert len(found)==1,found
|
||||
row={k:found[0][k] for k in FIELDS}
|
||||
assert row==dict(event=CONTROL,source=4,position=[8.5,101.0,2.5],volume_bits=1065353216,pitch_bits=1065353216),row
|
||||
controls.append(row);clock(phase,0 if phase=='before' else 12)
|
||||
commands.passed('execute if entity @a[name=Bob,type=player,nbt={UUID:[I;'+sound.uuid_words(ACTOR)+'],Health:20f,playerGameType:1}]')
|
||||
commands.reply('data get entity Bob')
|
||||
commands.passed('execute if entity @a[name=Alice,type=player,nbt={UUID:[I;'+sound.uuid_words(PLAYER)+'],Health:20f,Pos:[2.5d,100d,2.5d],playerGameType:1}]')
|
||||
commands.reply('data get entity Alice')
|
||||
actor.sync([ACTOR]);sound.dump(actor,'actor-final');actor.end();actor.kill();actor=None
|
||||
client.sync([PLAYER]);sound.dump(client,'final')
|
||||
commands.reply('scoreboard objectives remove h065')
|
||||
commands.reply('tick unfreeze')
|
||||
finally:
|
||||
(build/(label+'-clocks.json')).write_text(json.dumps(clocks,indent=2))
|
||||
try:
|
||||
if actor:actor.kill()
|
||||
if client:
|
||||
try:sound.dump(client,'failure-or-final');client.end()
|
||||
finally:client.kill()
|
||||
finally:
|
||||
server.stop();client_log.close();actor_log.close()
|
||||
if stop:stop.set()
|
||||
# The actual saved actor is authoritative for position and ground state;
|
||||
# retain the live command view separately rather than patching its projection.
|
||||
raw=bytearray(hashlib.md5(('OfflinePlayer:'+ACTOR).encode()).digest())
|
||||
raw[6]=(raw[6]&15)|48;raw[8]=(raw[8]&63)|128
|
||||
identifier=str(uuid.UUID(bytes=bytes(raw)))
|
||||
candidates=[directory/'world/playerdata'/f'{identifier}.dat',directory/'world/players/data'/f'{identifier}.dat']
|
||||
save=next(path for path in candidates if path.exists());body=nbt.read_dat(save)
|
||||
(build/(label+'-actor-save.json')).write_text(json.dumps(body,indent=2))
|
||||
assert body['UUID']==('I',[int(x) for x in sound.uuid_words(ACTOR).split(',')]),body['UUID']
|
||||
assert body['Health']==('f',20.0) and body['playerGameType']==1,body
|
||||
assert body['OnGround']==('b',1),body['OnGround']
|
||||
assert all(t=='d' and abs(v-e)<.00001 for (t,v),e in zip(body['Pos'],[8.5,100.5,5.5])),body['Pos']
|
||||
typed=(build/(label+'-wire.txt')).read_text()
|
||||
assert ' error ' not in typed,typed
|
||||
events=[line for line in typed.splitlines() if ' event 3007 ' in line]
|
||||
assert len(events)==1 and events[0].endswith(' event 3007 8 100 5 0'),events
|
||||
sounds=sound.raw_sounds(build/(label+'-wire.txt'))
|
||||
for row in sounds:
|
||||
if row['event']==SHRIEK:
|
||||
assert row['position']==[8.5,100.5,5.5] and row['volume_bits']==1073741824,row
|
||||
assert math.isfinite(row['pitch']) and 0.6<=row['pitch']<=1.0,row
|
||||
(build/(label+'-sounds.json')).write_text(json.dumps(sounds,indent=2))
|
||||
packets=net_capture.load(capture)
|
||||
assert net_capture.play_sessions(packets)==[1,2], 'sequential observer/actor connections required'
|
||||
relevant=[r for r in packets if r.get('dir')=='s2c' and r.get('state')=='play' and r.get('name') in ['sound','sound_entity','level_event']]
|
||||
(build/(label+'-codec-input.jsonl')).write_text(''.join(json.dumps(r)+'\n' for r in relevant))
|
||||
for row in sounds:
|
||||
full=bytes.fromhex(row['payload_hex']);identifier,offset=net_capture.read_varint(full,0)
|
||||
assert any(r['conn']==1 and r['id']==identifier and r['data']==full[offset:].hex() for r in relevant),'observer/raw connection identity'
|
||||
level=[r for r in relevant if r['conn']==1 and r['name']=='level_event' and bytes.fromhex(r['data'])[:4]==struct.pack('>i',3007)]
|
||||
assert len(level)==1,level
|
||||
payload=bytes.fromhex(level[0]['data'])
|
||||
assert len(payload)==17 and payload[-1]==0,level
|
||||
# Typed official decoding above admits id/position/data; retain the entire
|
||||
# packet and compare its bytes, including global=false, across the pair.
|
||||
return dict(controls=controls,level_event=payload.hex(),shriek_packets=sum(r['event']==SHRIEK for r in sounds),
|
||||
clocks='relative 0 before action / 12 after action',source_save='UUID/Health20/mode1/typedPos bounded1e-5/OnGround1 admitted',sound_emission_time=None)
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--binary',type=Path,required=True)
|
||||
parser.add_argument('--work-dir',type=Path,required=True)
|
||||
parser.add_argument('--reference-only',action='store_true')
|
||||
parser.add_argument('--reference-dir',type=Path)
|
||||
args=parser.parse_args();build=args.work_dir.resolve();build.mkdir(parents=True,exist_ok=False)
|
||||
spec=json.dumps(program(),indent=2)+'\n';(build/'program.json').write_text(spec)
|
||||
source=Path(__file__).read_bytes();sha=hashlib.sha256(source).hexdigest()
|
||||
(build/'manifest.json').write_text(json.dumps(dict(script_sha256=sha,helpers=helper_hashes(),declared_at=sound.now()),indent=2))
|
||||
lock,_,java,native,client=net.sides(str(build),args.binary.resolve())
|
||||
_,jars,libs=net.base.verify_artifacts();cp=os.pathsep.join(map(str,[jars['server'],*libs]))
|
||||
oracle=net.lab.ROOT/'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaPacketCodecOracle.java'
|
||||
subprocess.run(['javac','--release',str(lock['minecraft']['java_major']),'-proc:none','-cp',str(build/'classes')+os.pathsep+cp,'-d',build/'classes',oracle],check=True,capture_output=True)
|
||||
codec=['java','-cp',str(build/'classes')+os.pathsep+cp,'dev.horizonlab.parity.VanillaPacketCodecOracle']
|
||||
def side(prefix,command,world):
|
||||
out=[]
|
||||
for name,wet in CASES:
|
||||
label=prefix+'-'+name;out.append(run(label,wet,command,client,build,world))
|
||||
subprocess.run([*codec,build/(label+'-codec-input.jsonl'),build/(label+'-codec.txt'),'--verbose'],check=True)
|
||||
return out
|
||||
reference=args.reference_dir.resolve() if args.reference_dir else build
|
||||
if not args.reference_dir:
|
||||
world=sound.generate(java,build)
|
||||
expected=side('java',java,world)
|
||||
assert all(r['shriek_packets']==0 for r in expected),expected
|
||||
(build/'reference.json').write_text(json.dumps(expected,indent=2)+'\n')
|
||||
else:
|
||||
assert (reference/'program.json').read_text()==spec
|
||||
assert json.loads((reference/'manifest.json').read_text())['script_sha256']==sha
|
||||
assert json.loads((reference/'manifest.json').read_text())['helpers']==helper_hashes()
|
||||
if args.reference_only:return
|
||||
ref=(reference/'reference.json').read_bytes();expected=json.loads(ref)
|
||||
assert all(r['shriek_packets']==0 for r in expected)
|
||||
actual=side('native',native,reference/'generate/world')
|
||||
assert (reference/'reference.json').read_bytes()==ref
|
||||
report=dict(expected=expected,actual=actual,pass_contract=expected==actual,
|
||||
scope=program()['contract'],script_sha256=sha,reference_sha256=hashlib.sha256(ref).hexdigest())
|
||||
(build/'report.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
print(json.dumps(report))
|
||||
assert report['pass_contract'],'admitted shriek shared-capture mismatch'
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Reference in new issue
Block a user