d986f2a5db
Recording::from_bytes leitet start_tick aus der gespeicherten Startwelt ab. Kein Test prüfte das: Setzte ein Fehler start_tick beim Dekodieren auf 0, blieb er unbemerkt, und eine so dekodierte Aufzeichnung würde beim erneuten Kodieren oder Verlängern abstürzen. Der Test a_recording_can_start_in_the_middle_of_a_session kodiert die Aufzeichnung jetzt, dekodiert sie wieder und prüft, dass sie beim selben Takt endet und der ursprünglichen gleicht. Mit start_tick = 0 beim Dekodieren schlägt er fehl (Endtakt 70 statt 120). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
190 lines
5.9 KiB
Rust
190 lines
5.9 KiB
Rust
use grovewright_core::codec::DecodeError;
|
|
use grovewright_core::command::Command;
|
|
use grovewright_core::inventory::ITEM_STONE;
|
|
use grovewright_core::mapgen::outcrop_origin;
|
|
use grovewright_core::player::PlayerId;
|
|
use grovewright_core::replay::{Recording, RecordingError, RECORDING_VERSION};
|
|
use grovewright_core::snapshot::SnapshotError;
|
|
use grovewright_core::world::World;
|
|
|
|
const ADA: PlayerId = PlayerId(0);
|
|
|
|
/// Commands of tick `tick` in a short scripted session.
|
|
fn commands_for(world: &World, tick: u32) -> Vec<Command> {
|
|
if tick == 0 {
|
|
vec![Command::Join {
|
|
key: [1; 16],
|
|
name: "Ada".to_owned(),
|
|
}]
|
|
} else if tick.is_multiple_of(7) {
|
|
vec![Command::Dig {
|
|
player: ADA,
|
|
pos: outcrop_origin(world.map()),
|
|
}]
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
|
|
/// Plays 200 ticks from a fresh world and records them.
|
|
fn recorded_session() -> (Recording, World) {
|
|
let mut world = World::new(11);
|
|
let mut recording = Recording::start(&world);
|
|
for tick in 0..200 {
|
|
let commands = commands_for(&world, tick);
|
|
recording.step(&mut world, &commands);
|
|
}
|
|
(recording, world)
|
|
}
|
|
|
|
#[test]
|
|
fn stepping_through_a_recording_gives_the_events_of_the_world() {
|
|
let mut plain = World::new(11);
|
|
let mut recorded = World::new(11);
|
|
let mut recording = Recording::start(&recorded);
|
|
for tick in 0..30 {
|
|
let commands = commands_for(&plain, tick);
|
|
assert_eq!(
|
|
recording.step(&mut recorded, &commands),
|
|
plain.step(&commands),
|
|
"tick {tick}"
|
|
);
|
|
}
|
|
assert_eq!(recorded, plain);
|
|
assert_eq!(recording.end_tick(), 30);
|
|
}
|
|
|
|
#[test]
|
|
fn replaying_reaches_the_same_state() {
|
|
let (recording, world) = recorded_session();
|
|
assert_eq!(recording.tick_count(), 200);
|
|
assert_eq!(world.player(ADA).unwrap().inventory.count(ITEM_STONE), 1);
|
|
assert_eq!(recording.replay(), Ok(world));
|
|
}
|
|
|
|
#[test]
|
|
fn a_recording_survives_encoding() {
|
|
let (recording, world) = recorded_session();
|
|
let bytes = recording.to_bytes(&world);
|
|
let (decoded, expected_hash) = Recording::from_bytes(&bytes).unwrap();
|
|
assert_eq!(decoded, recording);
|
|
assert_eq!(expected_hash, world.state_hash());
|
|
}
|
|
|
|
#[test]
|
|
fn verify_accepts_a_faithful_recording() {
|
|
let (recording, world) = recorded_session();
|
|
assert_eq!(Recording::verify(&recording.to_bytes(&world)), Ok(world));
|
|
}
|
|
|
|
#[test]
|
|
fn verify_reports_a_replay_that_ends_in_another_state() {
|
|
let (recording, world) = recorded_session();
|
|
let mut bytes = recording.to_bytes(&world);
|
|
// The expected state hash follows the marker and the version.
|
|
bytes[8] ^= 0x01;
|
|
match Recording::verify(&bytes) {
|
|
Err(RecordingError::Diverged { expected, actual }) => {
|
|
assert_eq!(actual, world.state_hash());
|
|
assert_ne!(expected, actual);
|
|
}
|
|
other => panic!("expected a divergence, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_recording_can_start_in_the_middle_of_a_session() {
|
|
let mut world = World::new(11);
|
|
for tick in 0..50 {
|
|
let commands = commands_for(&world, tick);
|
|
world.step(&commands);
|
|
}
|
|
let mut recording = Recording::start(&world);
|
|
for tick in 50..120 {
|
|
let commands = commands_for(&world, tick);
|
|
recording.step(&mut world, &commands);
|
|
}
|
|
assert_eq!(recording.tick_count(), 70);
|
|
assert_eq!(recording.end_tick(), 120);
|
|
|
|
// Decoding derives the start tick from the stored start world. The
|
|
// decoded recording must end at the same tick and equal the original.
|
|
let (decoded, _) = Recording::from_bytes(&recording.to_bytes(&world)).unwrap();
|
|
assert_eq!(decoded.end_tick(), 120);
|
|
assert_eq!(decoded, recording);
|
|
|
|
assert_eq!(recording.replay(), Ok(world));
|
|
}
|
|
|
|
#[test]
|
|
fn an_empty_recording_replays_to_its_starting_state() {
|
|
let world = World::new(3);
|
|
let recording = Recording::start(&world);
|
|
assert_eq!(recording.tick_count(), 0);
|
|
assert_eq!(Recording::verify(&recording.to_bytes(&world)), Ok(world));
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "the world is not at the tick this recording has reached")]
|
|
fn a_recording_refuses_a_world_it_does_not_follow() {
|
|
let mut world = World::new(3);
|
|
let mut recording = Recording::start(&world);
|
|
world.step(&[]);
|
|
recording.step(&mut world, &[]);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "the end state is not at the tick this recording has reached")]
|
|
fn a_recording_refuses_an_end_state_at_another_tick() {
|
|
let (recording, mut world) = recorded_session();
|
|
world.step(&[]);
|
|
recording.to_bytes(&world);
|
|
}
|
|
|
|
#[test]
|
|
fn damaged_recordings_are_rejected() {
|
|
let (recording, world) = recorded_session();
|
|
let bytes = recording.to_bytes(&world);
|
|
|
|
assert_eq!(
|
|
Recording::from_bytes(b""),
|
|
Err(RecordingError::NotARecording)
|
|
);
|
|
assert_eq!(
|
|
Recording::from_bytes(&world.to_snapshot()),
|
|
Err(RecordingError::NotARecording)
|
|
);
|
|
|
|
let mut other_version = bytes.clone();
|
|
other_version[4..8].copy_from_slice(&(RECORDING_VERSION + 1).to_le_bytes());
|
|
assert_eq!(
|
|
Recording::from_bytes(&other_version),
|
|
Err(RecordingError::UnsupportedVersion(RECORDING_VERSION + 1))
|
|
);
|
|
|
|
assert_eq!(
|
|
Recording::from_bytes(&bytes[..bytes.len() - 1]),
|
|
Err(RecordingError::Corrupt(DecodeError::UnexpectedEnd))
|
|
);
|
|
|
|
let mut padded = bytes.clone();
|
|
padded.push(0);
|
|
assert_eq!(
|
|
Recording::from_bytes(&padded),
|
|
Err(RecordingError::Corrupt(DecodeError::TrailingBytes))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_recording_with_a_damaged_start_is_rejected() {
|
|
let (recording, world) = recorded_session();
|
|
let mut bytes = recording.to_bytes(&world);
|
|
// The snapshot starts after marker, version, expected hash and its own
|
|
// length: 4 + 4 + 8 + 4 bytes.
|
|
bytes[20 + 10] ^= 0x01;
|
|
assert_eq!(
|
|
Recording::from_bytes(&bytes),
|
|
Err(RecordingError::BadStart(SnapshotError::ChecksumMismatch))
|
|
);
|
|
}
|