use crate::types::{RegionId, Severity, TaskId, Time};
use serde::{Deserialize, Serialize};
use std::io;
pub const REPLAY_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TraceMetadata {
pub version: u32,
pub seed: u64,
pub recorded_at: u64,
pub config_hash: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl TraceMetadata {
#[must_use]
pub fn new(seed: u64) -> Self {
Self {
version: REPLAY_SCHEMA_VERSION,
seed,
recorded_at: 0,
config_hash: 0,
description: None,
}
}
#[must_use]
pub const fn with_config_hash(mut self, hash: u64) -> Self {
self.config_hash = hash;
self
}
#[must_use]
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
#[must_use]
pub fn is_compatible(&self) -> bool {
self.version == REPLAY_SCHEMA_VERSION
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(transparent)]
pub struct CompactTaskId(pub u64);
impl From<TaskId> for CompactTaskId {
fn from(id: TaskId) -> Self {
let idx = id.arena_index();
let packed = (u64::from(idx.index()) << 32) | u64::from(idx.generation());
Self(packed)
}
}
impl CompactTaskId {
#[must_use]
pub const fn unpack(self) -> (u32, u32) {
let index = (self.0 >> 32) as u32;
let generation = self.0 as u32;
(index, generation)
}
#[cfg(any(test, feature = "test-internals"))]
#[must_use]
pub fn to_task_id(self) -> TaskId {
let (index, generation) = self.unpack();
TaskId::new_for_test(index, generation)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(transparent)]
pub struct CompactRegionId(pub u64);
impl From<RegionId> for CompactRegionId {
fn from(id: RegionId) -> Self {
let idx = id.arena_index();
let packed = (u64::from(idx.index()) << 32) | u64::from(idx.generation());
Self(packed)
}
}
impl CompactRegionId {
#[must_use]
pub const fn unpack(self) -> (u32, u32) {
let index = (self.0 >> 32) as u32;
let generation = self.0 as u32;
(index, generation)
}
#[cfg(any(test, feature = "test-internals"))]
#[must_use]
pub fn to_region_id(self) -> RegionId {
let (index, generation) = self.unpack();
RegionId::new_for_test(index, generation)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type")]
pub enum ReplayEvent {
TaskScheduled {
task: CompactTaskId,
at_tick: u64,
},
TaskYielded {
task: CompactTaskId,
},
TaskCompleted {
task: CompactTaskId,
outcome: u8,
},
TaskSpawned {
task: CompactTaskId,
region: CompactRegionId,
at_tick: u64,
},
TimeAdvanced {
from_nanos: u64,
to_nanos: u64,
},
TimerCreated {
timer_id: u64,
deadline_nanos: u64,
},
TimerFired {
timer_id: u64,
},
TimerCancelled {
timer_id: u64,
},
IoReady {
token: u64,
readiness: u8,
},
IoResult {
token: u64,
bytes: i64,
},
IoError {
token: u64,
kind: u8,
},
RngSeed {
seed: u64,
},
RngValue {
value: u64,
},
ChaosInjection {
kind: u8,
task: Option<CompactTaskId>,
data: u64,
},
RegionCreated {
region: CompactRegionId,
parent: Option<CompactRegionId>,
at_tick: u64,
},
RegionClosed {
region: CompactRegionId,
outcome: u8,
},
RegionCancelled {
region: CompactRegionId,
cancel_kind: u8,
},
WakerWake {
task: CompactTaskId,
},
WakerBatchWake {
count: u32,
},
Checkpoint {
sequence: u64,
time_nanos: u64,
active_tasks: u32,
active_regions: u32,
},
}
impl ReplayEvent {
#[must_use]
pub const fn estimated_size(&self) -> usize {
match self {
Self::TaskYielded { .. }
| Self::TimerFired { .. }
| Self::TimerCancelled { .. }
| Self::RngSeed { .. }
| Self::RngValue { .. }
| Self::WakerWake { .. } => 9, Self::TaskCompleted { .. }
| Self::IoReady { .. }
| Self::IoError { .. }
| Self::RegionClosed { .. }
| Self::RegionCancelled { .. } => 10, Self::TaskScheduled { .. }
| Self::TimeAdvanced { .. }
| Self::TimerCreated { .. }
| Self::IoResult { .. }
| Self::RegionCreated { parent: None, .. } => 17, Self::TaskSpawned { .. }
| Self::RegionCreated {
parent: Some(_), ..
}
| Self::Checkpoint { .. } => 25, Self::ChaosInjection { task: None, .. } => 11, Self::ChaosInjection { task: Some(_), .. } => 19, Self::WakerBatchWake { .. } => 5, }
}
#[must_use]
pub fn task_scheduled(task: impl Into<CompactTaskId>, at_tick: u64) -> Self {
Self::TaskScheduled {
task: task.into(),
at_tick,
}
}
#[must_use]
pub fn task_completed(task: impl Into<CompactTaskId>, severity: Severity) -> Self {
Self::TaskCompleted {
task: task.into(),
outcome: severity.as_u8(),
}
}
#[must_use]
pub fn time_advanced(from: Time, to: Time) -> Self {
Self::TimeAdvanced {
from_nanos: from.as_nanos(),
to_nanos: to.as_nanos(),
}
}
#[must_use]
#[allow(clippy::fn_params_excessive_bools)]
pub fn io_ready(token: u64, readable: bool, writable: bool, error: bool, hangup: bool) -> Self {
let mut readiness = 0u8;
if readable {
readiness |= 1;
}
if writable {
readiness |= 2;
}
if error {
readiness |= 4;
}
if hangup {
readiness |= 8;
}
Self::IoReady { token, readiness }
}
#[must_use]
pub fn io_error(token: u64, kind: io::ErrorKind) -> Self {
Self::IoError {
token,
kind: error_kind_to_u8(kind),
}
}
#[must_use]
pub fn region_created(
region: impl Into<CompactRegionId>,
parent: Option<impl Into<CompactRegionId>>,
at_tick: u64,
) -> Self {
Self::RegionCreated {
region: region.into(),
parent: parent.map(Into::into),
at_tick,
}
}
#[must_use]
pub fn region_closed(region: impl Into<CompactRegionId>, severity: Severity) -> Self {
Self::RegionClosed {
region: region.into(),
outcome: severity.as_u8(),
}
}
#[must_use]
pub fn region_cancelled(region: impl Into<CompactRegionId>, cancel_kind: u8) -> Self {
Self::RegionCancelled {
region: region.into(),
cancel_kind,
}
}
#[must_use]
pub fn checkpoint(
sequence: u64,
time_nanos: u64,
active_tasks: u32,
active_regions: u32,
) -> Self {
Self::Checkpoint {
sequence,
time_nanos,
active_tasks,
active_regions,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayTrace {
pub metadata: TraceMetadata,
pub events: Vec<ReplayEvent>,
#[serde(skip)]
pub cursor: usize,
}
impl ReplayTrace {
#[must_use]
pub fn new(metadata: TraceMetadata) -> Self {
Self {
metadata,
events: Vec::new(),
cursor: 0,
}
}
#[must_use]
pub fn with_capacity(metadata: TraceMetadata, capacity: usize) -> Self {
Self {
metadata,
events: Vec::with_capacity(capacity),
cursor: 0,
}
}
pub fn push(&mut self, event: ReplayEvent) {
self.events.push(event);
}
#[must_use]
pub fn len(&self) -> usize {
self.events.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub fn to_bytes(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
rmp_serde::to_vec(self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ReplayTraceError> {
let trace: Self = rmp_serde::from_slice(bytes)?;
if !trace.metadata.is_compatible() {
return Err(ReplayTraceError::IncompatibleVersion {
expected: REPLAY_SCHEMA_VERSION,
found: trace.metadata.version,
});
}
Ok(trace)
}
pub fn iter(&self) -> impl Iterator<Item = &ReplayEvent> {
self.events.iter()
}
#[must_use]
pub fn estimated_size(&self) -> usize {
50 + self
.events
.iter()
.map(ReplayEvent::estimated_size)
.sum::<usize>()
}
}
#[derive(Debug, thiserror::Error)]
pub enum ReplayTraceError {
#[error("serialization error: {0}")]
Serde(#[from] rmp_serde::decode::Error),
#[error("incompatible trace version: expected {expected}, found {found}")]
IncompatibleVersion {
expected: u32,
found: u32,
},
}
#[must_use]
fn error_kind_to_u8(kind: io::ErrorKind) -> u8 {
use io::ErrorKind::{
AddrInUse, AddrNotAvailable, AlreadyExists, BrokenPipe, ConnectionAborted,
ConnectionRefused, ConnectionReset, Interrupted, InvalidData, InvalidInput, NotConnected,
NotFound, OutOfMemory, PermissionDenied, TimedOut, UnexpectedEof, WouldBlock, WriteZero,
};
match kind {
NotFound => 1,
PermissionDenied => 2,
ConnectionRefused => 3,
ConnectionReset => 4,
ConnectionAborted => 5,
NotConnected => 6,
AddrInUse => 7,
AddrNotAvailable => 8,
BrokenPipe => 9,
AlreadyExists => 10,
WouldBlock => 11,
InvalidInput => 12,
InvalidData => 13,
TimedOut => 14,
WriteZero => 15,
Interrupted => 16,
UnexpectedEof => 17,
OutOfMemory => 18,
_ => 255, }
}
#[must_use]
pub fn u8_to_error_kind(value: u8) -> io::ErrorKind {
use io::ErrorKind::{
AddrInUse, AddrNotAvailable, AlreadyExists, BrokenPipe, ConnectionAborted,
ConnectionRefused, ConnectionReset, Interrupted, InvalidData, InvalidInput, NotConnected,
NotFound, Other, OutOfMemory, PermissionDenied, TimedOut, UnexpectedEof, WouldBlock,
WriteZero,
};
match value {
1 => NotFound,
2 => PermissionDenied,
3 => ConnectionRefused,
4 => ConnectionReset,
5 => ConnectionAborted,
6 => NotConnected,
7 => AddrInUse,
8 => AddrNotAvailable,
9 => BrokenPipe,
10 => AlreadyExists,
11 => WouldBlock,
12 => InvalidInput,
13 => InvalidData,
14 => TimedOut,
15 => WriteZero,
16 => Interrupted,
17 => UnexpectedEof,
18 => OutOfMemory,
_ => Other,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metadata_creation() {
let meta = TraceMetadata::new(42);
assert_eq!(meta.version, REPLAY_SCHEMA_VERSION);
assert_eq!(meta.seed, 42);
assert_eq!(meta.recorded_at, 0);
assert!(meta.is_compatible());
}
#[test]
fn metadata_creation_is_deterministic_for_same_seed() {
let first = TraceMetadata::new(42);
let second = TraceMetadata::new(42);
assert_eq!(first, second);
assert_eq!(first.recorded_at, 0);
}
#[test]
fn metadata_builder() {
let meta = TraceMetadata::new(42)
.with_config_hash(0xDEAD_BEEF)
.with_description("test trace");
assert_eq!(meta.config_hash, 0xDEAD_BEEF);
assert_eq!(meta.description, Some("test trace".to_string()));
}
#[test]
fn compact_task_id_roundtrip() {
let task = TaskId::new_for_test(123, 456);
let compact = CompactTaskId::from(task);
let (index, generation) = compact.unpack();
assert_eq!(index, 123);
assert_eq!(generation, 456);
assert_eq!(compact.to_task_id(), task);
}
#[test]
fn replay_event_sizes() {
let events = [
ReplayEvent::TaskScheduled {
task: CompactTaskId(0),
at_tick: 0,
},
ReplayEvent::TaskYielded {
task: CompactTaskId(0),
},
ReplayEvent::TaskCompleted {
task: CompactTaskId(0),
outcome: 0,
},
ReplayEvent::TimeAdvanced {
from_nanos: 0,
to_nanos: 0,
},
ReplayEvent::TimerFired { timer_id: 0 },
ReplayEvent::IoReady {
token: 0,
readiness: 0,
},
ReplayEvent::RngSeed { seed: 0 },
ReplayEvent::WakerWake {
task: CompactTaskId(0),
},
];
for event in &events {
let size = event.estimated_size();
assert!(size < 64, "Event {event:?} exceeds 64 bytes: {size} bytes");
}
}
#[test]
fn trace_serialization_roundtrip() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
trace.push(ReplayEvent::RngSeed { seed: 42 });
trace.push(ReplayEvent::TaskScheduled {
task: CompactTaskId(1),
at_tick: 0,
});
trace.push(ReplayEvent::TimeAdvanced {
from_nanos: 0,
to_nanos: 1_000_000,
});
trace.push(ReplayEvent::TaskCompleted {
task: CompactTaskId(1),
outcome: 0,
});
let bytes = trace.to_bytes().expect("serialize");
let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
assert_eq!(loaded.metadata.seed, 42);
assert_eq!(loaded.events.len(), 4);
assert_eq!(loaded.events[0], ReplayEvent::RngSeed { seed: 42 });
}
#[test]
fn trace_actual_serialized_size() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
for i in 0..100 {
trace.push(ReplayEvent::TaskScheduled {
task: CompactTaskId(i),
at_tick: i,
});
}
let bytes = trace.to_bytes().expect("serialize");
let avg_size = bytes.len() / 100;
assert!(
avg_size < 32,
"Average serialized event size {avg_size} bytes exceeds expected"
);
}
#[test]
fn error_kind_roundtrip() {
use io::ErrorKind::*;
let kinds = [
NotFound,
PermissionDenied,
ConnectionRefused,
ConnectionReset,
BrokenPipe,
WouldBlock,
TimedOut,
];
for kind in kinds {
let encoded = error_kind_to_u8(kind);
let decoded = u8_to_error_kind(encoded);
assert_eq!(kind, decoded, "Failed roundtrip for {kind:?}");
}
}
#[test]
fn version_compatibility_check() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
trace.push(ReplayEvent::RngSeed { seed: 42 });
let bytes = trace.to_bytes().expect("serialize");
let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
assert!(loaded.metadata.is_compatible());
}
#[test]
fn io_ready_flags() {
let event = ReplayEvent::io_ready(123, true, false, false, false);
if let ReplayEvent::IoReady { token, readiness } = event {
assert_eq!(token, 123);
assert_eq!(readiness & 1, 1); assert_eq!(readiness & 2, 0); } else {
panic!("Expected IoReady");
}
let event = ReplayEvent::io_ready(456, true, true, true, true);
if let ReplayEvent::IoReady { readiness, .. } = event {
assert_eq!(readiness, 0b1111); } else {
panic!("Expected IoReady");
}
}
#[test]
fn chaos_injection_variants() {
let event_no_task = ReplayEvent::ChaosInjection {
kind: 1, task: None,
data: 1_000_000, };
assert!(event_no_task.estimated_size() < 64);
let event_with_task = ReplayEvent::ChaosInjection {
kind: 0, task: Some(CompactTaskId(42)),
data: 0,
};
assert!(event_with_task.estimated_size() < 64);
}
#[test]
fn region_created_event() {
let event = ReplayEvent::region_created(CompactRegionId(1), Some(CompactRegionId(0)), 100);
if let ReplayEvent::RegionCreated {
region,
parent,
at_tick,
} = event
{
assert_eq!(region.0, 1);
assert_eq!(parent.map(|p| p.0), Some(0));
assert_eq!(at_tick, 100);
} else {
panic!("Expected RegionCreated");
}
let root = ReplayEvent::region_created(CompactRegionId(0), None::<CompactRegionId>, 0);
if let ReplayEvent::RegionCreated { parent, .. } = root {
assert!(parent.is_none());
} else {
panic!("Expected RegionCreated");
}
}
#[test]
fn region_closed_event() {
let event = ReplayEvent::region_closed(CompactRegionId(5), Severity::Ok);
if let ReplayEvent::RegionClosed { region, outcome } = event {
assert_eq!(region.0, 5);
assert_eq!(outcome, Severity::Ok.as_u8());
} else {
panic!("Expected RegionClosed");
}
}
#[test]
fn region_cancelled_event() {
let event = ReplayEvent::region_cancelled(CompactRegionId(3), 1);
if let ReplayEvent::RegionCancelled {
region,
cancel_kind,
} = event
{
assert_eq!(region.0, 3);
assert_eq!(cancel_kind, 1);
} else {
panic!("Expected RegionCancelled");
}
}
#[test]
fn checkpoint_event() {
let event = ReplayEvent::checkpoint(42, 1_000_000_000, 5, 2);
if let ReplayEvent::Checkpoint {
sequence,
time_nanos,
active_tasks,
active_regions,
} = event
{
assert_eq!(sequence, 42);
assert_eq!(time_nanos, 1_000_000_000);
assert_eq!(active_tasks, 5);
assert_eq!(active_regions, 2);
} else {
panic!("Expected Checkpoint");
}
}
#[test]
fn region_events_size() {
let events = [
ReplayEvent::RegionCreated {
region: CompactRegionId(0),
parent: None,
at_tick: 0,
},
ReplayEvent::RegionCreated {
region: CompactRegionId(0),
parent: Some(CompactRegionId(1)),
at_tick: 0,
},
ReplayEvent::RegionClosed {
region: CompactRegionId(0),
outcome: 0,
},
ReplayEvent::RegionCancelled {
region: CompactRegionId(0),
cancel_kind: 0,
},
ReplayEvent::Checkpoint {
sequence: 0,
time_nanos: 0,
active_tasks: 0,
active_regions: 0,
},
];
for event in &events {
let size = event.estimated_size();
assert!(size < 64, "Event {event:?} exceeds 64 bytes: {size} bytes");
}
}
#[test]
fn empty_trace_serialization_roundtrip() {
let trace = ReplayTrace::new(TraceMetadata::new(0));
assert!(trace.is_empty());
assert_eq!(trace.len(), 0);
let bytes = trace.to_bytes().expect("serialize empty");
let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize empty");
assert_eq!(loaded.metadata.seed, 0);
assert!(loaded.is_empty());
}
#[test]
fn incompatible_version_rejected() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
trace.push(ReplayEvent::RngSeed { seed: 42 });
let _bytes = trace.to_bytes().expect("serialize");
let meta = TraceMetadata {
version: 999,
seed: 42,
recorded_at: 0,
config_hash: 0,
description: None,
};
let bad_trace = ReplayTrace {
metadata: meta,
events: vec![ReplayEvent::RngSeed { seed: 42 }],
cursor: 0,
};
let bad_bytes = bad_trace.to_bytes().expect("serialize bad version");
let err = ReplayTrace::from_bytes(&bad_bytes).unwrap_err();
assert!(matches!(
err,
ReplayTraceError::IncompatibleVersion {
expected: REPLAY_SCHEMA_VERSION,
found: 999
}
));
}
#[test]
fn trace_with_capacity_preallocates() {
let trace = ReplayTrace::with_capacity(TraceMetadata::new(1), 100);
assert!(trace.is_empty());
assert_eq!(trace.len(), 0);
}
#[test]
fn estimated_size_increases_with_events() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
let base_size = trace.estimated_size();
trace.push(ReplayEvent::RngSeed { seed: 42 });
let one_event_size = trace.estimated_size();
assert!(one_event_size > base_size);
trace.push(ReplayEvent::TaskScheduled {
task: CompactTaskId(1),
at_tick: 0,
});
let two_event_size = trace.estimated_size();
assert!(two_event_size > one_event_size);
}
#[test]
fn compact_region_id_roundtrip() {
let region = RegionId::new_for_test(456, 789);
let compact = CompactRegionId::from(region);
let (index, generation) = compact.unpack();
assert_eq!(index, 456);
assert_eq!(generation, 789);
assert_eq!(compact.to_region_id(), region);
}
#[test]
fn metadata_compatibility_flag() {
let meta = TraceMetadata::new(42);
assert!(meta.is_compatible());
let old_meta = TraceMetadata {
version: 0,
seed: 42,
recorded_at: 0,
config_hash: 0,
description: None,
};
assert!(!old_meta.is_compatible());
}
#[test]
fn io_error_roundtrip_all_known_kinds() {
use io::ErrorKind::*;
let all_known = [
NotFound,
PermissionDenied,
ConnectionRefused,
ConnectionReset,
ConnectionAborted,
NotConnected,
AddrInUse,
AddrNotAvailable,
BrokenPipe,
AlreadyExists,
WouldBlock,
InvalidInput,
InvalidData,
TimedOut,
WriteZero,
Interrupted,
UnexpectedEof,
OutOfMemory,
];
for kind in all_known {
let encoded = error_kind_to_u8(kind);
let decoded = u8_to_error_kind(encoded);
assert_eq!(kind, decoded, "Roundtrip failed for {kind:?}");
}
}
#[test]
fn unknown_error_kind_maps_to_other() {
let decoded = u8_to_error_kind(255);
assert_eq!(decoded, io::ErrorKind::Other);
let decoded = u8_to_error_kind(200);
assert_eq!(decoded, io::ErrorKind::Other);
}
#[test]
fn trace_iter_yields_all_events() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
trace.push(ReplayEvent::RngSeed { seed: 1 });
trace.push(ReplayEvent::RngSeed { seed: 2 });
trace.push(ReplayEvent::RngSeed { seed: 3 });
assert_eq!(trace.iter().count(), 3);
}
#[test]
fn region_events_serialization_roundtrip() {
let mut trace = ReplayTrace::new(TraceMetadata::new(123));
trace.push(ReplayEvent::RegionCreated {
region: CompactRegionId(0),
parent: None,
at_tick: 0,
});
trace.push(ReplayEvent::RegionCreated {
region: CompactRegionId(1),
parent: Some(CompactRegionId(0)),
at_tick: 10,
});
trace.push(ReplayEvent::RegionCancelled {
region: CompactRegionId(1),
cancel_kind: 2,
});
trace.push(ReplayEvent::RegionClosed {
region: CompactRegionId(1),
outcome: 2, });
trace.push(ReplayEvent::RegionClosed {
region: CompactRegionId(0),
outcome: 0, });
trace.push(ReplayEvent::Checkpoint {
sequence: 1,
time_nanos: 1_000_000,
active_tasks: 0,
active_regions: 0,
});
let bytes = trace.to_bytes().expect("serialize");
let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
assert_eq!(loaded.events.len(), 6);
match &loaded.events[0] {
ReplayEvent::RegionCreated {
region,
parent,
at_tick,
} => {
assert_eq!(region.0, 0);
assert!(parent.is_none());
assert_eq!(*at_tick, 0);
}
_ => panic!("Expected RegionCreated"),
}
match &loaded.events[5] {
ReplayEvent::Checkpoint {
sequence,
time_nanos,
active_tasks,
active_regions,
} => {
assert_eq!(*sequence, 1);
assert_eq!(*time_nanos, 1_000_000);
assert_eq!(*active_tasks, 0);
assert_eq!(*active_regions, 0);
}
_ => panic!("Expected Checkpoint"),
}
}
#[test]
fn trace_metadata_debug_clone_eq() {
let m = TraceMetadata {
version: REPLAY_SCHEMA_VERSION,
seed: 42,
recorded_at: 0,
config_hash: 0xABC,
description: Some("test".into()),
};
let m2 = m.clone();
assert_eq!(m, m2);
let dbg = format!("{m:?}");
assert!(dbg.contains("TraceMetadata"));
}
#[test]
fn compact_task_id_debug_clone_copy_eq() {
let id = CompactTaskId(42);
let id2 = id; let id3 = id;
assert_eq!(id, id2);
assert_eq!(id, id3);
assert_ne!(id, CompactTaskId(99));
let dbg = format!("{id:?}");
assert!(dbg.contains("42"));
}
#[test]
fn compact_region_id_debug_clone_copy_eq() {
let id = CompactRegionId(7);
let id2 = id; let id3 = id;
assert_eq!(id, id2);
assert_eq!(id, id3);
assert_ne!(id, CompactRegionId(99));
let dbg = format!("{id:?}");
assert!(dbg.contains('7'));
}
#[test]
fn replay_event_debug_clone_eq() {
let e = ReplayEvent::TaskScheduled {
task: CompactTaskId(1),
at_tick: 100,
};
let e2 = e.clone();
assert_eq!(e, e2);
assert_ne!(
e,
ReplayEvent::TaskYielded {
task: CompactTaskId(1),
}
);
let dbg = format!("{e:?}");
assert!(dbg.contains("TaskScheduled"));
}
}