Snapshot System Testing
This document covers testing approaches and strategies for the Rummage snapshot system. For a general overview of testing in Rummage, see the Testing Overview.
Types of Tests
The snapshot system should be tested at several levels:
- Unit Tests: Isolated tests of individual snapshot components and functions
- Integration Tests: Tests of snapshot system interaction with other game systems
- End-to-End Tests: Tests of complete game scenarios using snapshots
- Performance Tests: Tests of snapshot system performance characteristics
- Network Tests: Tests of snapshot integration with networking
Unit Testing
Unit tests focus on individual components of the snapshot system:
#![allow(unused)] fn main() { #[test] fn test_snapshot_creation() { // Set up a minimal app let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_plugin(SnapshotPlugin); // Add some test entities let entity1 = app.world.spawn((Snapshotable, TestComponent { value: 42 })).id(); let entity2 = app.world.spawn((Snapshotable, TestComponent { value: 123 })).id(); // Create a game state let game_state = GameState { turn: 1, phase: Phase::Main1, active_player: 0, }; app.insert_resource(game_state); // Create a snapshot app.world.send_event(SnapshotEvent::Take); app.update(); // Verify the snapshot was created let snapshot_registry = app.world.resource::<SnapshotRegistry>(); assert_eq!(snapshot_registry.snapshots.len(), 1, "Should create one snapshot"); // Verify the snapshot contents let snapshot = snapshot_registry.most_recent().unwrap(); assert_eq!(snapshot.turn, 1, "Snapshot should have the correct turn"); assert_eq!(snapshot.phase, Phase::Main1, "Snapshot should have the correct phase"); assert_eq!(snapshot.active_player, 0, "Snapshot should have the correct active player"); // Verify entities were captured assert_eq!(snapshot.game_data.len(), 2, "Snapshot should include 2 entities"); } #[test] fn test_snapshot_application() { // Set up a minimal app let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_plugin(SnapshotPlugin); // Add some test entities let entity1 = app.world.spawn((Snapshotable, TestComponent { value: 42 })).id(); // Create a game state let game_state = GameState { turn: 1, phase: Phase::Main1, active_player: 0, }; app.insert_resource(game_state); // Create a snapshot app.world.send_event(SnapshotEvent::Take); app.update(); // Get the snapshot ID let snapshot_id = app.world.resource::<SnapshotRegistry>() .most_recent().unwrap().id; // Modify the entity if let Some(mut test_comp) = app.world.get_mut::<TestComponent>(entity1) { test_comp.value = 99; } // Apply the snapshot app.world.send_event(SnapshotEvent::Apply(snapshot_id)); app.update(); // Verify the entity was restored to its original state let test_comp = app.world.get::<TestComponent>(entity1).unwrap(); assert_eq!(test_comp.value, 42, "Component should be restored to original value"); } }
Integration Testing
Integration tests verify how snapshots interact with other game systems:
#![allow(unused)] fn main() { #[test] fn test_snapshot_with_turn_system() { // Set up a test app with relevant plugins let mut app = App::new(); app.add_plugins(( MinimalPlugins, SnapshotPlugin, TurnSystemPlugin, )); // Configure auto-snapshots let mut config = SnapshotConfig::default(); config.auto_snapshot_on_turn = true; app.insert_resource(config); // Set up initial game state app.insert_resource(GameState { turn: 1, phase: Phase::Main1, active_player: 0, }); // Add some test entities app.world.spawn((Snapshotable, TestComponent { value: 42 })); // Advance the turn app.world.send_event(AdvanceTurnEvent); app.update(); // Verify a snapshot was automatically created let snapshot_registry = app.world.resource::<SnapshotRegistry>(); assert_eq!(snapshot_registry.snapshots.len(), 1, "Should create one snapshot on turn change"); // Verify the snapshot has the correct turn number let snapshot = snapshot_registry.most_recent().unwrap(); assert_eq!(snapshot.turn, 2, "Snapshot should capture the new turn number"); } }
End-to-End Testing
End-to-end tests verify complete game scenarios:
#![allow(unused)] fn main() { #[test] fn test_full_game_with_snapshots() { // Set up a complete game environment let mut app = App::new(); app.add_plugins(( DefaultPlugins, GameEnginePlugin, SnapshotPlugin, )); // Set up a test game setup_test_game(&mut app); // Play through multiple turns, taking snapshots for turn in 1..5 { // Play through a turn play_turn(&mut app); // Take a snapshot app.world.send_event(SnapshotEvent::Take); app.update(); } // Verify we have snapshots for each turn let snapshot_registry = app.world.resource::<SnapshotRegistry>(); assert_eq!(snapshot_registry.snapshots.len(), 4, "Should have 4 snapshots"); // Go back to turn 2 let turn_2_snapshot = snapshot_registry.snapshots.values() .find(|s| s.turn == 2) .unwrap(); app.world.send_event(SnapshotEvent::Apply(turn_2_snapshot.id)); app.update(); // Verify game state was restored correctly let game_state = app.world.resource::<GameState>(); assert_eq!(game_state.turn, 2, "Game should be restored to turn 2"); // Continue playing from this restored state play_turn(&mut app); // Verify the game progressed correctly from the restored state let game_state = app.world.resource::<GameState>(); assert_eq!(game_state.turn, 3, "Game should advance to turn 3"); } }
Performance Testing
Performance tests measure the impact of snapshots:
#![allow(unused)] fn main() { use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn snapshot_creation_benchmark(c: &mut Criterion) { c.bench_function("create snapshot with 100 entities", |b| { // Set up a test app let mut app = App::new(); app.add_plugins((MinimalPlugins, SnapshotPlugin)); // Add 100 test entities for i in 0..100 { app.world.spawn((Snapshotable, TestComponent { value: i })); } b.iter(|| { // Create a snapshot app.world.send_event(SnapshotEvent::Take); app.update(); // Clear the snapshots for the next iteration app.world.resource_mut::<SnapshotRegistry>().snapshots.clear(); }); }); } fn snapshot_application_benchmark(c: &mut Criterion) { c.bench_function("apply snapshot with 100 entities", |b| { // Set up a test app let mut app = App::new(); app.add_plugins((MinimalPlugins, SnapshotPlugin)); // Add 100 test entities for i in 0..100 { app.world.spawn((Snapshotable, TestComponent { value: i })); } // Create a snapshot app.world.send_event(SnapshotEvent::Take); app.update(); // Get the snapshot ID let snapshot_id = app.world.resource::<SnapshotRegistry>() .most_recent().unwrap().id; b.iter(|| { // Apply the snapshot app.world.send_event(SnapshotEvent::Apply(snapshot_id)); app.update(); }); }); } criterion_group!( snapshot_benches, snapshot_creation_benchmark, snapshot_application_benchmark ); criterion_main!(snapshot_benches); }
Network Testing
Testing snapshot integration with networking:
#![allow(unused)] fn main() { #[test] fn test_network_snapshot_sync() { // Set up a server app let mut server_app = App::new(); server_app.add_plugins(( MinimalPlugins, RepliconServerPlugin, SnapshotPlugin, NetworkSnapshotPlugin, )); // Set up a client app let mut client_app = App::new(); client_app.add_plugins(( MinimalPlugins, RepliconClientPlugin, SnapshotPlugin, NetworkSnapshotPlugin, )); // Connect the client to the server let client_id = connect_client_to_server(&mut server_app, &mut client_app); // Set up game state on the server setup_test_game(&mut server_app); // Trigger a snapshot and network sync server_app.world.send_event(SnapshotEvent::Take); server_app.update(); // Run multiple updates to allow for network processing for _ in 0..10 { server_app.update(); client_app.update(); } // Verify the client received and applied the snapshot let client_state = client_app.world.resource::<GameState>(); let server_state = server_app.world.resource::<GameState>(); assert_eq!(client_state.turn, server_state.turn, "Client turn should match server"); assert_eq!(client_state.phase, server_state.phase, "Client phase should match server"); } }
Testing Deterministic RNG
Tests for RNG integration with snapshots:
#![allow(unused)] fn main() { #[test] fn test_rng_snapshot_determinism() { // Set up a test app let mut app = App::new(); app.add_plugins(( MinimalPlugins, SnapshotPlugin, DeterministicRNGPlugin, )); // Initialize RNG with a known seed app.insert_resource(NetworkedRngState { seed: 12345, usage_count: 0, }); // Create an RNG and generate some random numbers let mut rng = app.world.resource_mut::<NetworkedRngState>().create_rng(); let first_values: Vec<u32> = (0..10).map(|_| rng.gen::<u32>()).collect(); // Take a snapshot app.world.send_event(SnapshotEvent::Take); app.update(); // Get the snapshot ID let snapshot_id = app.world.resource::<SnapshotRegistry>() .most_recent().unwrap().id; // Generate more random numbers (changing the RNG state) let mut rng = app.world.resource_mut::<NetworkedRngState>().create_rng(); for _ in 0..20 { rng.gen::<u32>(); } // Apply the snapshot to restore the RNG state app.world.send_event(SnapshotEvent::Apply(snapshot_id)); app.update(); // Generate random numbers again from the restored state let mut rng = app.world.resource_mut::<NetworkedRngState>().create_rng(); let restored_values: Vec<u32> = (0..10).map(|_| rng.gen::<u32>()).collect(); // Verify the sequences are identical assert_eq!(first_values, restored_values, "RNG sequences should be identical after snapshot restoration"); } }
Test Fixtures
Creating reusable test fixtures:
#![allow(unused)] fn main() { /// Sets up a basic test environment for snapshot testing fn setup_snapshot_test_environment() -> App { let mut app = App::new(); app.add_plugins(( MinimalPlugins, SnapshotPlugin, )); // Add test entities app.world.spawn((Snapshotable, TestComponent { value: 1 })); app.world.spawn((Snapshotable, TestComponent { value: 2 })); app.world.spawn((Snapshotable, TestComponent { value: 3 })); // Add game state app.insert_resource(GameState { turn: 1, phase: Phase::Main1, active_player: 0, }); app } /// Creates a snapshot and returns the snapshot ID fn create_test_snapshot(app: &mut App) -> Uuid { app.world.send_event(SnapshotEvent::Take); app.update(); app.world.resource::<SnapshotRegistry>() .most_recent() .unwrap() .id } /// Test component for snapshot tests #[derive(Component, Clone, PartialEq, Debug, Serialize, Deserialize)] struct TestComponent { value: i32, } }
Mocking Dependencies
Using mocks for testing:
#![allow(unused)] fn main() { /// Mock game state for testing #[derive(Resource, Clone, Debug, Default)] struct MockGameState { turn: u32, phase: Phase, active_player: usize, } /// Mock event for testing auto-snapshots #[derive(Event)] struct MockTurnChangeEvent; /// Test that snapshots are triggered by events using mocks #[test] fn test_snapshot_event_triggers() { // Set up a test app let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_plugin(SnapshotPlugin) .add_event::<MockTurnChangeEvent>(); // Add a system that listens for MockTurnChangeEvent and triggers snapshots app.add_systems(Update, | mut turn_events: EventReader<MockTurnChangeEvent>, mut snapshot_events: EventWriter<SnapshotEvent>, | { for _ in turn_events.iter() { snapshot_events.send(SnapshotEvent::Take); } }); // Send a mock turn change event app.world.send_event(MockTurnChangeEvent); app.update(); // Verify a snapshot was created let snapshot_registry = app.world.resource::<SnapshotRegistry>(); assert_eq!(snapshot_registry.snapshots.len(), 1, "Should create a snapshot in response to the event"); } }
Best Practices
When testing the snapshot system:
- Isolate Tests: Each test should focus on a specific aspect of the snapshot system
- Use Test Fixtures: Create reusable setups for snapshot testing
- Test Error Handling: Verify behavior when snapshots fail or are corrupted
- Measure Performance: Track performance metrics for snapshot operations
- Test Edge Cases:
- Empty snapshots
- Very large snapshots
- Concurrent snapshot operations
- Snapshot application during state changes
- Test Integration Points: Verify all systems that interact with snapshots
- Use Mocks: Create mock implementations of dependencies for focused testing
Next Steps
- API Reference: Complete reference documentation for the snapshot system