1use crate::{SimTrace, TraceEvent, TraceEventKind};
9
10#[derive(Debug, Clone)]
12pub struct DeterminismResult {
13 pub deterministic: bool,
15 pub divergence_at: Option<usize>,
17 pub divergence_description: Option<String>,
19 pub run1_events: usize,
21 pub run2_events: usize,
23}
24
25impl DeterminismResult {
26 pub fn is_deterministic(&self) -> bool {
28 self.deterministic
29 }
30}
31
32pub 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
64pub 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
104fn 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
118fn 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, }
209}
210
211#[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}