Files
Grovewright/core/tests/snapshot.rs
T
Steven Wroblewski 311516dd0a Snapshot und Zustandsprüfsumme
Der Snapshot enthält den vollständigen Zustand der Welt hinter einem
Kopf mit Format und Version. Er dient als Speicherstand und als
Grundlage der Prüfsumme. Das Konzept ist entsprechend korrigiert: Der
Speicherstand ist der vollständige Zustand, nicht Startzahl plus
Änderungen.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-30 21:18:09 +02:00

122 lines
3.4 KiB
Rust

use grovewright_core::codec::DecodeError;
use grovewright_core::command::Command;
use grovewright_core::map::Pos;
use grovewright_core::mapgen::outcrop_origin;
use grovewright_core::player::PlayerId;
use grovewright_core::snapshot::{SnapshotError, SNAPSHOT_VERSION};
use grovewright_core::world::World;
const ADA: PlayerId = PlayerId(0);
fn rock(world: &World) -> Pos {
outcrop_origin(world.map())
}
/// A world at tick 12 with one player who has hit a rock once.
fn played_world() -> World {
let mut world = World::new(7);
world.step(&[Command::Join {
key: [3; 16],
name: "Ada".to_owned(),
}]);
let pos = rock(&world);
world.step(&[Command::Dig { player: ADA, pos }]);
for _ in 0..10 {
world.step(&[]);
}
world
}
#[test]
fn a_snapshot_restores_an_identical_world() {
let world = played_world();
let restored = World::from_snapshot(&world.to_snapshot()).unwrap();
assert_eq!(restored, world);
assert_eq!(restored.state_hash(), world.state_hash());
}
#[test]
fn the_state_hash_covers_every_part_of_the_state() {
let base = played_world();
let pos = rock(&base);
let mut waited = base.clone();
waited.step(&[]);
assert_ne!(waited.state_hash(), base.state_hash(), "tick");
let mut dug = base.clone();
dug.step(&[Command::Dig { player: ADA, pos }]);
assert_ne!(dug.state_hash(), waited.state_hash(), "map and cooldown");
let mut left = base.clone();
left.step(&[Command::Leave { player: ADA }]);
assert_ne!(left.state_hash(), waited.state_hash(), "players");
assert_ne!(
World::new(1).state_hash(),
World::new(2).state_hash(),
"seed"
);
}
#[test]
fn equal_worlds_have_equal_hashes() {
assert_eq!(played_world().state_hash(), played_world().state_hash());
}
#[test]
fn a_restored_world_continues_exactly_like_the_original() {
let mut original = played_world();
let mut restored = World::from_snapshot(&original.to_snapshot()).unwrap();
let pos = rock(&original);
for tick in 0..40u32 {
let commands = if tick.is_multiple_of(6) {
vec![Command::Dig { player: ADA, pos }]
} else {
vec![]
};
assert_eq!(original.step(&commands), restored.step(&commands));
}
assert_eq!(original.state_hash(), restored.state_hash());
assert_eq!(original, restored);
}
#[test]
fn bytes_that_are_not_a_snapshot_are_rejected() {
assert_eq!(World::from_snapshot(b""), Err(SnapshotError::NotASnapshot));
assert_eq!(
World::from_snapshot(b"GR"),
Err(SnapshotError::NotASnapshot)
);
assert_eq!(
World::from_snapshot(b"nope, not a save"),
Err(SnapshotError::NotASnapshot)
);
}
#[test]
fn snapshots_of_another_version_are_rejected() {
let mut bytes = played_world().to_snapshot();
bytes[4..8].copy_from_slice(&(SNAPSHOT_VERSION + 1).to_le_bytes());
assert_eq!(
World::from_snapshot(&bytes),
Err(SnapshotError::UnsupportedVersion(SNAPSHOT_VERSION + 1))
);
}
#[test]
fn cut_off_and_padded_snapshots_are_rejected() {
let bytes = played_world().to_snapshot();
assert_eq!(
World::from_snapshot(&bytes[..bytes.len() - 1]),
Err(SnapshotError::Corrupt(DecodeError::UnexpectedEnd))
);
let mut padded = bytes.clone();
padded.push(0);
assert_eq!(
World::from_snapshot(&padded),
Err(SnapshotError::Corrupt(DecodeError::TrailingBytes))
);
}