use crate::rng::DetRng;
use crate::seed::{SeedTree, VortexSeed};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SeqNo(pub u64);
#[derive(Debug, Clone)]
pub struct SimLogEntry {
pub seq: SeqNo,
pub timestamp_us: u64,
pub subsystem: Subsystem,
pub description: String,
pub payload: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Subsystem {
Executor,
Fs,
Clock,
Alloc,
Network,
Process,
Storage,
Custom,
}
impl fmt::Display for Subsystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Subsystem::Executor => write!(f, "executor"),
Subsystem::Fs => write!(f, "fs"),
Subsystem::Clock => write!(f, "clock"),
Subsystem::Alloc => write!(f, "alloc"),
Subsystem::Network => write!(f, "network"),
Subsystem::Process => write!(f, "process"),
Subsystem::Storage => write!(f, "storage"),
Subsystem::Custom => write!(f, "custom"),
}
}
}
pub struct SimEventLog {
entries: Vec<SimLogEntry>,
next_seq: u64,
}
impl SimEventLog {
pub fn new() -> Self {
Self {
entries: Vec::new(),
next_seq: 0,
}
}
pub fn record(
&mut self,
timestamp_us: u64,
subsystem: Subsystem,
description: String,
payload: Option<Vec<u8>>,
) -> SeqNo {
let seq = SeqNo(self.next_seq);
self.next_seq += 1;
self.entries.push(SimLogEntry {
seq,
timestamp_us,
subsystem,
description,
payload,
});
seq
}
pub fn record_simple(
&mut self,
timestamp_us: u64,
subsystem: Subsystem,
description: impl Into<String>,
) -> SeqNo {
self.record(timestamp_us, subsystem, description.into(), None)
}
pub fn entries(&self) -> &[SimLogEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn diff(a: &SimEventLog, b: &SimEventLog) -> Option<LogDivergence> {
let min_len = a.entries.len().min(b.entries.len());
for i in 0..min_len {
let ea = &a.entries[i];
let eb = &b.entries[i];
if ea.subsystem != eb.subsystem
|| ea.description != eb.description
|| ea.timestamp_us != eb.timestamp_us
|| ea.payload != eb.payload
{
return Some(LogDivergence {
seq: SeqNo(i as u64),
entry_a: ea.clone(),
entry_b: eb.clone(),
kind: DivergenceKind::ContentMismatch,
});
}
}
if a.entries.len() != b.entries.len() {
let longer = if a.entries.len() > b.entries.len() {
"a"
} else {
"b"
};
return Some(LogDivergence {
seq: SeqNo(min_len as u64),
entry_a: a
.entries
.get(min_len)
.cloned()
.unwrap_or_else(|| SimLogEntry {
seq: SeqNo(min_len as u64),
timestamp_us: 0,
subsystem: Subsystem::Custom,
description: format!("<log {longer} has no more entries>"),
payload: None,
}),
entry_b: b
.entries
.get(min_len)
.cloned()
.unwrap_or_else(|| SimLogEntry {
seq: SeqNo(min_len as u64),
timestamp_us: 0,
subsystem: Subsystem::Custom,
description: format!("<log {longer} has no more entries>"),
payload: None,
}),
kind: DivergenceKind::LengthMismatch {
len_a: a.entries.len(),
len_b: b.entries.len(),
},
});
}
None
}
}
impl Default for SimEventLog {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct LogDivergence {
pub seq: SeqNo,
pub entry_a: SimLogEntry,
pub entry_b: SimLogEntry,
pub kind: DivergenceKind,
}
#[derive(Debug, Clone)]
pub enum DivergenceKind {
ContentMismatch,
LengthMismatch { len_a: usize, len_b: usize },
}
impl fmt::Display for LogDivergence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
DivergenceKind::ContentMismatch => {
write!(
f,
"Divergence at seq {}: A=[{}] {:?} vs B=[{}] {:?}",
self.seq.0,
self.entry_a.subsystem,
self.entry_a.description,
self.entry_b.subsystem,
self.entry_b.description,
)
}
DivergenceKind::LengthMismatch { len_a, len_b } => {
write!(
f,
"Length mismatch at seq {}: log A has {} entries, log B has {} entries",
self.seq.0, len_a, len_b,
)
}
}
}
}
pub struct SimContext {
seed: VortexSeed,
seed_tree: SeedTree,
logical_time_us: u64,
event_log: SimEventLog,
}
impl SimContext {
pub fn new(seed: VortexSeed) -> Self {
Self {
seed,
seed_tree: SeedTree::new(seed),
logical_time_us: 0,
event_log: SimEventLog::new(),
}
}
pub fn from_u64(seed: u64) -> Self {
Self::new(VortexSeed::from_u64(seed))
}
pub fn seed(&self) -> VortexSeed {
self.seed
}
pub fn seed_tree(&self) -> &SeedTree {
&self.seed_tree
}
pub fn rng_for(&self, domain: &str) -> DetRng {
let sub_seed = self.seed_tree.derive(domain);
DetRng::new(sub_seed.to_u64())
}
pub fn logical_time_us(&self) -> u64 {
self.logical_time_us
}
pub fn advance_time_us(&mut self, delta_us: u64) {
self.logical_time_us += delta_us;
}
pub fn set_time_us(&mut self, time_us: u64) {
self.logical_time_us = time_us;
}
pub fn log_event(&mut self, subsystem: Subsystem, description: impl Into<String>) -> SeqNo {
self.event_log
.record_simple(self.logical_time_us, subsystem, description)
}
pub fn log_event_with_payload(
&mut self,
subsystem: Subsystem,
description: impl Into<String>,
payload: Vec<u8>,
) -> SeqNo {
self.event_log.record(
self.logical_time_us,
subsystem,
description.into(),
Some(payload),
)
}
pub fn event_log(&self) -> &SimEventLog {
&self.event_log
}
pub fn into_event_log(self) -> SimEventLog {
self.event_log
}
}
impl fmt::Debug for SimContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SimContext")
.field("seed", &self.seed)
.field("logical_time_us", &self.logical_time_us)
.field("events_recorded", &self.event_log.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_creation() {
let ctx = SimContext::from_u64(42);
assert_eq!(ctx.seed(), VortexSeed::from_u64(42));
assert_eq!(ctx.logical_time_us(), 0);
assert!(ctx.event_log().is_empty());
}
#[test]
fn test_context_time_advance() {
let mut ctx = SimContext::from_u64(42);
ctx.advance_time_us(1000);
assert_eq!(ctx.logical_time_us(), 1000);
ctx.advance_time_us(500);
assert_eq!(ctx.logical_time_us(), 1500);
}
#[test]
fn test_context_event_logging() {
let mut ctx = SimContext::from_u64(42);
ctx.advance_time_us(100);
let seq1 = ctx.log_event(Subsystem::Fs, "open /data/wal.log");
ctx.advance_time_us(50);
let seq2 = ctx.log_event(Subsystem::Fs, "write 4096 bytes");
assert_eq!(seq1, SeqNo(0));
assert_eq!(seq2, SeqNo(1));
assert_eq!(ctx.event_log().len(), 2);
assert_eq!(ctx.event_log().entries()[0].timestamp_us, 100);
assert_eq!(ctx.event_log().entries()[1].timestamp_us, 150);
}
#[test]
fn test_context_deterministic_rngs() {
let ctx1 = SimContext::from_u64(42);
let ctx2 = SimContext::from_u64(42);
let mut rng1 = ctx1.rng_for("fs");
let mut rng2 = ctx2.rng_for("fs");
let seq1: Vec<u64> = (0..100).map(|_| rng1.next_u64()).collect();
let seq2: Vec<u64> = (0..100).map(|_| rng2.next_u64()).collect();
assert_eq!(seq1, seq2);
}
#[test]
fn test_context_different_domains_different_rngs() {
let ctx = SimContext::from_u64(42);
let mut fs_rng = ctx.rng_for("fs");
let mut net_rng = ctx.rng_for("net");
assert_ne!(fs_rng.next_u64(), net_rng.next_u64());
}
#[test]
fn test_event_log_diff_identical() {
let mut log1 = SimEventLog::new();
let mut log2 = SimEventLog::new();
log1.record_simple(0, Subsystem::Fs, "open file");
log1.record_simple(100, Subsystem::Fs, "write data");
log2.record_simple(0, Subsystem::Fs, "open file");
log2.record_simple(100, Subsystem::Fs, "write data");
assert!(SimEventLog::diff(&log1, &log2).is_none());
}
#[test]
fn test_event_log_diff_content_mismatch() {
let mut log1 = SimEventLog::new();
let mut log2 = SimEventLog::new();
log1.record_simple(0, Subsystem::Fs, "open file A");
log2.record_simple(0, Subsystem::Fs, "open file B");
let d = SimEventLog::diff(&log1, &log2).unwrap();
assert_eq!(d.seq, SeqNo(0));
assert!(matches!(d.kind, DivergenceKind::ContentMismatch));
}
#[test]
fn test_event_log_diff_length_mismatch() {
let mut log1 = SimEventLog::new();
let mut log2 = SimEventLog::new();
log1.record_simple(0, Subsystem::Fs, "open file");
log1.record_simple(100, Subsystem::Fs, "write data");
log2.record_simple(0, Subsystem::Fs, "open file");
let d = SimEventLog::diff(&log1, &log2).unwrap();
assert_eq!(d.seq, SeqNo(1));
assert!(matches!(
d.kind,
DivergenceKind::LengthMismatch { len_a: 2, len_b: 1 }
));
}
#[test]
fn test_event_log_diff_display() {
let mut log1 = SimEventLog::new();
let mut log2 = SimEventLog::new();
log1.record_simple(0, Subsystem::Executor, "wake task A");
log2.record_simple(0, Subsystem::Executor, "wake task B");
let d = SimEventLog::diff(&log1, &log2).unwrap();
let display = format!("{d}");
assert!(display.contains("Divergence"));
assert!(display.contains("task A"));
assert!(display.contains("task B"));
}
}