From cd7d6a3dccfb82bd52be40100232e12121dcc764 Mon Sep 17 00:00:00 2001 From: Florian ERNST <30944236+TheMrZZ@users.noreply.github.com> Date: Fri, 6 Nov 2026 00:48:21 +0100 Subject: [PATCH] Preserve inherited squid damage acceptance result Co-Authored-By: Codex --- crates/horizon-client/src/mobs.rs | 73 ++++++++++- docs/parity/squid-damage-result.md | 145 +++++++++++++++++++++ tools/port_map/classes-01.tsv | 3 +- tools/test_squid_damage_result_parity.py | 152 +++++++++++++++++++++++ 4 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 docs/parity/squid-damage-result.md create mode 100644 tools/test_squid_damage_result_parity.py diff --git a/crates/horizon-client/src/mobs.rs b/crates/horizon-client/src/mobs.rs index 0bec4b7818..93e36e0ee9 100644 --- a/crates/horizon-client/src/mobs.rs +++ b/crates/horizon-client/src/mobs.rs @@ -3547,7 +3547,10 @@ impl Mob { if self.species() == Species::GlowSquid && self.ai.last_hurt_by.is_some() { aquatic::hurt_server(self); } - true + // Squid.hurtServer (also GlowSquid's result): parent health damage has + // happened, but without a last living attacker the subclass returns false. + !matches!(self.species(), Species::Squid | Species::GlowSquid) + || self.ai.last_hurt_by.is_some() } /// `damage` by a source whose direct entity is `direct` (an entity type id), which `source` /// cannot carry: `DamageSources.indirectMagic(potion or cloud, owner)`. @@ -5502,6 +5505,70 @@ mod effect_refusal_tests { ); } + fn check_squid_damage_result(id: &str) { + let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap()); + let types = MobTypes::load_for_server(&root).unwrap(); + let mut attacker = mob(&types, "minecraft:cow"); + attacker.id = 2; + attacker.position = Vec3::new(4.0, 64.0, 4.0); + assert!(attacker.alive()); + let by_cow = Damage { + kind: DamageKind::Generic, + ..Damage::mob(attacker.id, [4.0, 64.0, 4.0]).with_species(Species::Cow) + }; + // An unrelated parent's generic acceptance remains true. + let mut cow = mob(&types, "minecraft:cow"); + assert!(cow.damage(1.0, Damage::of(DamageKind::Generic))); + assert_eq!(cow.health, 9.0); + // A parent's existing rejection never reaches the subclass aftermath. + let mut protected = mob(&types, id); + protected.invulnerable_flag = true; + assert!(!protected.damage(1.0, by_cow)); + assert_eq!(protected.health, 10.0); + if protected.species() == Species::GlowSquid { + assert_eq!(aquatic::dark_ticks(&protected), 0); + } + for no_ai in [false, true] { + for attributed in [true, false] { + let mut victim = mob(&types, id); + victim.no_ai = no_ai; + assert_eq!(victim.health, 10.0); + assert!(victim.ai.last_hurt_by.is_none()); + let source = if attributed { + by_cow + } else { + Damage::of(DamageKind::Generic) + }; + let accepted = victim.damage(1.0, source); + // Check the parent's state first: false must not roll back damage. + assert_eq!(victim.health, 9.0); + assert_eq!( + victim.ai.last_hurt_by, + attributed.then_some(Who::Mob(attacker.id)) + ); + if victim.species() == Species::GlowSquid { + assert_eq!( + aquatic::dark_ticks(&victim), + if attributed { 100 } else { 0 } + ); + } + assert_eq!(accepted, attributed, "{id} NoAI={no_ai}"); + } + } + } + + #[test] + #[ignore = "needs prepared entity types in HORIZON_TEST_RESOURCES"] + fn glow_squid_damage_result_keeps_parent_health_side_effect() { + check_squid_damage_result("minecraft:glow_squid"); + } + + #[test] + #[ignore = "needs prepared entity types in HORIZON_TEST_RESOURCES"] + fn squid_damage_result_keeps_parent_health_side_effect() { + check_squid_damage_result("minecraft:squid"); + } + /// Squid.hurtServer reports a hit only when a living entity dealt it, so /// environmental damage leaves a glow squid glowing; a mob's blow dims it. #[test] @@ -5515,11 +5582,13 @@ mod effect_refusal_tests { let types = MobTypes::load_for_server(&root).unwrap(); let mut burned = mob(&types, "minecraft:glow_squid"); burned.health = 10.0; - assert!(burned.damage(1.0, Damage::of(DamageKind::Campfire))); + assert!(!burned.damage(1.0, Damage::of(DamageKind::Campfire))); + assert_eq!(burned.health, 9.0); assert_eq!(aquatic::dark_ticks(&burned), 0); let mut struck = mob(&types, "minecraft:glow_squid"); struck.health = 10.0; assert!(struck.damage(1.0, Damage::mob(2, [0.0, 64.0, 0.0]))); + assert_eq!(struck.health, 9.0); assert_eq!(aquatic::dark_ticks(&struck), 100); } } diff --git a/docs/parity/squid-damage-result.md b/docs/parity/squid-damage-result.md new file mode 100644 index 0000000000..5d101f7417 --- /dev/null +++ b/docs/parity/squid-damage-result.md @@ -0,0 +1,145 @@ +# Squid's inherited damage acceptance result + +Pinned Java26.3 `Squid.hurtServer` first calls its parent, then reports true only +with a non-null last living attacker. `GlowSquid.hurtServer` returns that same +result and sets dark ticks100 only when true. A false subclass result does not +undo the parent's health loss. + +Current main already has the correct ink/dark guards, from #8864 (closed #8724). +They remain untouched. The sole gameplay change replaces the shared adapter's +final unconditional true with the inherited squid/glow acceptance condition. +Other species retain true after their accepted shared parent path. The old guard +test's erroneous environment-hit true assertion is corrected, adding health-loss +checks without changing that guard's implementation. + +No parent-health/identity refactor, H176 timer, stale-attacker lifecycle, ink RNG, +particles, sound, packet, rendering or other-species behavior repair/claim. + +## Declared actual Game/Java program + +``` +python3 tools/test_squid_damage_result_parity.py --binary /path/to/game --work-dir /scratch/run +python3 tools/test_squid_damage_result_parity.py --full-diagnostic --binary /path/to/game --work-dir /scratch/full +``` + +`--reference /scratch/old/vanilla.jsonl` requires byte-identical fresh Java bytes. +The existing command harness runs actual authoritative Game with its connected +owner and independently runs pinned Java in isolated fresh data. No new observer. + +Six fresh, unique-UUID victims: GlowSquid NoAI0/1 crossed with generic/cow-attributed +hits; ordinary Squid NoAI1 is the independently Java-measured inherited control. +Each starts Health10 and receives one generic1 hit. A real healthy NoAI cow causes +the attributed hits. Actor UUID/type/Health/Pos/NoAI/NoGravity/Silent/Persistence, +loaded water/floor/glass geometry, owner identity/position and actual relative +clock0 guard admission. No world steps, reused victim, stale attacker or cooldown +history; the cow remains independently alive/unchanged after all actions. + +Each hit has fresh result/success sentinels set to-99. Fresh Java FIRST measured +failure feedback/callback/stores: this program invokes false/0 and stores0 for +unattributed hits, not an assumed missing callback. Health becomes9 and glow dark +remains0. Cow-attributed hits give callback true/1 and both stores1, Health9 and +glow dark100. Every damage message/style/callback/store remains strictly compared. + +Predeclared before native captures: **193** complete raw records. Only full bodies, +Motion vectors and absolute clock-start/end are diagnostic; all commands remain +traced. `--full-diagnostic` strictly compares all **221** records. No masks, +tolerances, feedback normalization or selection changes after capture. Complete +Motion records are retained without an RNG/physics parity claim. + +Frozen source: `2266c4de14561146044c422e6c525b3ce52b0f79`. + +| Program | Exact script SHA256 | +|---|---| +| Scoped193 | `25eec1729a070935d3b4bf075978bd1a44399df11a86e770a6387a8d862e4a7b` | +| Full221 | `548fe826c429f551e55012a16bdff078ffca9e5147e4c7e58b009674028c3dc1` | + +Old current Game: **184/193**. Health9 and dark0 already match; historical dark100 +failures are not recycled as this baseline. Exactly these three suffixes differ +for EACH of glow_ai0_generic, glow_ai1_generic and squid_generic (nine records): + +| Suffix | Java expected | Old native actual | +|---|---|---| +| -hit | Invulnerable failure, callback false/0 | Applied1 damage success, true/1 | +| -result | 0 | 1 | +| -success | 0 | 1 | + +Every attributed hit, actual health/dark/type/identity/geometry/clock control +passes old. Full old diagnostic: **196/221**, including full bodies and Motion. + +## Reachable regressions and retained evidence + +`glow_squid_damage_result_keeps_parent_health_side_effect` and its Squid twin +call actual `Mob.damage` on real prepared entity definitions. Both fail old at +returned true versus false AFTER asserting Health9, last attacker and dark0. +Both pass fixed. Fresh victims cover both NoAI modes, with prior positive cow-hit, +other-species Cow generic acceptance and parent-invulnerability controls. No +copied health algorithm, synthetic return value or clock setter. The existing +resource-backed glow guard test also passes with its corrected return expectation. + +Programs, byte-identical scoped references, raw styled feedback/callbacks, full +bodies/Motion, transcripts, source/checkpoint hashes, pinned bytecode and immutable +old/fixed executables are retained under the owner's `.local/h177`. No game files +are published. Available file timestamps and logged stage durations are retained; +unobserved event stage times remain unknown. + +## Fixed result and complete diagnostic residuals + +Final actual Game: **193/193**, versus **184/193** old, with byte-identical scoped +script and fresh Java reference bytes. Both resource-backed Mob.damage tests and +the corrected existing dark-guard test pass. Full221: **196/221 → 205/221**. No +full221, whole-body, RNG/ink or raw-clock parity claim. + +The full program is byte-identical across builds. Its fresh Java references differ +only at absolute clocks; non-clock records are identical. All Motion records in +this fixture match, but do not establish a general movement/RNG contract. All16 +remaining full-record differences are listed below. In addition to extra default +attributes, the three attributed after-bodies omit Java's saved attacker fields. +This saved-attacker projection remains an unrelated diagnostic; runtime attributed +acceptance/dark100 are strictly covered. Complete raw bodies/styles/callbacks +remain mismatches in the full diagnostic. No normalization changes the counts. + +| Exact label | Java expected | Fixed native actual | +|---|---|---| +| `clock-start` | `305 (old fresh reference: 265)` | `0` | +| `before-cow-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.20000000298023224d}]` | +| `glow_ai0_cow-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `glow_ai0_cow-after-body` | `attributes: []; last_hurt_by_mob: [I; 177, 0, 0, 100]; ticks_since_last_hurt_by_mob: 0` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]; both saved attacker fields absent` | +| `glow_ai0_generic-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `glow_ai0_generic-after-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `glow_ai1_cow-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `glow_ai1_cow-after-body` | `attributes: []; last_hurt_by_mob: [I; 177, 0, 0, 100]; ticks_since_last_hurt_by_mob: 0` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]; both saved attacker fields absent` | +| `glow_ai1_generic-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `glow_ai1_generic-after-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `squid_cow-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `squid_cow-after-body` | `attributes: []; last_hurt_by_mob: [I; 177, 0, 0, 100]; ticks_since_last_hurt_by_mob: 0` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]; both saved attacker fields absent` | +| `squid_generic-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `squid_generic-after-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` | +| `after-cow-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.20000000298023224d}]` | +| `clock-end` | `305 (old fresh reference: 265)` | `0` | + +Scoped reference SHA256: `6b3a5398408e2bcdac6d6d949fb5bed88e8cb6d25d96fc97b913321bfcb492ef`. +Full fixed fresh reference SHA256: `2856764df3a97dbf707dc6ad5417a205f34b53c3a473ce05062f0e42b9a04f5e`. + +## Local readiness and source checkpoint + +Final production/test patch: rustfmt, clippy with warnings denied, doc tests and +public-tree checks pass. `pr_ready.py --quick --base 2266c4de14561146044c422e6c525b3ce52b0f79` +runs 1887 passing Rust tests, one failure, 64 ignored. The failure is the existing +`drops_match_vanillas_motion_bit_for_bit` Z last-bit difference at yaw37.5, +pitch-21.25: actual4597448471913462162 versus expected4597448471913462163. +The own archived OLD executable reproduces exactly the same failure. No drop +physics edit. Both new ignored resource-backed tests were explicitly run and pass, +as does the corrected existing guard test. + +Python: 696 tests, four failures and one skip. One is the installer signature +fixture offering x86_64 assets on an ARM64 host; three are optional-release +archive combinations hitting macOS Bash3.2 unset arrays. Narrow installer rerun +confirms its prerequisite. No installer/release repair. Broad parity is skipped +by `--quick`; the explicit actual Game/Java pairs above were run independently. +The readiness command exits nonzero for these unrelated failures; it is not green. + +Latest actual main pull/checkpoint: `42e6d9cc498998b7bd9d5648360f72531ae0fd8e`. +The relevant Mob/species/damage-command and command-harness/Java-oracle files have +no delta from the frozen proof base. No rebase, main merge or combined-latest +runtime claim. Evidence-only documentation was completed after the required run; +whitespace/public-tree checks are repeated on the final staged files. diff --git a/tools/port_map/classes-01.tsv b/tools/port_map/classes-01.tsv index f2d4e07a38..a4da2cf293 100644 --- a/tools/port_map/classes-01.tsv +++ b/tools/port_map/classes-01.tsv @@ -1,6 +1,7 @@ +Squid.hurtServer inline crates/horizon-client/src/mobs.rs::Mob::damage_with_player_context after shared parent damage, require last living attacker for true; retain health loss and existing ink guard GlowSquid.aiStep port crates/horizon-client/src/mobs/aquatic.rs::glow_squid_ai_step GlowSquid.checkGlowSquidSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules S::GlowSquid arm: 33 blocks below sea level, dark, in water -GlowSquid.hurtServer port crates/horizon-client/src/mobs/aquatic.rs::hurt_server +GlowSquid.hurtServer inline crates/horizon-client/src/mobs.rs::Mob::damage_with_player_context inherited Squid acceptance is returned after parent health effects; existing dim guard calls aquatic::hurt_server WolfSoundVariants skip:data registry bootstrap of the wolf sound sets; sounds are in species.rs and finalize.rs AbstractDragonPhaseInstance.getTurnSpeed inline crates/horizon-client/src/game/dragon.rs::Game::tick_dragon 0.7 / dist / rot turn for every phase but Landing; only applied while a fly target is set DragonChargePlayerPhase.doServerTick inline crates/horizon-client/src/game/dragon.rs::Game::tick_phase Charging arm: 10-tick countdown once close; gap: minor: no collision re-target diff --git a/tools/test_squid_damage_result_parity.py b/tools/test_squid_damage_result_parity.py new file mode 100644 index 0000000000..d57b1aca2a --- /dev/null +++ b/tools/test_squid_damage_result_parity.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Fresh Squid/GlowSquid damage results through actual Game and pinned Java. + +No world steps. Full bodies, Motion and absolute clocks remain executable raw +traces; --full-diagnostic strictly compares those too. No RNG/ink/timer claim. +""" +import hashlib +import json +from pathlib import Path +import sys + +import test_command_exec_parity as driver + +COW = '000000b1-0000-0000-0000-000000000064' +CASES = [('glow_ai0_cow', 'glow_squid', 0, True), + ('glow_ai0_generic', 'glow_squid', 0, False), + ('glow_ai1_cow', 'glow_squid', 1, True), + ('glow_ai1_generic', 'glow_squid', 1, False), + ('squid_cow', 'squid', 1, True), ('squid_generic', 'squid', 1, False)] + + +def program(full=False): + ops = [] + + def cmd(text, label, result=None, callback=None, admission=True, diagnostic=False): + op = {'op': 'cmd', 'cmd': text, 'label': label, 'admission': admission} + if result is not None: + op['expect_result'] = result + if callback is not None: + op['expect_callback'] = callback + if diagnostic and not full: + op.update(record=False, trace=True) + ops.append(op) + + cmd('/tick freeze', 'freeze') + cmd('/gamerule spawn_mobs false', 'no-spawn') + cmd('/gamerule advance_time false', 'no-daylight') + cmd('/forceload add 0 0', 'force', 1) + ops.append({'op': 'settle', 'required_chunks': [[0, 0]]}) + cmd('/fill 0 199 0 15 199 15 stone', 'floor', 256) + cmd('/fill 1 200 1 14 204 14 glass', 'walls-roof', 980) + cmd('/fill 2 200 2 13 203 13 water[level=0]', 'water', 576) + ops.append({'op': 'settle', 'required_chunks': [[0, 0]]}) + cmd('/tp @s 8.5 205 14.5 0 0', 'owner-position', 1) + cmd('/gamemode creative @s', 'owner-mode') + cmd('/execute if block 8 204 14 glass', 'owner-support', 1) + cmd('/scoreboard objectives add h177 dummy', 'objective') + cmd('/execute store result score start h177 run time query gametime', 'clock-start', diagnostic=True) + cmd('/execute if entity @s[type=player,name=Player]', 'owner-type', 1) + for field, result in [('UUID', 4), ('Health', 20), ('playerGameType', 1), ('Pos', 3)]: + cmd(f'/data get entity @s {field}', 'owner-' + field, result) + cmd('/summon cow 12.5 201 10.5 {UUID:[I;177,0,0,100],Health:10f,NoAI:1b,NoGravity:1b,Silent:1b,PersistenceRequired:1b,CustomName:"H177_cow"}', 'cow-summon', 1) + + def cow(label): + cmd(f'/execute as {COW} if entity @s[type=minecraft:cow]', label + '-cow-type', 1) + cmd('/execute if loaded 12 201 10', label + '-cow-loaded', 1) + cmd('/execute if block 12 201 10 water[level=0]', label + '-cow-water', 1) + for field, result in [('UUID', 4), ('Health', 10), ('Pos', 3), ('NoAI', 1), ('NoGravity', 1)]: + cmd(f'/data get entity {COW} {field}', label + '-cow-' + field, result) + cmd(f'/data get entity {COW}', label + '-cow-body', diagnostic=True) + + cow('before') + for n, (case, kind, no_ai, attributed) in enumerate(CASES, 1): + x = n * 2 + uuid = f'000000b1-0000-0000-0000-{n:012x}' + dark = ',DarkTicksRemaining:0' if kind == 'glow_squid' else '' + cmd(f'/execute if loaded {x} 201 4', case + '-loaded', 1) + cmd(f'/execute if block {x} 201 4 water[level=0]', case + '-water', 1) + cmd(f'/execute if block {x} 199 4 stone', case + '-floor', 1) + cmd(f'/summon {kind} {x + .5} 201 4.5 {{UUID:[I;177,0,0,{n}],Health:10f,NoAI:{no_ai}b,' + f'NoGravity:1b,Silent:1b,PersistenceRequired:1b,CustomName:"H177_{case}"{dark}}}', case + '-summon', 1) + for phase in ['before', 'after']: + if phase == 'after': + for holder in ['result', 'success']: + cmd(f'/scoreboard players set {holder} h177 -99', case + '-reset-' + holder, -99) + source = f' by {COW}' if attributed else '' + cmd(f'/execute store result score result h177 store success score success h177 ' + f'run damage {uuid} 1 minecraft:generic{source}', case + '-hit', admission=attributed) + for holder in ['result', 'success']: + cmd(f'/scoreboard players get {holder} h177', case + '-' + holder, admission=attributed) + cmd(f'/execute as {uuid} if entity @s[type=minecraft:{kind}]', case + '-' + phase + '-type', 1) + for field, result in [('UUID', 4), ('Health', 10 if phase == 'before' else 9), ('Pos', 3), + ('NoGravity', 1), ('Silent', 1), ('PersistenceRequired', 1)]: + cmd(f'/data get entity {uuid} {field}', case + '-' + phase + '-' + field, result) + cmd(f'/data get entity {uuid} NoAI', case + '-' + phase + '-NoAI', + callback=[True, 1] if no_ai else [False, 0]) + if kind == 'glow_squid': + expected = 100 if phase == 'after' and attributed else 0 + cmd(f'/data get entity {uuid} DarkTicksRemaining', case + '-' + phase + '-dark', expected) + cmd(f'/data get entity {uuid} Motion', case + '-' + phase + '-Motion', diagnostic=True) + cmd(f'/data get entity {uuid}', case + '-' + phase + '-body', diagnostic=True) + cow('after') + cmd('/execute store result score now h177 run time query gametime', 'clock-end', diagnostic=True) + cmd('/scoreboard players operation now h177 -= start h177', 'clock-delta-operation', 0) + cmd('/scoreboard players get now h177', 'clock-delta', 0) + return ops + + +def main(): + full = '--full-diagnostic' in sys.argv + if full: + sys.argv.remove('--full-diagnostic') + reference = None + if '--reference' in sys.argv: + i = sys.argv.index('--reference') + reference = Path(sys.argv[i + 1]).resolve() + del sys.argv[i:i + 2] + if '--work-dir' not in sys.argv: + sys.argv += ['--work-dir', str(driver.lab.LOCAL / 'squid-damage-result-parity')] + build = Path(sys.argv[sys.argv.index('--work-dir') + 1]).resolve() + ops = program(full) + driver.corpus_module.groups = lambda: [('squid inherited damage result', ops)] + sys.argv.append('--allow-gaps') + driver.main() + recorded = [op for op in ops if op['op'] == 'cmd' and op.get('record', True)] + java = [json.loads(s) for s in (build / 'vanilla.jsonl').read_text().splitlines()] + for op, row in zip(recorded, java): + if 'expect_results' in op: + assert row['results'] == op['expect_results'], ('Java action admission', op, row) + rows_by_label = {op['label']: row for op, row in zip(recorded, java)} + # Measured on the fresh reference before either native capture: an error + # here invokes false/0, not an assumed missing callback or stale sentinel. + for case, _, _, attributed in CASES: + expected = [[True, 1]] if attributed else [[False, 0]] + assert rows_by_label[case + '-hit']['results'] == expected + for holder in ['result', 'success']: + assert rows_by_label[case + '-' + holder]['results'] == [[True, int(attributed)]] + if '--reference-only' in sys.argv: + return + source = reference or build / 'vanilla.jsonl' + if reference: + assert source.read_bytes() == (build / 'vanilla.jsonl').read_bytes(), 'fresh Java bytes changed' + expected = [json.loads(s) for s in source.read_text().splitlines()] + native = [json.loads(s) for s in (build / 'rust.jsonl').read_text().splitlines()] + assert len(expected) == len(native) == len(recorded) + differences = [{'label': op['label'], 'expected': a, 'actual': b} + for op, a, b in zip(recorded, expected, native) if a != b] + report = {'records': len(native), 'passed': len(native) - len(differences), + 'reference_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), + 'comparison': 'complete raw records, no masks/tolerances', 'differences': differences} + (build / 'strict-report.json').write_text(json.dumps(report, indent=2) + '\n') + if not full: + for op, a, b in zip(recorded, expected, native): + if op['admission']: + assert a == b, ('actual fixture admission', op, a, b) + print(f'Strict raw records: {report["passed"]}/{report["records"]}') + if differences: + raise SystemExit(1) + + +if __name__ == '__main__': + main()