1use crate::types::{RegionId, Severity, TaskId, Time};
49use serde::{Deserialize, Serialize};
50use std::io;
51
52pub const REPLAY_SCHEMA_VERSION: u32 = 1;
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66pub struct TraceMetadata {
67 pub version: u32,
69
70 pub seed: u64,
72
73 pub recorded_at: u64,
75
76 pub config_hash: u64,
80
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub description: Option<String>,
84}
85
86impl TraceMetadata {
87 #[must_use]
89 pub fn new(seed: u64) -> Self {
90 Self {
91 version: REPLAY_SCHEMA_VERSION,
92 seed,
93 recorded_at: std::time::SystemTime::now()
94 .duration_since(std::time::UNIX_EPOCH)
95 .map(|d| d.as_nanos() as u64)
96 .unwrap_or(0),
97 config_hash: 0,
98 description: None,
99 }
100 }
101
102 #[must_use]
104 pub const fn with_config_hash(mut self, hash: u64) -> Self {
105 self.config_hash = hash;
106 self
107 }
108
109 #[must_use]
111 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
112 self.description = Some(desc.into());
113 self
114 }
115
116 #[must_use]
118 pub fn is_compatible(&self) -> bool {
119 self.version == REPLAY_SCHEMA_VERSION
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132#[repr(transparent)]
133pub struct CompactTaskId(pub u64);
134
135impl From<TaskId> for CompactTaskId {
136 fn from(id: TaskId) -> Self {
137 let idx = id.arena_index();
138 let packed = (u64::from(idx.index()) << 32) | u64::from(idx.generation());
139 Self(packed)
140 }
141}
142
143impl CompactTaskId {
144 #[must_use]
146 pub const fn unpack(self) -> (u32, u32) {
147 let index = (self.0 >> 32) as u32;
148 let generation = self.0 as u32;
149 (index, generation)
150 }
151
152 #[cfg(any(test, feature = "test-internals"))]
154 #[must_use]
155 pub fn to_task_id(self) -> TaskId {
156 let (index, generation) = self.unpack();
157 TaskId::new_for_test(index, generation)
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[repr(transparent)]
164pub struct CompactRegionId(pub u64);
165
166impl From<RegionId> for CompactRegionId {
167 fn from(id: RegionId) -> Self {
168 let idx = id.arena_index();
169 let packed = (u64::from(idx.index()) << 32) | u64::from(idx.generation());
170 Self(packed)
171 }
172}
173
174impl CompactRegionId {
175 #[must_use]
177 pub const fn unpack(self) -> (u32, u32) {
178 let index = (self.0 >> 32) as u32;
179 let generation = self.0 as u32;
180 (index, generation)
181 }
182
183 #[cfg(any(test, feature = "test-internals"))]
185 #[must_use]
186 pub fn to_region_id(self) -> RegionId {
187 let (index, generation) = self.unpack();
188 RegionId::new_for_test(index, generation)
189 }
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
209#[serde(tag = "type")]
210pub enum ReplayEvent {
211 TaskScheduled {
218 task: CompactTaskId,
220 at_tick: u64,
222 },
223
224 TaskYielded {
226 task: CompactTaskId,
228 },
229
230 TaskCompleted {
232 task: CompactTaskId,
234 outcome: u8,
236 },
237
238 TaskSpawned {
240 task: CompactTaskId,
242 region: CompactRegionId,
244 at_tick: u64,
246 },
247
248 TimeAdvanced {
253 from_nanos: u64,
255 to_nanos: u64,
257 },
258
259 TimerCreated {
261 timer_id: u64,
263 deadline_nanos: u64,
265 },
266
267 TimerFired {
269 timer_id: u64,
271 },
272
273 TimerCancelled {
275 timer_id: u64,
277 },
278
279 IoReady {
284 token: u64,
286 readiness: u8,
288 },
289
290 IoResult {
292 token: u64,
294 bytes: i64,
296 },
297
298 IoError {
300 token: u64,
302 kind: u8,
304 },
305
306 RngSeed {
311 seed: u64,
313 },
314
315 RngValue {
317 value: u64,
319 },
320
321 ChaosInjection {
326 kind: u8,
328 task: Option<CompactTaskId>,
330 data: u64,
332 },
333
334 RegionCreated {
342 region: CompactRegionId,
344 parent: Option<CompactRegionId>,
346 at_tick: u64,
348 },
349
350 RegionClosed {
354 region: CompactRegionId,
356 outcome: u8,
358 },
359
360 RegionCancelled {
364 region: CompactRegionId,
366 cancel_kind: u8,
368 },
369
370 WakerWake {
375 task: CompactTaskId,
377 },
378
379 WakerBatchWake {
381 count: u32,
383 },
384
385 Checkpoint {
395 sequence: u64,
397 time_nanos: u64,
399 active_tasks: u32,
401 active_regions: u32,
403 },
404}
405
406impl ReplayEvent {
407 #[must_use]
412 pub const fn estimated_size(&self) -> usize {
413 match self {
414 Self::TaskYielded { .. }
415 | Self::TimerFired { .. }
416 | Self::TimerCancelled { .. }
417 | Self::RngSeed { .. }
418 | Self::RngValue { .. }
419 | Self::WakerWake { .. } => 9, Self::TaskCompleted { .. }
421 | Self::IoReady { .. }
422 | Self::IoError { .. }
423 | Self::RegionClosed { .. }
424 | Self::RegionCancelled { .. } => 10, Self::TaskScheduled { .. }
426 | Self::TimeAdvanced { .. }
427 | Self::TimerCreated { .. }
428 | Self::IoResult { .. }
429 | Self::RegionCreated { parent: None, .. } => 17, Self::TaskSpawned { .. }
431 | Self::RegionCreated {
432 parent: Some(_), ..
433 }
434 | Self::Checkpoint { .. } => 25, Self::ChaosInjection { task: None, .. } => 11, Self::ChaosInjection { task: Some(_), .. } => 19, Self::WakerBatchWake { .. } => 5, }
439 }
440
441 #[must_use]
443 pub fn task_scheduled(task: impl Into<CompactTaskId>, at_tick: u64) -> Self {
444 Self::TaskScheduled {
445 task: task.into(),
446 at_tick,
447 }
448 }
449
450 #[must_use]
452 pub fn task_completed(task: impl Into<CompactTaskId>, severity: Severity) -> Self {
453 Self::TaskCompleted {
454 task: task.into(),
455 outcome: severity.as_u8(),
456 }
457 }
458
459 #[must_use]
461 pub fn time_advanced(from: Time, to: Time) -> Self {
462 Self::TimeAdvanced {
463 from_nanos: from.as_nanos(),
464 to_nanos: to.as_nanos(),
465 }
466 }
467
468 #[must_use]
470 #[allow(clippy::fn_params_excessive_bools)]
471 pub fn io_ready(token: u64, readable: bool, writable: bool, error: bool, hangup: bool) -> Self {
472 let mut readiness = 0u8;
473 if readable {
474 readiness |= 1;
475 }
476 if writable {
477 readiness |= 2;
478 }
479 if error {
480 readiness |= 4;
481 }
482 if hangup {
483 readiness |= 8;
484 }
485 Self::IoReady { token, readiness }
486 }
487
488 #[must_use]
490 pub fn io_error(token: u64, kind: io::ErrorKind) -> Self {
491 Self::IoError {
492 token,
493 kind: error_kind_to_u8(kind),
494 }
495 }
496
497 #[must_use]
499 pub fn region_created(
500 region: impl Into<CompactRegionId>,
501 parent: Option<impl Into<CompactRegionId>>,
502 at_tick: u64,
503 ) -> Self {
504 Self::RegionCreated {
505 region: region.into(),
506 parent: parent.map(Into::into),
507 at_tick,
508 }
509 }
510
511 #[must_use]
513 pub fn region_closed(region: impl Into<CompactRegionId>, severity: Severity) -> Self {
514 Self::RegionClosed {
515 region: region.into(),
516 outcome: severity.as_u8(),
517 }
518 }
519
520 #[must_use]
522 pub fn region_cancelled(region: impl Into<CompactRegionId>, cancel_kind: u8) -> Self {
523 Self::RegionCancelled {
524 region: region.into(),
525 cancel_kind,
526 }
527 }
528
529 #[must_use]
531 pub fn checkpoint(
532 sequence: u64,
533 time_nanos: u64,
534 active_tasks: u32,
535 active_regions: u32,
536 ) -> Self {
537 Self::Checkpoint {
538 sequence,
539 time_nanos,
540 active_tasks,
541 active_regions,
542 }
543 }
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct ReplayTrace {
553 pub metadata: TraceMetadata,
555 pub events: Vec<ReplayEvent>,
557 #[serde(skip)]
559 pub cursor: usize,
560}
561
562impl ReplayTrace {
563 #[must_use]
565 pub fn new(metadata: TraceMetadata) -> Self {
566 Self {
567 metadata,
568 events: Vec::new(),
569 cursor: 0,
570 }
571 }
572
573 #[must_use]
575 pub fn with_capacity(metadata: TraceMetadata, capacity: usize) -> Self {
576 Self {
577 metadata,
578 events: Vec::with_capacity(capacity),
579 cursor: 0,
580 }
581 }
582
583 pub fn push(&mut self, event: ReplayEvent) {
585 self.events.push(event);
586 }
587
588 #[must_use]
590 pub fn len(&self) -> usize {
591 self.events.len()
592 }
593
594 #[must_use]
596 pub fn is_empty(&self) -> bool {
597 self.events.is_empty()
598 }
599
600 pub fn to_bytes(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
606 rmp_serde::to_vec(self)
607 }
608
609 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ReplayTraceError> {
615 let trace: Self = rmp_serde::from_slice(bytes)?;
616 if !trace.metadata.is_compatible() {
617 return Err(ReplayTraceError::IncompatibleVersion {
618 expected: REPLAY_SCHEMA_VERSION,
619 found: trace.metadata.version,
620 });
621 }
622 Ok(trace)
623 }
624
625 pub fn iter(&self) -> impl Iterator<Item = &ReplayEvent> {
627 self.events.iter()
628 }
629
630 #[must_use]
632 pub fn estimated_size(&self) -> usize {
633 50 + self
635 .events
636 .iter()
637 .map(ReplayEvent::estimated_size)
638 .sum::<usize>()
639 }
640}
641
642#[derive(Debug, thiserror::Error)]
644pub enum ReplayTraceError {
645 #[error("serialization error: {0}")]
647 Serde(#[from] rmp_serde::decode::Error),
648
649 #[error("incompatible trace version: expected {expected}, found {found}")]
651 IncompatibleVersion {
652 expected: u32,
654 found: u32,
656 },
657}
658
659#[must_use]
665fn error_kind_to_u8(kind: io::ErrorKind) -> u8 {
666 use io::ErrorKind::{
667 AddrInUse, AddrNotAvailable, AlreadyExists, BrokenPipe, ConnectionAborted,
668 ConnectionRefused, ConnectionReset, Interrupted, InvalidData, InvalidInput, NotConnected,
669 NotFound, OutOfMemory, PermissionDenied, TimedOut, UnexpectedEof, WouldBlock, WriteZero,
670 };
671 match kind {
672 NotFound => 1,
673 PermissionDenied => 2,
674 ConnectionRefused => 3,
675 ConnectionReset => 4,
676 ConnectionAborted => 5,
677 NotConnected => 6,
678 AddrInUse => 7,
679 AddrNotAvailable => 8,
680 BrokenPipe => 9,
681 AlreadyExists => 10,
682 WouldBlock => 11,
683 InvalidInput => 12,
684 InvalidData => 13,
685 TimedOut => 14,
686 WriteZero => 15,
687 Interrupted => 16,
688 UnexpectedEof => 17,
689 OutOfMemory => 18,
690 _ => 255, }
692}
693
694#[must_use]
696pub fn u8_to_error_kind(value: u8) -> io::ErrorKind {
697 use io::ErrorKind::{
698 AddrInUse, AddrNotAvailable, AlreadyExists, BrokenPipe, ConnectionAborted,
699 ConnectionRefused, ConnectionReset, Interrupted, InvalidData, InvalidInput, NotConnected,
700 NotFound, Other, OutOfMemory, PermissionDenied, TimedOut, UnexpectedEof, WouldBlock,
701 WriteZero,
702 };
703 match value {
704 1 => NotFound,
705 2 => PermissionDenied,
706 3 => ConnectionRefused,
707 4 => ConnectionReset,
708 5 => ConnectionAborted,
709 6 => NotConnected,
710 7 => AddrInUse,
711 8 => AddrNotAvailable,
712 9 => BrokenPipe,
713 10 => AlreadyExists,
714 11 => WouldBlock,
715 12 => InvalidInput,
716 13 => InvalidData,
717 14 => TimedOut,
718 15 => WriteZero,
719 16 => Interrupted,
720 17 => UnexpectedEof,
721 18 => OutOfMemory,
722 _ => Other,
723 }
724}
725
726#[cfg(test)]
731mod tests {
732 use super::*;
733
734 #[test]
735 fn metadata_creation() {
736 let meta = TraceMetadata::new(42);
737 assert_eq!(meta.version, REPLAY_SCHEMA_VERSION);
738 assert_eq!(meta.seed, 42);
739 assert!(meta.is_compatible());
740 }
741
742 #[test]
743 fn metadata_builder() {
744 let meta = TraceMetadata::new(42)
745 .with_config_hash(0xDEAD_BEEF)
746 .with_description("test trace");
747 assert_eq!(meta.config_hash, 0xDEAD_BEEF);
748 assert_eq!(meta.description, Some("test trace".to_string()));
749 }
750
751 #[test]
752 fn compact_task_id_roundtrip() {
753 let task = TaskId::new_for_test(123, 456);
754 let compact = CompactTaskId::from(task);
755 let (index, gen) = compact.unpack();
756 assert_eq!(index, 123);
757 assert_eq!(gen, 456);
758 assert_eq!(compact.to_task_id(), task);
759 }
760
761 #[test]
762 fn replay_event_sizes() {
763 let events = [
765 ReplayEvent::TaskScheduled {
766 task: CompactTaskId(0),
767 at_tick: 0,
768 },
769 ReplayEvent::TaskYielded {
770 task: CompactTaskId(0),
771 },
772 ReplayEvent::TaskCompleted {
773 task: CompactTaskId(0),
774 outcome: 0,
775 },
776 ReplayEvent::TimeAdvanced {
777 from_nanos: 0,
778 to_nanos: 0,
779 },
780 ReplayEvent::TimerFired { timer_id: 0 },
781 ReplayEvent::IoReady {
782 token: 0,
783 readiness: 0,
784 },
785 ReplayEvent::RngSeed { seed: 0 },
786 ReplayEvent::WakerWake {
787 task: CompactTaskId(0),
788 },
789 ];
790
791 for event in &events {
792 let size = event.estimated_size();
793 assert!(size < 64, "Event {event:?} exceeds 64 bytes: {size} bytes");
794 }
795 }
796
797 #[test]
798 fn trace_serialization_roundtrip() {
799 let mut trace = ReplayTrace::new(TraceMetadata::new(42));
800 trace.push(ReplayEvent::RngSeed { seed: 42 });
801 trace.push(ReplayEvent::TaskScheduled {
802 task: CompactTaskId(1),
803 at_tick: 0,
804 });
805 trace.push(ReplayEvent::TimeAdvanced {
806 from_nanos: 0,
807 to_nanos: 1_000_000,
808 });
809 trace.push(ReplayEvent::TaskCompleted {
810 task: CompactTaskId(1),
811 outcome: 0,
812 });
813
814 let bytes = trace.to_bytes().expect("serialize");
815 let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
816
817 assert_eq!(loaded.metadata.seed, 42);
818 assert_eq!(loaded.events.len(), 4);
819 assert_eq!(loaded.events[0], ReplayEvent::RngSeed { seed: 42 });
820 }
821
822 #[test]
823 fn trace_actual_serialized_size() {
824 let mut trace = ReplayTrace::new(TraceMetadata::new(42));
825
826 for i in 0..100 {
828 trace.push(ReplayEvent::TaskScheduled {
829 task: CompactTaskId(i),
830 at_tick: i,
831 });
832 }
833
834 let bytes = trace.to_bytes().expect("serialize");
835 let avg_size = bytes.len() / 100;
836
837 assert!(
839 avg_size < 32,
840 "Average serialized event size {avg_size} bytes exceeds expected"
841 );
842 }
843
844 #[test]
845 fn error_kind_roundtrip() {
846 use io::ErrorKind::*;
847 let kinds = [
848 NotFound,
849 PermissionDenied,
850 ConnectionRefused,
851 ConnectionReset,
852 BrokenPipe,
853 WouldBlock,
854 TimedOut,
855 ];
856
857 for kind in kinds {
858 let encoded = error_kind_to_u8(kind);
859 let decoded = u8_to_error_kind(encoded);
860 assert_eq!(kind, decoded, "Failed roundtrip for {kind:?}");
861 }
862 }
863
864 #[test]
865 fn version_compatibility_check() {
866 let mut trace = ReplayTrace::new(TraceMetadata::new(42));
867 trace.push(ReplayEvent::RngSeed { seed: 42 });
868
869 let bytes = trace.to_bytes().expect("serialize");
871
872 let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
875 assert!(loaded.metadata.is_compatible());
876 }
877
878 #[test]
879 fn io_ready_flags() {
880 let event = ReplayEvent::io_ready(123, true, false, false, false);
881 if let ReplayEvent::IoReady { token, readiness } = event {
882 assert_eq!(token, 123);
883 assert_eq!(readiness & 1, 1); assert_eq!(readiness & 2, 0); } else {
886 panic!("Expected IoReady");
887 }
888
889 let event = ReplayEvent::io_ready(456, true, true, true, true);
890 if let ReplayEvent::IoReady { readiness, .. } = event {
891 assert_eq!(readiness, 0b1111); } else {
893 panic!("Expected IoReady");
894 }
895 }
896
897 #[test]
898 fn chaos_injection_variants() {
899 let event_no_task = ReplayEvent::ChaosInjection {
900 kind: 1, task: None,
902 data: 1_000_000, };
904 assert!(event_no_task.estimated_size() < 64);
905
906 let event_with_task = ReplayEvent::ChaosInjection {
907 kind: 0, task: Some(CompactTaskId(42)),
909 data: 0,
910 };
911 assert!(event_with_task.estimated_size() < 64);
912 }
913
914 #[test]
915 fn region_created_event() {
916 let event = ReplayEvent::region_created(CompactRegionId(1), Some(CompactRegionId(0)), 100);
917
918 if let ReplayEvent::RegionCreated {
919 region,
920 parent,
921 at_tick,
922 } = event
923 {
924 assert_eq!(region.0, 1);
925 assert_eq!(parent.map(|p| p.0), Some(0));
926 assert_eq!(at_tick, 100);
927 } else {
928 panic!("Expected RegionCreated");
929 }
930
931 let root = ReplayEvent::region_created(CompactRegionId(0), None::<CompactRegionId>, 0);
933 if let ReplayEvent::RegionCreated { parent, .. } = root {
934 assert!(parent.is_none());
935 } else {
936 panic!("Expected RegionCreated");
937 }
938 }
939
940 #[test]
941 fn region_closed_event() {
942 let event = ReplayEvent::region_closed(CompactRegionId(5), Severity::Ok);
943
944 if let ReplayEvent::RegionClosed { region, outcome } = event {
945 assert_eq!(region.0, 5);
946 assert_eq!(outcome, Severity::Ok.as_u8());
947 } else {
948 panic!("Expected RegionClosed");
949 }
950 }
951
952 #[test]
953 fn region_cancelled_event() {
954 let event = ReplayEvent::region_cancelled(CompactRegionId(3), 1);
955
956 if let ReplayEvent::RegionCancelled {
957 region,
958 cancel_kind,
959 } = event
960 {
961 assert_eq!(region.0, 3);
962 assert_eq!(cancel_kind, 1);
963 } else {
964 panic!("Expected RegionCancelled");
965 }
966 }
967
968 #[test]
969 fn checkpoint_event() {
970 let event = ReplayEvent::checkpoint(42, 1_000_000_000, 5, 2);
971
972 if let ReplayEvent::Checkpoint {
973 sequence,
974 time_nanos,
975 active_tasks,
976 active_regions,
977 } = event
978 {
979 assert_eq!(sequence, 42);
980 assert_eq!(time_nanos, 1_000_000_000);
981 assert_eq!(active_tasks, 5);
982 assert_eq!(active_regions, 2);
983 } else {
984 panic!("Expected Checkpoint");
985 }
986 }
987
988 #[test]
989 fn region_events_size() {
990 let events = [
992 ReplayEvent::RegionCreated {
993 region: CompactRegionId(0),
994 parent: None,
995 at_tick: 0,
996 },
997 ReplayEvent::RegionCreated {
998 region: CompactRegionId(0),
999 parent: Some(CompactRegionId(1)),
1000 at_tick: 0,
1001 },
1002 ReplayEvent::RegionClosed {
1003 region: CompactRegionId(0),
1004 outcome: 0,
1005 },
1006 ReplayEvent::RegionCancelled {
1007 region: CompactRegionId(0),
1008 cancel_kind: 0,
1009 },
1010 ReplayEvent::Checkpoint {
1011 sequence: 0,
1012 time_nanos: 0,
1013 active_tasks: 0,
1014 active_regions: 0,
1015 },
1016 ];
1017
1018 for event in &events {
1019 let size = event.estimated_size();
1020 assert!(size < 64, "Event {event:?} exceeds 64 bytes: {size} bytes");
1021 }
1022 }
1023
1024 #[test]
1025 fn empty_trace_serialization_roundtrip() {
1026 let trace = ReplayTrace::new(TraceMetadata::new(0));
1027 assert!(trace.is_empty());
1028 assert_eq!(trace.len(), 0);
1029
1030 let bytes = trace.to_bytes().expect("serialize empty");
1031 let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize empty");
1032
1033 assert_eq!(loaded.metadata.seed, 0);
1034 assert!(loaded.is_empty());
1035 }
1036
1037 #[test]
1038 fn incompatible_version_rejected() {
1039 let mut trace = ReplayTrace::new(TraceMetadata::new(42));
1040 trace.push(ReplayEvent::RngSeed { seed: 42 });
1041
1042 let _bytes = trace.to_bytes().expect("serialize");
1043
1044 let meta = TraceMetadata {
1048 version: 999,
1049 seed: 42,
1050 recorded_at: 0,
1051 config_hash: 0,
1052 description: None,
1053 };
1054 let bad_trace = ReplayTrace {
1055 metadata: meta,
1056 events: vec![ReplayEvent::RngSeed { seed: 42 }],
1057 cursor: 0,
1058 };
1059 let bad_bytes = bad_trace.to_bytes().expect("serialize bad version");
1060 let err = ReplayTrace::from_bytes(&bad_bytes).unwrap_err();
1061 assert!(matches!(
1062 err,
1063 ReplayTraceError::IncompatibleVersion {
1064 expected: REPLAY_SCHEMA_VERSION,
1065 found: 999
1066 }
1067 ));
1068 }
1069
1070 #[test]
1071 fn trace_with_capacity_preallocates() {
1072 let trace = ReplayTrace::with_capacity(TraceMetadata::new(1), 100);
1073 assert!(trace.is_empty());
1074 assert_eq!(trace.len(), 0);
1075 }
1076
1077 #[test]
1078 fn estimated_size_increases_with_events() {
1079 let mut trace = ReplayTrace::new(TraceMetadata::new(42));
1080 let base_size = trace.estimated_size();
1081
1082 trace.push(ReplayEvent::RngSeed { seed: 42 });
1083 let one_event_size = trace.estimated_size();
1084 assert!(one_event_size > base_size);
1085
1086 trace.push(ReplayEvent::TaskScheduled {
1087 task: CompactTaskId(1),
1088 at_tick: 0,
1089 });
1090 let two_event_size = trace.estimated_size();
1091 assert!(two_event_size > one_event_size);
1092 }
1093
1094 #[test]
1095 fn compact_region_id_roundtrip() {
1096 let region = RegionId::new_for_test(456, 789);
1097 let compact = CompactRegionId::from(region);
1098 let (index, gen) = compact.unpack();
1099 assert_eq!(index, 456);
1100 assert_eq!(gen, 789);
1101 assert_eq!(compact.to_region_id(), region);
1102 }
1103
1104 #[test]
1105 fn metadata_compatibility_flag() {
1106 let meta = TraceMetadata::new(42);
1107 assert!(meta.is_compatible());
1108
1109 let old_meta = TraceMetadata {
1110 version: 0,
1111 seed: 42,
1112 recorded_at: 0,
1113 config_hash: 0,
1114 description: None,
1115 };
1116 assert!(!old_meta.is_compatible());
1117 }
1118
1119 #[test]
1120 fn io_error_roundtrip_all_known_kinds() {
1121 use io::ErrorKind::*;
1122 let all_known = [
1123 NotFound,
1124 PermissionDenied,
1125 ConnectionRefused,
1126 ConnectionReset,
1127 ConnectionAborted,
1128 NotConnected,
1129 AddrInUse,
1130 AddrNotAvailable,
1131 BrokenPipe,
1132 AlreadyExists,
1133 WouldBlock,
1134 InvalidInput,
1135 InvalidData,
1136 TimedOut,
1137 WriteZero,
1138 Interrupted,
1139 UnexpectedEof,
1140 OutOfMemory,
1141 ];
1142
1143 for kind in all_known {
1144 let encoded = error_kind_to_u8(kind);
1145 let decoded = u8_to_error_kind(encoded);
1146 assert_eq!(kind, decoded, "Roundtrip failed for {kind:?}");
1147 }
1148 }
1149
1150 #[test]
1151 fn unknown_error_kind_maps_to_other() {
1152 let decoded = u8_to_error_kind(255);
1153 assert_eq!(decoded, io::ErrorKind::Other);
1154 let decoded = u8_to_error_kind(200);
1155 assert_eq!(decoded, io::ErrorKind::Other);
1156 }
1157
1158 #[test]
1159 fn trace_iter_yields_all_events() {
1160 let mut trace = ReplayTrace::new(TraceMetadata::new(42));
1161 trace.push(ReplayEvent::RngSeed { seed: 1 });
1162 trace.push(ReplayEvent::RngSeed { seed: 2 });
1163 trace.push(ReplayEvent::RngSeed { seed: 3 });
1164
1165 assert_eq!(trace.iter().count(), 3);
1166 }
1167
1168 #[test]
1169 fn region_events_serialization_roundtrip() {
1170 let mut trace = ReplayTrace::new(TraceMetadata::new(123));
1171
1172 trace.push(ReplayEvent::RegionCreated {
1174 region: CompactRegionId(0),
1175 parent: None,
1176 at_tick: 0,
1177 });
1178 trace.push(ReplayEvent::RegionCreated {
1179 region: CompactRegionId(1),
1180 parent: Some(CompactRegionId(0)),
1181 at_tick: 10,
1182 });
1183 trace.push(ReplayEvent::RegionCancelled {
1184 region: CompactRegionId(1),
1185 cancel_kind: 2,
1186 });
1187 trace.push(ReplayEvent::RegionClosed {
1188 region: CompactRegionId(1),
1189 outcome: 2, });
1191 trace.push(ReplayEvent::RegionClosed {
1192 region: CompactRegionId(0),
1193 outcome: 0, });
1195 trace.push(ReplayEvent::Checkpoint {
1196 sequence: 1,
1197 time_nanos: 1_000_000,
1198 active_tasks: 0,
1199 active_regions: 0,
1200 });
1201
1202 let bytes = trace.to_bytes().expect("serialize");
1203 let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
1204
1205 assert_eq!(loaded.events.len(), 6);
1206
1207 match &loaded.events[0] {
1209 ReplayEvent::RegionCreated {
1210 region,
1211 parent,
1212 at_tick,
1213 } => {
1214 assert_eq!(region.0, 0);
1215 assert!(parent.is_none());
1216 assert_eq!(*at_tick, 0);
1217 }
1218 _ => panic!("Expected RegionCreated"),
1219 }
1220
1221 match &loaded.events[5] {
1223 ReplayEvent::Checkpoint {
1224 sequence,
1225 time_nanos,
1226 active_tasks,
1227 active_regions,
1228 } => {
1229 assert_eq!(*sequence, 1);
1230 assert_eq!(*time_nanos, 1_000_000);
1231 assert_eq!(*active_tasks, 0);
1232 assert_eq!(*active_regions, 0);
1233 }
1234 _ => panic!("Expected Checkpoint"),
1235 }
1236 }
1237}