Skip to main content

vortex_trace/
determinism.rs

1//! Determinism verification: double-run same-seed trace identity.
2//!
3//! Runs a simulation twice with the same seed and verifies that both
4//! runs produce bit-identical traces. Any divergence indicates a
5//! non-determinism bug (e.g., HashMap iteration order, thread scheduling,
6//! system time leaking into the sim).
7
8use crate::{SimTrace, TraceEvent, TraceEventKind};
9
10/// Result of a determinism check.
11#[derive(Debug, Clone)]
12pub struct DeterminismResult {
13    /// Whether the two runs produced identical traces.
14    pub deterministic: bool,
15    /// First diverging event index (if any).
16    pub divergence_at: Option<usize>,
17    /// Description of the divergence (if any).
18    pub divergence_description: Option<String>,
19    /// Number of events in run 1.
20    pub run1_events: usize,
21    /// Number of events in run 2.
22    pub run2_events: usize,
23}
24
25impl DeterminismResult {
26    /// Whether the simulation is deterministic.
27    pub fn is_deterministic(&self) -> bool {
28        self.deterministic
29    }
30}
31
32/// Verify determinism by running a simulation twice and comparing traces.
33///
34/// - `sim_fn`: `(seed) -> SimTrace` — runs the simulation with a seed and
35///   returns the resulting trace.
36/// - `seed`: the seed to test with.
37///
38/// ```
39/// use vortex_trace::determinism::verify_determinism;
40/// use vortex_trace::{SimTrace, TraceEventKind};
41///
42/// let result = verify_determinism(42, |seed| {
43///     let mut trace = SimTrace::new();
44///     // Deterministic simulation: same seed → same events
45///     for i in 0..10 {
46///         trace.record(i, 1, TraceEventKind::TimerFired {
47///             timer_type: format!("t{}", seed + i),
48///         });
49///     }
50///     trace
51/// });
52/// assert!(result.is_deterministic());
53/// ```
54pub fn verify_determinism<F>(seed: u64, sim_fn: F) -> DeterminismResult
55where
56    F: Fn(u64) -> SimTrace,
57{
58    let trace1 = sim_fn(seed);
59    let trace2 = sim_fn(seed);
60
61    compare_traces(&trace1, &trace2)
62}
63
64/// Compare two traces for equality, returning detailed divergence info.
65pub fn compare_traces(trace1: &SimTrace, trace2: &SimTrace) -> DeterminismResult {
66    let events1 = trace1.events();
67    let events2 = trace2.events();
68
69    if events1.len() != events2.len() {
70        return DeterminismResult {
71            deterministic: false,
72            divergence_at: Some(events1.len().min(events2.len())),
73            divergence_description: Some(format!(
74                "Trace length mismatch: run1={}, run2={}",
75                events1.len(),
76                events2.len()
77            )),
78            run1_events: events1.len(),
79            run2_events: events2.len(),
80        };
81    }
82
83    for (i, (e1, e2)) in events1.iter().zip(events2.iter()).enumerate() {
84        if let Some(desc) = events_differ(e1, e2) {
85            return DeterminismResult {
86                deterministic: false,
87                divergence_at: Some(i),
88                divergence_description: Some(format!("Event {i}: {desc}")),
89                run1_events: events1.len(),
90                run2_events: events2.len(),
91            };
92        }
93    }
94
95    DeterminismResult {
96        deterministic: true,
97        divergence_at: None,
98        divergence_description: None,
99        run1_events: events1.len(),
100        run2_events: events2.len(),
101    }
102}
103
104/// Compare two events, returning a description of the difference if any.
105fn events_differ(e1: &TraceEvent, e2: &TraceEvent) -> Option<String> {
106    if e1.tick != e2.tick {
107        return Some(format!("tick differs: {} vs {}", e1.tick, e2.tick));
108    }
109    if e1.node_id != e2.node_id {
110        return Some(format!("node_id differs: {} vs {}", e1.node_id, e2.node_id));
111    }
112    if !event_kinds_equal(&e1.kind, &e2.kind) {
113        return Some(format!("kind differs: {:?} vs {:?}", e1.kind, e2.kind));
114    }
115    None
116}
117
118/// Compare two event kinds for equality.
119fn event_kinds_equal(a: &TraceEventKind, b: &TraceEventKind) -> bool {
120    match (a, b) {
121        (
122            TraceEventKind::MessageSent {
123                to: to1,
124                msg_type: mt1,
125                size_bytes: s1,
126            },
127            TraceEventKind::MessageSent {
128                to: to2,
129                msg_type: mt2,
130                size_bytes: s2,
131            },
132        ) => to1 == to2 && mt1 == mt2 && s1 == s2,
133        (
134            TraceEventKind::MessageDelivered {
135                from: f1,
136                msg_type: mt1,
137                size_bytes: s1,
138            },
139            TraceEventKind::MessageDelivered {
140                from: f2,
141                msg_type: mt2,
142                size_bytes: s2,
143            },
144        ) => f1 == f2 && mt1 == mt2 && s1 == s2,
145        (
146            TraceEventKind::MessageDropped {
147                from: f1,
148                to: t1,
149                reason: r1,
150            },
151            TraceEventKind::MessageDropped {
152                from: f2,
153                to: t2,
154                reason: r2,
155            },
156        ) => f1 == f2 && t1 == t2 && r1 == r2,
157        (
158            TraceEventKind::TimerFired { timer_type: t1 },
159            TraceEventKind::TimerFired { timer_type: t2 },
160        ) => t1 == t2,
161        (
162            TraceEventKind::StateTransition {
163                from_state: fs1,
164                to_state: ts1,
165                metadata: m1,
166            },
167            TraceEventKind::StateTransition {
168                from_state: fs2,
169                to_state: ts2,
170                metadata: m2,
171            },
172        ) => fs1 == fs2 && ts1 == ts2 && m1 == m2,
173        (
174            TraceEventKind::FaultInjected {
175                fault_type: ft1,
176                details: d1,
177            },
178            TraceEventKind::FaultInjected {
179                fault_type: ft2,
180                details: d2,
181            },
182        ) => ft1 == ft2 && d1 == d2,
183        (
184            TraceEventKind::FaultHealed {
185                fault_type: ft1,
186                details: d1,
187            },
188            TraceEventKind::FaultHealed {
189                fault_type: ft2,
190                details: d2,
191            },
192        ) => ft1 == ft2 && d1 == d2,
193        (
194            TraceEventKind::StorageOp {
195                op_type: ot1,
196                key_count: kc1,
197            },
198            TraceEventKind::StorageOp {
199                op_type: ot2,
200                key_count: kc2,
201            },
202        ) => ot1 == ot2 && kc1 == kc2,
203        (
204            TraceEventKind::Custom { tag: t1, data: d1 },
205            TraceEventKind::Custom { tag: t2, data: d2 },
206        ) => t1 == t2 && d1 == d2,
207        _ => false, // Different variants
208    }
209}
210
211/// Assert that a simulation is deterministic (same seed produces identical trace).
212///
213/// This is the recommended way to enforce R-11 in CI. Call this macro in your
214/// test functions to automatically run the simulation twice and assert trace identity.
215///
216/// ```rust,ignore
217/// use vortex_trace::assert_deterministic;
218///
219/// #[test]
220/// fn test_my_simulation_is_deterministic() {
221///     assert_deterministic!(42, |seed| {
222///         let mut trace = vortex_trace::SimTrace::new();
223///         // ... run your simulation ...
224///         trace
225///     });
226/// }
227/// ```
228#[macro_export]
229macro_rules! assert_deterministic {
230    ($seed:expr, $sim_fn:expr) => {{
231        let result = $crate::verify_determinism($seed, $sim_fn);
232        assert!(
233            result.is_deterministic(),
234            "DETERMINISM VIOLATION (R-11): seed={}, divergence at event {:?}: {}",
235            $seed,
236            result.divergence_at,
237            result
238                .divergence_description
239                .as_deref()
240                .unwrap_or("unknown"),
241        );
242    }};
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_deterministic_simulation() {
251        let result = verify_determinism(42, |seed| {
252            let mut trace = SimTrace::new();
253            for i in 0..10 {
254                trace.record(
255                    i,
256                    1,
257                    TraceEventKind::TimerFired {
258                        timer_type: format!("t{}", seed + i),
259                    },
260                );
261            }
262            trace
263        });
264        assert!(result.is_deterministic());
265        assert_eq!(result.run1_events, 10);
266        assert_eq!(result.run2_events, 10);
267    }
268
269    #[test]
270    fn test_non_deterministic_simulation() {
271        use std::sync::atomic::{AtomicU64, Ordering};
272        static CALL_COUNT: AtomicU64 = AtomicU64::new(0);
273
274        let result = verify_determinism(42, |_seed| {
275            let call = CALL_COUNT.fetch_add(1, Ordering::SeqCst);
276            let mut trace = SimTrace::new();
277            trace.record(
278                call,
279                1,
280                TraceEventKind::Custom {
281                    tag: "test".into(),
282                    data: format!("call={call}"),
283                },
284            );
285            trace
286        });
287        assert!(!result.is_deterministic());
288        assert_eq!(result.divergence_at, Some(0));
289    }
290
291    #[test]
292    fn test_length_mismatch() {
293        use std::sync::atomic::{AtomicU64, Ordering};
294        static CALL_COUNT2: AtomicU64 = AtomicU64::new(0);
295
296        let result = verify_determinism(42, |_seed| {
297            let call = CALL_COUNT2.fetch_add(1, Ordering::SeqCst);
298            let mut trace = SimTrace::new();
299            for i in 0..=call {
300                trace.record(
301                    i,
302                    1,
303                    TraceEventKind::TimerFired {
304                        timer_type: "t".into(),
305                    },
306                );
307            }
308            trace
309        });
310        assert!(!result.is_deterministic());
311        assert!(
312            result
313                .divergence_description
314                .unwrap()
315                .contains("length mismatch")
316        );
317    }
318
319    #[test]
320    fn test_compare_traces_identical() {
321        let mut t1 = SimTrace::new();
322        let mut t2 = SimTrace::new();
323        for i in 0..5 {
324            let kind = TraceEventKind::MessageSent {
325                to: 2,
326                msg_type: "rpc".into(),
327                size_bytes: 64,
328            };
329            t1.record(i, 1, kind.clone());
330            t2.record(i, 1, kind);
331        }
332        let result = compare_traces(&t1, &t2);
333        assert!(result.is_deterministic());
334    }
335
336    #[test]
337    fn test_compare_traces_divergent() {
338        let mut t1 = SimTrace::new();
339        let mut t2 = SimTrace::new();
340        t1.record(
341            0,
342            1,
343            TraceEventKind::TimerFired {
344                timer_type: "a".into(),
345            },
346        );
347        t2.record(
348            0,
349            1,
350            TraceEventKind::TimerFired {
351                timer_type: "b".into(),
352            },
353        );
354        let result = compare_traces(&t1, &t2);
355        assert!(!result.is_deterministic());
356        assert_eq!(result.divergence_at, Some(0));
357    }
358}