1use crate::event::InputEvent;
17use crate::result::{ProbarError, ProbarResult};
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20use std::collections::HashMap;
21use std::fs;
22use std::path::Path;
23
24pub const REPLAY_FORMAT_VERSION: u32 = 1;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ReplayHeader {
30 pub version: u32,
32 pub game_name: String,
34 pub game_version: String,
36 pub created_at: u64,
38 pub seed: u64,
40 pub total_frames: u64,
42 pub fps: u32,
44 pub checksum: String,
46}
47
48impl ReplayHeader {
49 #[must_use]
51 pub fn new(game_name: &str, game_version: &str, seed: u64) -> Self {
52 Self {
53 version: REPLAY_FORMAT_VERSION,
54 game_name: game_name.to_string(),
55 game_version: game_version.to_string(),
56 created_at: std::time::SystemTime::now()
57 .duration_since(std::time::UNIX_EPOCH)
58 .unwrap_or_default()
59 .as_secs(),
60 seed,
61 total_frames: 0,
62 fps: 60,
63 checksum: String::new(),
64 }
65 }
66
67 #[must_use]
69 pub const fn with_fps(mut self, fps: u32) -> Self {
70 self.fps = fps;
71 self
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct TimedInput {
78 pub frame: u64,
80 pub event: InputEvent,
82}
83
84impl TimedInput {
85 #[must_use]
87 pub const fn new(frame: u64, event: InputEvent) -> Self {
88 Self { frame, event }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct StateCheckpoint {
95 pub frame: u64,
97 pub state_hash: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub state_data: Option<HashMap<String, serde_json::Value>>,
102}
103
104impl StateCheckpoint {
105 #[must_use]
107 pub fn new(frame: u64, state_hash: &str) -> Self {
108 Self {
109 frame,
110 state_hash: state_hash.to_string(),
111 state_data: None,
112 }
113 }
114
115 #[must_use]
117 pub fn with_data(
118 frame: u64,
119 state_hash: &str,
120 data: HashMap<String, serde_json::Value>,
121 ) -> Self {
122 Self {
123 frame,
124 state_hash: state_hash.to_string(),
125 state_data: Some(data),
126 }
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct Replay {
133 pub header: ReplayHeader,
135 pub inputs: Vec<TimedInput>,
137 pub checkpoints: Vec<StateCheckpoint>,
139 #[serde(default)]
141 pub metadata: HashMap<String, String>,
142}
143
144impl Replay {
145 #[must_use]
147 pub fn new(header: ReplayHeader) -> Self {
148 Self {
149 header,
150 inputs: Vec::new(),
151 checkpoints: Vec::new(),
152 metadata: HashMap::new(),
153 }
154 }
155
156 pub fn add_input(&mut self, frame: u64, event: InputEvent) {
158 self.inputs.push(TimedInput::new(frame, event));
159 self.header.total_frames = self.header.total_frames.max(frame + 1);
160 }
161
162 pub fn add_checkpoint(&mut self, checkpoint: StateCheckpoint) {
164 self.header.total_frames = self.header.total_frames.max(checkpoint.frame + 1);
165 self.checkpoints.push(checkpoint);
166 }
167
168 pub fn set_metadata(&mut self, key: &str, value: &str) {
170 self.metadata.insert(key.to_string(), value.to_string());
171 }
172
173 #[must_use]
175 pub fn inputs_at_frame(&self, frame: u64) -> Vec<&InputEvent> {
176 self.inputs
177 .iter()
178 .filter(|i| i.frame == frame)
179 .map(|i| &i.event)
180 .collect()
181 }
182
183 #[must_use]
185 pub fn checkpoint_at_or_before(&self, frame: u64) -> Option<&StateCheckpoint> {
186 self.checkpoints
187 .iter()
188 .filter(|c| c.frame <= frame)
189 .max_by_key(|c| c.frame)
190 }
191
192 #[must_use]
194 pub fn compute_checksum(&self) -> String {
195 let mut hasher = Sha256::new();
196
197 hasher.update(self.header.seed.to_le_bytes());
199 hasher.update(self.header.total_frames.to_le_bytes());
200 hasher.update(self.header.fps.to_le_bytes());
201
202 for input in &self.inputs {
204 hasher.update(input.frame.to_le_bytes());
205 hasher.update(format!("{:?}", input.event).as_bytes());
206 }
207
208 for checkpoint in &self.checkpoints {
210 hasher.update(checkpoint.frame.to_le_bytes());
211 hasher.update(checkpoint.state_hash.as_bytes());
212 }
213
214 let result = hasher.finalize();
215 format!("{result:x}")
216 }
217
218 pub fn finalize(&mut self) {
220 self.header.checksum = self.compute_checksum();
221 }
222
223 #[must_use]
225 pub fn verify_checksum(&self) -> bool {
226 let computed = self.compute_checksum();
228 computed == self.header.checksum
229 }
230
231 pub fn save_yaml(&self, path: &Path) -> ProbarResult<()> {
233 let yaml = serde_yaml_ng::to_string(self).map_err(|e| {
234 ProbarError::SnapshotSerializationError {
235 message: format!("Failed to serialize replay: {e}"),
236 }
237 })?;
238
239 if let Some(parent) = path.parent() {
240 fs::create_dir_all(parent)?;
241 }
242
243 fs::write(path, yaml)?;
244 Ok(())
245 }
246
247 pub fn load_yaml(path: &Path) -> ProbarResult<Self> {
249 let yaml = fs::read_to_string(path)?;
250 let replay: Replay = serde_yaml_ng::from_str(&yaml).map_err(|e| {
251 ProbarError::SnapshotSerializationError {
252 message: format!("Failed to deserialize replay: {e}"),
253 }
254 })?;
255 Ok(replay)
256 }
257
258 pub fn save_json(&self, path: &Path) -> ProbarResult<()> {
260 let json = serde_json::to_string_pretty(self)?;
261
262 if let Some(parent) = path.parent() {
263 fs::create_dir_all(parent)?;
264 }
265
266 fs::write(path, json)?;
267 Ok(())
268 }
269
270 pub fn load_json(path: &Path) -> ProbarResult<Self> {
272 let json = fs::read_to_string(path)?;
273 let replay: Replay = serde_json::from_str(&json)?;
274 Ok(replay)
275 }
276}
277
278#[derive(Debug)]
280pub struct ReplayRecorder {
281 replay: Replay,
283 current_frame: u64,
285 checkpoint_interval: u64,
287 recording: bool,
289}
290
291impl ReplayRecorder {
292 #[must_use]
294 pub fn new(game_name: &str, game_version: &str, seed: u64) -> Self {
295 let header = ReplayHeader::new(game_name, game_version, seed);
296 Self {
297 replay: Replay::new(header),
298 current_frame: 0,
299 checkpoint_interval: 60, recording: true,
301 }
302 }
303
304 #[must_use]
306 pub const fn with_checkpoint_interval(mut self, interval: u64) -> Self {
307 self.checkpoint_interval = interval;
308 self
309 }
310
311 #[must_use]
313 pub fn with_fps(mut self, fps: u32) -> Self {
314 self.replay.header = self.replay.header.with_fps(fps);
315 self
316 }
317
318 pub fn record_input(&mut self, event: InputEvent) {
320 if self.recording {
321 self.replay.add_input(self.current_frame, event);
322 }
323 }
324
325 pub fn record_inputs(&mut self, events: &[InputEvent]) {
327 for event in events {
328 self.record_input(event.clone());
329 }
330 }
331
332 pub fn next_frame(&mut self, state_hash: Option<&str>) {
334 self.current_frame += 1;
335
336 if let Some(hash) = state_hash {
338 if self.current_frame % self.checkpoint_interval == 0 {
339 self.replay
340 .add_checkpoint(StateCheckpoint::new(self.current_frame, hash));
341 }
342 }
343 }
344
345 pub fn checkpoint(&mut self, state_hash: &str) {
347 self.replay
348 .add_checkpoint(StateCheckpoint::new(self.current_frame, state_hash));
349 }
350
351 pub fn checkpoint_with_data(
353 &mut self,
354 state_hash: &str,
355 data: HashMap<String, serde_json::Value>,
356 ) {
357 self.replay.add_checkpoint(StateCheckpoint::with_data(
358 self.current_frame,
359 state_hash,
360 data,
361 ));
362 }
363
364 pub fn stop(&mut self) {
366 self.recording = false;
367 }
368
369 #[must_use]
371 pub fn finalize(mut self) -> Replay {
372 self.replay.finalize();
373 self.replay
374 }
375
376 #[must_use]
378 pub const fn current_frame(&self) -> u64 {
379 self.current_frame
380 }
381
382 #[must_use]
384 pub const fn is_recording(&self) -> bool {
385 self.recording
386 }
387}
388
389#[derive(Debug)]
391pub struct ReplayPlayer {
392 replay: Replay,
394 current_frame: u64,
396 speed: f64,
398 playing: bool,
400 input_index: usize,
402}
403
404impl ReplayPlayer {
405 #[must_use]
407 pub fn new(replay: Replay) -> Self {
408 Self {
409 replay,
410 current_frame: 0,
411 speed: 1.0,
412 playing: true,
413 input_index: 0,
414 }
415 }
416
417 #[must_use]
419 pub fn with_speed(mut self, speed: f64) -> Self {
420 self.speed = speed;
421 self
422 }
423
424 #[must_use]
426 pub fn get_frame_inputs(&mut self) -> Vec<InputEvent> {
427 if !self.playing {
428 return Vec::new();
429 }
430
431 let mut inputs = Vec::new();
432
433 while self.input_index < self.replay.inputs.len() {
435 let timed = &self.replay.inputs[self.input_index];
436 if timed.frame == self.current_frame {
437 inputs.push(timed.event.clone());
438 self.input_index += 1;
439 } else if timed.frame > self.current_frame {
440 break;
441 } else {
442 self.input_index += 1;
443 }
444 }
445
446 self.current_frame += 1;
447
448 if self.current_frame >= self.replay.header.total_frames {
450 self.playing = false;
451 }
452
453 inputs
454 }
455
456 #[must_use]
458 pub fn expected_checkpoint(&self) -> Option<&StateCheckpoint> {
459 self.replay
460 .checkpoints
461 .iter()
462 .find(|c| c.frame == self.current_frame - 1)
463 }
464
465 pub fn verify_state(&self, state_hash: &str) -> ProbarResult<()> {
467 if let Some(checkpoint) = self.expected_checkpoint() {
468 if checkpoint.state_hash != state_hash {
469 return Err(ProbarError::AssertionFailed {
470 message: format!(
471 "State divergence at frame {}: expected hash '{}', got '{}'",
472 checkpoint.frame, checkpoint.state_hash, state_hash
473 ),
474 });
475 }
476 }
477 Ok(())
478 }
479
480 #[must_use]
482 pub const fn current_frame(&self) -> u64 {
483 self.current_frame
484 }
485
486 #[must_use]
488 pub const fn is_playing(&self) -> bool {
489 self.playing
490 }
491
492 #[must_use]
494 pub const fn total_frames(&self) -> u64 {
495 self.replay.header.total_frames
496 }
497
498 #[must_use]
500 pub fn progress(&self) -> f64 {
501 if self.replay.header.total_frames == 0 {
502 return 1.0;
503 }
504 self.current_frame as f64 / self.replay.header.total_frames as f64
505 }
506
507 pub fn seek(&mut self, frame: u64) {
509 self.current_frame = frame.min(self.replay.header.total_frames);
510 self.playing = self.current_frame < self.replay.header.total_frames;
511
512 self.input_index = 0;
514 while self.input_index < self.replay.inputs.len()
515 && self.replay.inputs[self.input_index].frame < self.current_frame
516 {
517 self.input_index += 1;
518 }
519 }
520
521 pub fn pause(&mut self) {
523 self.playing = false;
524 }
525
526 pub fn resume(&mut self) {
528 if self.current_frame < self.replay.header.total_frames {
529 self.playing = true;
530 }
531 }
532
533 #[must_use]
535 pub fn replay(&self) -> &Replay {
536 &self.replay
537 }
538}
539
540#[derive(Debug, Clone)]
542pub struct VerificationResult {
543 pub passed: bool,
545 pub frames_verified: u64,
547 pub checkpoints_verified: usize,
549 pub divergence_frame: Option<u64>,
551 pub divergence_details: Option<String>,
553}
554
555impl VerificationResult {
556 #[must_use]
558 pub const fn success(frames_verified: u64, checkpoints_verified: usize) -> Self {
559 Self {
560 passed: true,
561 frames_verified,
562 checkpoints_verified,
563 divergence_frame: None,
564 divergence_details: None,
565 }
566 }
567
568 #[must_use]
570 pub fn failure(frame: u64, details: &str) -> Self {
571 Self {
572 passed: false,
573 frames_verified: frame,
574 checkpoints_verified: 0,
575 divergence_frame: Some(frame),
576 divergence_details: Some(details.to_string()),
577 }
578 }
579}
580
581#[cfg(test)]
582#[allow(clippy::unwrap_used, clippy::expect_used)]
583mod tests {
584 use super::*;
585
586 mod replay_header_tests {
587 use super::*;
588
589 #[test]
590 fn test_new() {
591 let header = ReplayHeader::new("test_game", "1.0.0", 42);
592 assert_eq!(header.game_name, "test_game");
593 assert_eq!(header.game_version, "1.0.0");
594 assert_eq!(header.seed, 42);
595 assert_eq!(header.version, REPLAY_FORMAT_VERSION);
596 }
597
598 #[test]
599 fn test_with_fps() {
600 let header = ReplayHeader::new("game", "1.0", 0).with_fps(30);
601 assert_eq!(header.fps, 30);
602 }
603 }
604
605 mod timed_input_tests {
606 use super::*;
607
608 #[test]
609 fn test_new() {
610 let event = InputEvent::key_press("Space");
611 let timed = TimedInput::new(100, event);
612 assert_eq!(timed.frame, 100);
613 }
614 }
615
616 mod state_checkpoint_tests {
617 use super::*;
618
619 #[test]
620 fn test_new() {
621 let cp = StateCheckpoint::new(50, "abc123");
622 assert_eq!(cp.frame, 50);
623 assert_eq!(cp.state_hash, "abc123");
624 assert!(cp.state_data.is_none());
625 }
626
627 #[test]
628 fn test_with_data() {
629 let mut data = HashMap::new();
630 data.insert("score".to_string(), serde_json::json!(100));
631 let cp = StateCheckpoint::with_data(50, "abc123", data);
632 assert!(cp.state_data.is_some());
633 }
634 }
635
636 mod replay_tests {
637 use super::*;
638
639 #[test]
640 fn test_new() {
641 let header = ReplayHeader::new("game", "1.0", 42);
642 let replay = Replay::new(header);
643 assert!(replay.inputs.is_empty());
644 assert!(replay.checkpoints.is_empty());
645 }
646
647 #[test]
648 fn test_add_input() {
649 let header = ReplayHeader::new("game", "1.0", 42);
650 let mut replay = Replay::new(header);
651
652 replay.add_input(0, InputEvent::key_press("A"));
653 replay.add_input(10, InputEvent::key_press("B"));
654
655 assert_eq!(replay.inputs.len(), 2);
656 assert_eq!(replay.header.total_frames, 11);
657 }
658
659 #[test]
660 fn test_inputs_at_frame() {
661 let header = ReplayHeader::new("game", "1.0", 42);
662 let mut replay = Replay::new(header);
663
664 replay.add_input(5, InputEvent::key_press("A"));
665 replay.add_input(5, InputEvent::key_press("B"));
666 replay.add_input(10, InputEvent::key_press("C"));
667
668 let inputs = replay.inputs_at_frame(5);
669 assert_eq!(inputs.len(), 2);
670 }
671
672 #[test]
673 fn test_add_checkpoint() {
674 let header = ReplayHeader::new("game", "1.0", 42);
675 let mut replay = Replay::new(header);
676
677 replay.add_checkpoint(StateCheckpoint::new(60, "hash1"));
678 replay.add_checkpoint(StateCheckpoint::new(120, "hash2"));
679
680 assert_eq!(replay.checkpoints.len(), 2);
681 }
682
683 #[test]
684 fn test_checkpoint_at_or_before() {
685 let header = ReplayHeader::new("game", "1.0", 42);
686 let mut replay = Replay::new(header);
687
688 replay.add_checkpoint(StateCheckpoint::new(60, "hash1"));
689 replay.add_checkpoint(StateCheckpoint::new(120, "hash2"));
690
691 let cp = replay.checkpoint_at_or_before(100);
692 assert!(cp.is_some());
693 assert_eq!(cp.unwrap().frame, 60);
694
695 let cp = replay.checkpoint_at_or_before(120);
696 assert!(cp.is_some());
697 assert_eq!(cp.unwrap().frame, 120);
698
699 let cp = replay.checkpoint_at_or_before(50);
700 assert!(cp.is_none());
701 }
702
703 #[test]
704 fn test_metadata() {
705 let header = ReplayHeader::new("game", "1.0", 42);
706 let mut replay = Replay::new(header);
707
708 replay.set_metadata("player", "Alice");
709 replay.set_metadata("difficulty", "hard");
710
711 assert_eq!(replay.metadata.get("player"), Some(&"Alice".to_string()));
712 }
713
714 #[test]
715 fn test_compute_checksum() {
716 let header = ReplayHeader::new("game", "1.0", 42);
717 let mut replay1 = Replay::new(header.clone());
718 let mut replay2 = Replay::new(header);
719
720 replay1.add_input(0, InputEvent::key_press("A"));
721 replay2.add_input(0, InputEvent::key_press("A"));
722
723 assert_eq!(replay1.compute_checksum(), replay2.compute_checksum());
724
725 replay2.add_input(1, InputEvent::key_press("B"));
726 assert_ne!(replay1.compute_checksum(), replay2.compute_checksum());
727 }
728
729 #[test]
730 fn test_finalize_and_verify() {
731 let header = ReplayHeader::new("game", "1.0", 42);
732 let mut replay = Replay::new(header);
733 replay.add_input(0, InputEvent::key_press("A"));
734 replay.finalize();
735
736 assert!(!replay.header.checksum.is_empty());
737 assert!(replay.verify_checksum());
738 }
739 }
740
741 mod replay_recorder_tests {
742 use super::*;
743
744 #[test]
745 fn test_new() {
746 let recorder = ReplayRecorder::new("game", "1.0", 42);
747 assert_eq!(recorder.current_frame(), 0);
748 assert!(recorder.is_recording());
749 }
750
751 #[test]
752 fn test_record_input() {
753 let mut recorder = ReplayRecorder::new("game", "1.0", 42);
754 recorder.record_input(InputEvent::key_press("A"));
755 recorder.next_frame(None);
756 recorder.record_input(InputEvent::key_press("B"));
757
758 let replay = recorder.finalize();
759 assert_eq!(replay.inputs.len(), 2);
760 }
761
762 #[test]
763 fn test_checkpoint() {
764 let mut recorder = ReplayRecorder::new("game", "1.0", 42).with_checkpoint_interval(10);
765
766 for i in 0..25 {
767 recorder.next_frame(Some(&format!("hash{}", i)));
768 }
769
770 let replay = recorder.finalize();
771 assert_eq!(replay.checkpoints.len(), 2);
773 }
774
775 #[test]
776 fn test_stop_recording() {
777 let mut recorder = ReplayRecorder::new("game", "1.0", 42);
778 recorder.record_input(InputEvent::key_press("A"));
779 recorder.stop();
780 recorder.record_input(InputEvent::key_press("B"));
781
782 let replay = recorder.finalize();
783 assert_eq!(replay.inputs.len(), 1); }
785 }
786
787 mod replay_player_tests {
788 use super::*;
789
790 fn create_test_replay() -> Replay {
791 let header = ReplayHeader::new("game", "1.0", 42);
792 let mut replay = Replay::new(header);
793
794 replay.add_input(0, InputEvent::key_press("A"));
795 replay.add_input(5, InputEvent::key_press("B"));
796 replay.add_input(5, InputEvent::key_press("C"));
797 replay.add_input(10, InputEvent::key_press("D"));
798 replay.add_checkpoint(StateCheckpoint::new(5, "hash5"));
799 replay.header.total_frames = 15;
800 replay
801 }
802
803 #[test]
804 fn test_new() {
805 let replay = create_test_replay();
806 let player = ReplayPlayer::new(replay);
807
808 assert_eq!(player.current_frame(), 0);
809 assert!(player.is_playing());
810 }
811
812 #[test]
813 fn test_get_frame_inputs() {
814 let replay = create_test_replay();
815 let mut player = ReplayPlayer::new(replay);
816
817 let inputs = player.get_frame_inputs();
819 assert_eq!(inputs.len(), 1);
820 assert_eq!(player.current_frame(), 1);
821
822 for _ in 1..5 {
824 let inputs = player.get_frame_inputs();
825 assert!(inputs.is_empty());
826 }
827
828 let inputs = player.get_frame_inputs();
830 assert_eq!(inputs.len(), 2);
831 }
832
833 #[test]
834 fn test_progress() {
835 let replay = create_test_replay();
836 let mut player = ReplayPlayer::new(replay);
837
838 assert!((player.progress() - 0.0).abs() < f64::EPSILON);
839
840 for _ in 0..7 {
841 let _ = player.get_frame_inputs();
842 }
843
844 assert!((player.progress() - 7.0 / 15.0).abs() < 0.01);
846 }
847
848 #[test]
849 fn test_seek() {
850 let replay = create_test_replay();
851 let mut player = ReplayPlayer::new(replay);
852
853 player.seek(10);
854 assert_eq!(player.current_frame(), 10);
855
856 let inputs = player.get_frame_inputs();
858 assert_eq!(inputs.len(), 1);
859 }
860
861 #[test]
862 fn test_pause_resume() {
863 let replay = create_test_replay();
864 let mut player = ReplayPlayer::new(replay);
865
866 player.pause();
867 assert!(!player.is_playing());
868
869 let inputs = player.get_frame_inputs();
870 assert!(inputs.is_empty());
871 assert_eq!(player.current_frame(), 0); player.resume();
874 assert!(player.is_playing());
875 }
876
877 #[test]
878 fn test_playback_completion() {
879 let replay = create_test_replay();
880 let mut player = ReplayPlayer::new(replay);
881
882 while player.is_playing() {
884 let _ = player.get_frame_inputs();
885 }
886
887 assert_eq!(player.current_frame(), 15);
888 assert!(!player.is_playing());
889 }
890
891 #[test]
892 fn test_verify_state_pass() {
893 let replay = create_test_replay();
894 let mut player = ReplayPlayer::new(replay);
895
896 for _ in 0..6 {
898 let _ = player.get_frame_inputs();
899 }
900
901 assert!(player.verify_state("hash5").is_ok());
903 }
904
905 #[test]
906 fn test_verify_state_fail() {
907 let replay = create_test_replay();
908 let mut player = ReplayPlayer::new(replay);
909
910 for _ in 0..6 {
912 let _ = player.get_frame_inputs();
913 }
914
915 assert!(player.verify_state("wrong_hash").is_err());
917 }
918 }
919
920 mod file_io_tests {
921 use super::*;
922 use tempfile::TempDir;
923
924 #[test]
925 fn test_save_and_load_yaml() {
926 let temp_dir = TempDir::new().unwrap();
927 let path = temp_dir.path().join("replay.yaml");
928
929 let header = ReplayHeader::new("game", "1.0", 42);
930 let mut replay = Replay::new(header);
931 replay.add_input(0, InputEvent::key_press("A"));
932 replay.finalize();
933
934 replay.save_yaml(&path).unwrap();
935 assert!(path.exists());
936
937 let loaded = Replay::load_yaml(&path).unwrap();
938 assert_eq!(loaded.header.seed, 42);
939 assert_eq!(loaded.inputs.len(), 1);
940 assert!(loaded.verify_checksum());
941 }
942
943 #[test]
944 fn test_save_and_load_json() {
945 let temp_dir = TempDir::new().unwrap();
946 let path = temp_dir.path().join("replay.json");
947
948 let header = ReplayHeader::new("game", "1.0", 42);
949 let mut replay = Replay::new(header);
950 replay.add_input(0, InputEvent::key_press("A"));
951 replay.finalize();
952
953 replay.save_json(&path).unwrap();
954 assert!(path.exists());
955
956 let loaded = Replay::load_json(&path).unwrap();
957 assert_eq!(loaded.header.seed, 42);
958 assert!(loaded.verify_checksum());
959 }
960 }
961
962 mod verification_result_tests {
963 use super::*;
964
965 #[test]
966 fn test_success() {
967 let result = VerificationResult::success(100, 5);
968 assert!(result.passed);
969 assert_eq!(result.frames_verified, 100);
970 assert_eq!(result.checkpoints_verified, 5);
971 assert!(result.divergence_frame.is_none());
972 assert!(result.divergence_details.is_none());
973 }
974
975 #[test]
976 fn test_failure() {
977 let result = VerificationResult::failure(50, "State mismatch");
978 assert!(!result.passed);
979 assert_eq!(result.frames_verified, 50);
980 assert_eq!(result.divergence_frame, Some(50));
981 assert!(result
982 .divergence_details
983 .as_ref()
984 .unwrap()
985 .contains("State mismatch"));
986 }
987 }
988
989 mod additional_replay_tests {
990 use super::*;
991
992 #[test]
993 fn test_replay_header_default_fps() {
994 let header = ReplayHeader::new("game", "1.0", 0);
995 assert_eq!(header.fps, 60);
996 }
997
998 #[test]
999 fn test_replay_header_created_at() {
1000 let header = ReplayHeader::new("game", "1.0", 0);
1001 assert!(header.created_at > 0);
1002 }
1003
1004 #[test]
1005 fn test_replay_header_checksum_empty() {
1006 let header = ReplayHeader::new("game", "1.0", 0);
1007 assert!(header.checksum.is_empty());
1008 }
1009
1010 #[test]
1011 fn test_timed_input_event_types() {
1012 let events = vec![
1013 InputEvent::key_press("A"),
1014 InputEvent::key_release("B"),
1015 InputEvent::mouse_click(100.0, 200.0),
1016 ];
1017
1018 for (i, event) in events.into_iter().enumerate() {
1019 let timed = TimedInput::new(i as u64, event);
1020 assert_eq!(timed.frame, i as u64);
1021 }
1022 }
1023
1024 #[test]
1025 fn test_replay_inputs_at_frame_none() {
1026 let header = ReplayHeader::new("game", "1.0", 0);
1027 let replay = Replay::new(header);
1028 let inputs = replay.inputs_at_frame(0);
1029 assert!(inputs.is_empty());
1030 }
1031
1032 #[test]
1033 fn test_replay_checkpoint_none() {
1034 let header = ReplayHeader::new("game", "1.0", 0);
1035 let replay = Replay::new(header);
1036 assert!(replay.checkpoint_at_or_before(0).is_none());
1037 }
1038
1039 #[test]
1040 fn test_replay_verify_checksum_mismatch() {
1041 let header = ReplayHeader::new("game", "1.0", 42);
1042 let mut replay = Replay::new(header);
1043 replay.add_input(0, InputEvent::key_press("A"));
1044 replay.finalize();
1045
1046 replay.header.checksum = "invalid".to_string();
1048 assert!(!replay.verify_checksum());
1049 }
1050 }
1051
1052 mod additional_recorder_tests {
1053 use super::*;
1054
1055 #[test]
1056 fn test_recorder_record_inputs() {
1057 let mut recorder = ReplayRecorder::new("game", "1.0", 42);
1058 let events = vec![
1059 InputEvent::key_press("A"),
1060 InputEvent::key_press("B"),
1061 InputEvent::key_press("C"),
1062 ];
1063 recorder.record_inputs(&events);
1064
1065 let replay = recorder.finalize();
1066 assert_eq!(replay.inputs.len(), 3);
1067 }
1068
1069 #[test]
1070 fn test_recorder_checkpoint() {
1071 let mut recorder = ReplayRecorder::new("game", "1.0", 42);
1072 recorder.checkpoint("hash_at_0");
1073 recorder.next_frame(None);
1074 recorder.checkpoint("hash_at_1");
1075
1076 let replay = recorder.finalize();
1077 assert_eq!(replay.checkpoints.len(), 2);
1078 }
1079
1080 #[test]
1081 fn test_recorder_checkpoint_with_data() {
1082 let mut recorder = ReplayRecorder::new("game", "1.0", 42);
1083 let mut data = HashMap::new();
1084 data.insert("score".to_string(), serde_json::json!(100));
1085 data.insert("level".to_string(), serde_json::json!(5));
1086 recorder.checkpoint_with_data("hash", data);
1087
1088 let replay = recorder.finalize();
1089 assert_eq!(replay.checkpoints.len(), 1);
1090 assert!(replay.checkpoints[0].state_data.is_some());
1091 }
1092
1093 #[test]
1094 fn test_recorder_with_fps() {
1095 let recorder = ReplayRecorder::new("game", "1.0", 42).with_fps(30);
1096 let replay = recorder.finalize();
1097 assert_eq!(replay.header.fps, 30);
1098 }
1099
1100 #[test]
1101 fn test_recorder_not_recording() {
1102 let mut recorder = ReplayRecorder::new("game", "1.0", 42);
1103 recorder.stop();
1104 assert!(!recorder.is_recording());
1105
1106 recorder.record_input(InputEvent::key_press("A"));
1108 let replay = recorder.finalize();
1109 assert!(replay.inputs.is_empty());
1110 }
1111 }
1112
1113 mod additional_player_tests {
1114 use super::*;
1115
1116 fn create_simple_replay(total_frames: u64) -> Replay {
1117 let header = ReplayHeader::new("game", "1.0", 42);
1118 let mut replay = Replay::new(header);
1119 replay.header.total_frames = total_frames;
1120 replay
1121 }
1122
1123 #[test]
1124 fn test_player_with_speed() {
1125 let replay = create_simple_replay(100);
1126 let player = ReplayPlayer::new(replay).with_speed(2.0);
1127 assert!((player.speed - 2.0).abs() < f64::EPSILON);
1128 }
1129
1130 #[test]
1131 fn test_player_total_frames() {
1132 let replay = create_simple_replay(100);
1133 let player = ReplayPlayer::new(replay);
1134 assert_eq!(player.total_frames(), 100);
1135 }
1136
1137 #[test]
1138 fn test_player_progress_zero_frames() {
1139 let replay = create_simple_replay(0);
1140 let player = ReplayPlayer::new(replay);
1141 assert!((player.progress() - 1.0).abs() < f64::EPSILON);
1142 }
1143
1144 #[test]
1145 fn test_player_seek_beyond_total() {
1146 let replay = create_simple_replay(50);
1147 let mut player = ReplayPlayer::new(replay);
1148 player.seek(100);
1149 assert_eq!(player.current_frame(), 50);
1150 assert!(!player.is_playing());
1151 }
1152
1153 #[test]
1154 fn test_player_resume_at_end() {
1155 let replay = create_simple_replay(10);
1156 let mut player = ReplayPlayer::new(replay);
1157
1158 while player.is_playing() {
1160 let _ = player.get_frame_inputs();
1161 }
1162
1163 player.resume();
1164 assert!(!player.is_playing());
1166 }
1167
1168 #[test]
1169 fn test_player_replay_accessor() {
1170 let replay = create_simple_replay(50);
1171 let player = ReplayPlayer::new(replay);
1172 let accessed_replay = player.replay();
1173 assert_eq!(accessed_replay.header.total_frames, 50);
1174 }
1175
1176 #[test]
1177 fn test_player_expected_checkpoint_none() {
1178 let replay = create_simple_replay(50);
1179 let player = ReplayPlayer::new(replay);
1180 assert!(player.expected_checkpoint().is_none());
1182 }
1183
1184 #[test]
1185 fn test_player_verify_state_no_checkpoint() {
1186 let replay = create_simple_replay(50);
1187 let player = ReplayPlayer::new(replay);
1188 assert!(player.verify_state("any_hash").is_ok());
1190 }
1191
1192 #[test]
1193 fn test_player_inputs_with_gaps() {
1194 let header = ReplayHeader::new("game", "1.0", 42);
1195 let mut replay = Replay::new(header);
1196
1197 replay.add_input(0, InputEvent::key_press("A"));
1199 replay.add_input(10, InputEvent::key_press("B"));
1200 replay.add_input(20, InputEvent::key_press("C"));
1201 replay.header.total_frames = 25;
1202
1203 let mut player = ReplayPlayer::new(replay);
1204
1205 let inputs = player.get_frame_inputs();
1207 assert_eq!(inputs.len(), 1);
1208
1209 for _ in 1..10 {
1211 let inputs = player.get_frame_inputs();
1212 assert!(inputs.is_empty());
1213 }
1214
1215 let inputs = player.get_frame_inputs();
1217 assert_eq!(inputs.len(), 1);
1218 }
1219
1220 #[test]
1221 fn test_player_seek_updates_input_index() {
1222 let header = ReplayHeader::new("game", "1.0", 42);
1223 let mut replay = Replay::new(header);
1224
1225 replay.add_input(0, InputEvent::key_press("A"));
1226 replay.add_input(5, InputEvent::key_press("B"));
1227 replay.add_input(10, InputEvent::key_press("C"));
1228 replay.header.total_frames = 15;
1229
1230 let mut player = ReplayPlayer::new(replay);
1231
1232 player.seek(7);
1234
1235 let inputs = player.get_frame_inputs();
1237 assert!(inputs.is_empty());
1238
1239 let _ = player.get_frame_inputs(); let _ = player.get_frame_inputs(); let inputs = player.get_frame_inputs(); assert_eq!(inputs.len(), 1);
1244 }
1245 }
1246
1247 mod additional_file_io_tests {
1248 use super::*;
1249 use tempfile::TempDir;
1250
1251 #[test]
1252 fn test_save_yaml_creates_parent_dirs() {
1253 let temp_dir = TempDir::new().unwrap();
1254 let path = temp_dir
1255 .path()
1256 .join("subdir")
1257 .join("deep")
1258 .join("replay.yaml");
1259
1260 let header = ReplayHeader::new("game", "1.0", 42);
1261 let replay = Replay::new(header);
1262
1263 replay.save_yaml(&path).unwrap();
1264 assert!(path.exists());
1265 }
1266
1267 #[test]
1268 fn test_save_json_creates_parent_dirs() {
1269 let temp_dir = TempDir::new().unwrap();
1270 let path = temp_dir
1271 .path()
1272 .join("subdir")
1273 .join("deep")
1274 .join("replay.json");
1275
1276 let header = ReplayHeader::new("game", "1.0", 42);
1277 let replay = Replay::new(header);
1278
1279 replay.save_json(&path).unwrap();
1280 assert!(path.exists());
1281 }
1282
1283 #[test]
1284 fn test_load_yaml_not_found() {
1285 let result = Replay::load_yaml(Path::new("/nonexistent/replay.yaml"));
1286 assert!(result.is_err());
1287 }
1288
1289 #[test]
1290 fn test_load_json_not_found() {
1291 let result = Replay::load_json(Path::new("/nonexistent/replay.json"));
1292 assert!(result.is_err());
1293 }
1294
1295 #[test]
1296 fn test_load_yaml_invalid() {
1297 let temp_dir = TempDir::new().unwrap();
1298 let path = temp_dir.path().join("invalid.yaml");
1299 std::fs::write(&path, "not: valid: yaml: {{{").unwrap();
1300
1301 let result = Replay::load_yaml(&path);
1302 assert!(result.is_err());
1303 }
1304
1305 #[test]
1306 fn test_load_json_invalid() {
1307 let temp_dir = TempDir::new().unwrap();
1308 let path = temp_dir.path().join("invalid.json");
1309 std::fs::write(&path, "not valid json {{{").unwrap();
1310
1311 let result = Replay::load_json(&path);
1312 assert!(result.is_err());
1313 }
1314
1315 #[test]
1316 fn test_replay_with_metadata_yaml() {
1317 let temp_dir = TempDir::new().unwrap();
1318 let path = temp_dir.path().join("replay.yaml");
1319
1320 let header = ReplayHeader::new("game", "1.0", 42);
1321 let mut replay = Replay::new(header);
1322 replay.set_metadata("player", "Alice");
1323 replay.set_metadata("score", "1000");
1324 replay.finalize();
1325
1326 replay.save_yaml(&path).unwrap();
1327 let loaded = Replay::load_yaml(&path).unwrap();
1328
1329 assert_eq!(loaded.metadata.get("player"), Some(&"Alice".to_string()));
1330 assert_eq!(loaded.metadata.get("score"), Some(&"1000".to_string()));
1331 }
1332
1333 #[test]
1334 fn test_replay_with_checkpoints_json() {
1335 let temp_dir = TempDir::new().unwrap();
1336 let path = temp_dir.path().join("replay.json");
1337
1338 let header = ReplayHeader::new("game", "1.0", 42);
1339 let mut replay = Replay::new(header);
1340
1341 let mut data = HashMap::new();
1342 data.insert("level".to_string(), serde_json::json!(3));
1343 replay.add_checkpoint(StateCheckpoint::with_data(60, "hash1", data));
1344
1345 replay.finalize();
1346 replay.save_json(&path).unwrap();
1347
1348 let loaded = Replay::load_json(&path).unwrap();
1349 assert_eq!(loaded.checkpoints.len(), 1);
1350 assert!(loaded.checkpoints[0].state_data.is_some());
1351 }
1352 }
1353
1354 mod additional_edge_case_tests {
1355 use super::*;
1356
1357 #[test]
1358 fn test_replay_format_version() {
1359 assert_eq!(REPLAY_FORMAT_VERSION, 1);
1360 }
1361
1362 #[test]
1363 fn test_state_checkpoint_without_data() {
1364 let cp = StateCheckpoint::new(100, "hash");
1365 assert!(cp.state_data.is_none());
1366 assert_eq!(cp.frame, 100);
1367 assert_eq!(cp.state_hash, "hash");
1368 }
1369
1370 #[test]
1371 fn test_replay_checksum_with_checkpoints() {
1372 let header = ReplayHeader::new("game", "1.0", 42);
1373 let mut replay = Replay::new(header);
1374
1375 replay.add_checkpoint(StateCheckpoint::new(10, "hash1"));
1376 replay.add_checkpoint(StateCheckpoint::new(20, "hash2"));
1377
1378 let checksum1 = replay.compute_checksum();
1379
1380 replay.add_checkpoint(StateCheckpoint::new(30, "hash3"));
1381 let checksum2 = replay.compute_checksum();
1382
1383 assert_ne!(checksum1, checksum2);
1384 }
1385
1386 #[test]
1387 fn test_replay_total_frames_from_checkpoint() {
1388 let header = ReplayHeader::new("game", "1.0", 42);
1389 let mut replay = Replay::new(header);
1390
1391 assert_eq!(replay.header.total_frames, 0);
1392
1393 replay.add_checkpoint(StateCheckpoint::new(100, "hash"));
1394 assert_eq!(replay.header.total_frames, 101);
1395 }
1396
1397 #[test]
1398 fn test_replay_total_frames_from_input() {
1399 let header = ReplayHeader::new("game", "1.0", 42);
1400 let mut replay = Replay::new(header);
1401
1402 replay.add_input(50, InputEvent::key_press("A"));
1403 assert_eq!(replay.header.total_frames, 51);
1404 }
1405
1406 #[test]
1407 fn test_player_skipped_inputs() {
1408 let header = ReplayHeader::new("game", "1.0", 42);
1409 let mut replay = Replay::new(header);
1410
1411 replay.add_input(0, InputEvent::key_press("A"));
1413 replay.header.total_frames = 5;
1414
1415 let mut player = ReplayPlayer::new(replay);
1416
1417 player.seek(3);
1419
1420 let inputs = player.get_frame_inputs();
1422 assert!(inputs.is_empty());
1423 }
1424 }
1425}