From 3857fe99a6f31bb818e02d522736b9e2c3368d46 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Wed, 30 Sep 2026 22:18:35 +0200 Subject: [PATCH] =?UTF-8?q?Festgeschriebene=20Werte=20und=20Test=20f=C3=BC?= =?UTF-8?q?r=20=C3=BCberlange=20Namen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die neuen Tests in golden.rs vergleichen gegen einmal aufgezeichnete Werte statt gegen eine zweite Berechnung im selben Build. So fällt über Builds hinweg auf, wenn sich Regeln oder Formate unbeabsichtigt ändern: der Startzustand der Welt, das Ende einer festen Befehlsfolge und die Prüfsumme der Aufzeichnung. Die Fehlermeldung nennt die Versionskonstante, die dann erhöht werden muss. Der neue Test in world.rs deckt ab, dass überlange Namen auf beiden Beitrittswegen gekürzt gespeichert werden: beim ersten Beitritt und bei der Rückkehr. Co-Authored-By: Claude Sonnet 5.5 --- core/tests/golden.rs | 147 +++++++++++++++++++++++++++++++++++++++++++ core/tests/world.rs | 26 +++++++- 2 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 core/tests/golden.rs diff --git a/core/tests/golden.rs b/core/tests/golden.rs new file mode 100644 index 0000000..7c462a3 --- /dev/null +++ b/core/tests/golden.rs @@ -0,0 +1,147 @@ +//! Pinned values for everything that must not change unnoticed. +//! +//! Every other test compares two computations inside the same build, so a +//! change that affects both sides alike stays green. This file compares +//! against values recorded once. If one of them fails, rules or encodings +//! changed, on purpose or by accident. If saves or recordings are affected, +//! raise the version constant named in the failure message, then update the +//! value here deliberately. + +use grovewright_core::command::Command; +use grovewright_core::hash::fnv1a64; +use grovewright_core::map::Pos; +use grovewright_core::mapgen::outcrop_origin; +use grovewright_core::player::PlayerId; +use grovewright_core::replay::Recording; +use grovewright_core::world::World; + +const SEED: u64 = 2026; + +/// The commands of a session of 60 ticks. It is built from literal values +/// and the outcrop origin of the map, not from constants such as +/// `DIG_COOLDOWN_TICKS`, so that changing a rule changes the result and not +/// the script. +/// +/// Never change or extend this script. To pin more, add a new script and a +/// new pinned value instead. +fn frozen_script(world: &World) -> Vec> { + let origin = outcrop_origin(world.map()); + let right_of_origin = Pos::new(origin.x + 1, origin.y); + // Pairs of (tick, command). Commands of the same tick are applied in the + // order listed here. + let scripted = [ + // Two players join in the same tick. + ( + 0, + Command::Join { + key: [1; 16], + name: "Ada".to_owned(), + }, + ), + ( + 0, + Command::Join { + key: [2; 16], + name: "Grace".to_owned(), + }, + ), + // Player 0 hits the outcrop origin three times, five ticks apart. + // The third hit breaks the rock and gives a stone. + ( + 2, + Command::Dig { + player: PlayerId(0), + pos: origin, + }, + ), + ( + 7, + Command::Dig { + player: PlayerId(0), + pos: origin, + }, + ), + ( + 12, + Command::Dig { + player: PlayerId(0), + pos: origin, + }, + ), + // Player 1 hits the tile to the right of the origin once. + ( + 20, + Command::Dig { + player: PlayerId(1), + pos: right_of_origin, + }, + ), + // Player 1 leaves and returns with a name of 40 bytes, which the + // world shortens to 32. + ( + 30, + Command::Leave { + player: PlayerId(1), + }, + ), + ( + 40, + Command::Join { + key: [2; 16], + name: "ä".repeat(20), + }, + ), + ]; + let mut ticks = vec![Vec::new(); 60]; + for (tick, command) in scripted { + ticks[tick].push(command); + } + ticks +} + +/// Covers the map generator, the map encoding (terrain numbers, byte order of +/// width and height) and the snapshot header. +#[test] +fn the_starting_snapshot_is_unchanged() { + let actual = fnv1a64(&World::new(SEED).to_snapshot()); + assert_eq!( + actual, 0x1b78_d996_1586_fbf1, + "the starting world or the snapshot format changed; if saves are \ + affected, raise SNAPSHOT_VERSION in core/src/snapshot.rs; then \ + update this value" + ); +} + +/// Covers the rules of joining, leaving and digging, the shortening of names +/// and the encoding of players. +#[test] +fn the_frozen_session_ends_in_the_same_state() { + let mut world = World::new(SEED); + for commands in frozen_script(&world) { + world.step(&commands); + } + assert_eq!( + world.state_hash(), + 0xc304_43df_fc74_dcfc, + "the rules or the snapshot format changed; old recordings no longer \ + verify; if saves are affected, raise SNAPSHOT_VERSION in \ + core/src/snapshot.rs; then update this value" + ); +} + +/// Covers the recording header and layout and the wire format of commands. +#[test] +fn the_recording_format_is_unchanged() { + let mut world = World::new(SEED); + let mut recording = Recording::start(&world); + for commands in frozen_script(&world) { + recording.push_tick(&commands); + world.step(&commands); + } + assert_eq!( + fnv1a64(&recording.to_bytes(&world)), + 0x2312_5a7d_d8ef_5ae0, + "the recording or command format changed; raise RECORDING_VERSION in \ + core/src/replay.rs; then update this value" + ); +} diff --git a/core/tests/world.rs b/core/tests/world.rs index ae70c04..b839f5d 100644 --- a/core/tests/world.rs +++ b/core/tests/world.rs @@ -2,7 +2,9 @@ use grovewright_core::command::Command; use grovewright_core::inventory::ITEM_STONE; use grovewright_core::map::{Pos, Terrain}; use grovewright_core::mapgen::{outcrop_origin, spawn_pos}; -use grovewright_core::player::{PlayerId, PlayerKey, MAX_KNOWN_PLAYERS, MAX_ONLINE_PLAYERS}; +use grovewright_core::player::{ + PlayerId, PlayerKey, MAX_KNOWN_PLAYERS, MAX_NAME_BYTES, MAX_ONLINE_PLAYERS, +}; use grovewright_core::world::{Event, World, DIG_COOLDOWN_TICKS, TICKS_PER_SECOND}; fn key(n: u8) -> PlayerKey { @@ -112,6 +114,28 @@ fn a_returning_player_can_change_their_name() { assert_eq!(world.player(id).unwrap().name, "Neuer Name"); } +#[test] +fn overlong_names_are_stored_shortened_on_both_join_paths() { + let mut world = World::new(1); + world.step(&[Command::Join { + key: key(1), + name: "ä".repeat(20), + }]); + assert_eq!(world.player(PlayerId(0)).unwrap().name, "ä".repeat(16)); + + world.step(&[Command::Leave { + player: PlayerId(0), + }]); + world.step(&[Command::Join { + key: key(1), + name: "x".repeat(MAX_NAME_BYTES + 10), + }]); + assert_eq!( + world.player(PlayerId(0)).unwrap().name, + "x".repeat(MAX_NAME_BYTES) + ); +} + #[test] fn a_fifth_online_player_is_refused() { let mut world = World::new(1);