moonpool_sim/chaos/
assertions.rs

1//! Assertion macros and result tracking for simulation testing.
2//!
3//! This module provides `always_assert!` and `sometimes_assert!` macros for testing
4//! distributed system properties. Assertions are tracked using thread-local storage
5//! to enable statistical analysis of system behavior across multiple simulation runs.
6
7use std::cell::RefCell;
8use std::collections::HashMap;
9
10/// Statistics for a tracked assertion.
11///
12/// Records the total number of times an assertion was checked and how many
13/// times it succeeded, enabling calculation of success rates for probabilistic
14/// properties in distributed systems.
15#[derive(Debug, Clone, PartialEq)]
16pub struct AssertionStats {
17    /// Total number of times this assertion was evaluated
18    pub total_checks: usize,
19    /// Number of times the assertion condition was true
20    pub successes: usize,
21}
22
23impl AssertionStats {
24    /// Create new assertion statistics starting at zero.
25    pub fn new() -> Self {
26        Self {
27            total_checks: 0,
28            successes: 0,
29        }
30    }
31
32    /// Calculate the success rate as a percentage (0.0 to 100.0).
33    ///
34    /// Returns 0.0 if no checks have been performed yet.
35    ///
36    /// Calculate the success rate as a percentage (0.0 to 100.0).
37    pub fn success_rate(&self) -> f64 {
38        if self.total_checks == 0 {
39            0.0
40        } else {
41            (self.successes as f64 / self.total_checks as f64) * 100.0
42        }
43    }
44
45    /// Record a new assertion check with the given result.
46    ///
47    /// Increments total_checks and successes (if the result was true).
48    pub fn record(&mut self, success: bool) {
49        self.total_checks += 1;
50        if success {
51            self.successes += 1;
52        }
53    }
54}
55
56impl Default for AssertionStats {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62thread_local! {
63    /// Thread-local storage for assertion results.
64    ///
65    /// Each thread maintains independent assertion statistics, ensuring
66    /// proper isolation between parallel test execution while allowing
67    /// statistical collection within each simulation run.
68    static ASSERTION_RESULTS: RefCell<HashMap<String, AssertionStats>> = RefCell::new(HashMap::new());
69}
70
71/// Record an assertion result for statistical tracking.
72///
73/// This function is used internally by the `sometimes_assert!` macro to track
74/// assertion outcomes for later analysis.
75///
76/// # Parameters
77///
78/// * `name` - The assertion identifier
79/// * `success` - Whether the assertion condition was true
80pub fn record_assertion(name: &str, success: bool) {
81    ASSERTION_RESULTS.with(|results| {
82        let mut results = results.borrow_mut();
83        let stats = results.entry(name.to_string()).or_default();
84        stats.record(success);
85    });
86}
87
88/// Get current assertion statistics for all tracked assertions.
89///
90/// Returns a snapshot of assertion results for the current thread.
91/// This is typically called after simulation runs to analyze system behavior.
92///
93/// Get a snapshot of all assertion statistics collected so far.
94pub fn get_assertion_results() -> HashMap<String, AssertionStats> {
95    ASSERTION_RESULTS.with(|results| results.borrow().clone())
96}
97
98/// Reset all assertion statistics to empty state.
99///
100/// This should be called before each simulation run to ensure clean state
101/// between consecutive simulations on the same thread.
102///
103/// Clear all assertion statistics.
104pub fn reset_assertion_results() {
105    ASSERTION_RESULTS.with(|results| {
106        results.borrow_mut().clear();
107    });
108}
109
110/// Check assertion validation and panic if violations are found.
111///
112/// This is a simplified function that checks for basic assertion issues.
113/// It looks for assertions that never succeed (0% success rate).
114///
115/// # Parameters
116///
117/// * `_report` - The simulation report (unused in simplified version)
118///
119/// # Panics
120///
121/// Panics if there are assertions that never succeed.
122pub fn panic_on_assertion_violations(_report: &crate::runner::SimulationReport) {
123    let results = get_assertion_results();
124    let mut violations = Vec::new();
125
126    for (name, stats) in &results {
127        if stats.total_checks > 0 && stats.success_rate() == 0.0 {
128            violations.push(format!(
129                "sometimes_assert!('{}') has 0% success rate (expected at least 1%)",
130                name
131            ));
132        }
133    }
134
135    if !violations.is_empty() {
136        println!("❌ Assertion violations found:");
137        for violation in &violations {
138            println!("  - {}", violation);
139        }
140        panic!("❌ Unexpected assertion violations detected!");
141    } else {
142        println!("✅ All assertions passed basic validation!");
143    }
144}
145
146/// Validate that all `sometimes_assert!` assertions actually behave as "sometimes".
147///
148/// This simplified function checks that `sometimes_assert!` assertions have a
149/// success rate of at least 1%.
150///
151/// # Returns
152///
153/// A vector of violation messages, or empty if all assertions are valid.
154///
155/// Validate that all assertion contracts have been met.
156pub fn validate_assertion_contracts() -> Vec<String> {
157    let mut violations = Vec::new();
158    let results = get_assertion_results();
159
160    for (name, stats) in &results {
161        let rate = stats.success_rate();
162        if stats.total_checks > 0 && rate == 0.0 {
163            violations.push(format!(
164                "sometimes_assert!('{}') has {:.1}% success rate (expected at least 1%)",
165                name, rate
166            ));
167        }
168    }
169
170    violations
171}
172
173/// Assert that a condition is always true, panicking on failure.
174///
175/// This macro is used for conditions that must always hold in a correct
176/// distributed system implementation. If the condition fails, the simulation
177/// will panic immediately with a descriptive error message including the current seed.
178///
179/// # Parameters
180///
181/// * `name` - An identifier for this assertion (for error reporting)
182/// * `condition` - The expression to evaluate (must be boolean)
183/// * `message` - A descriptive error message to show on failure
184///
185/// Assert that a condition must always be true. Panics immediately if false.
186#[macro_export]
187macro_rules! always_assert {
188    ($name:ident, $condition:expr, $message:expr) => {
189        let result = $condition;
190        if !result {
191            let current_seed = $crate::sim::get_current_sim_seed();
192            panic!(
193                "Always assertion '{}' failed (seed: {}): {}",
194                stringify!($name),
195                current_seed,
196                $message
197            );
198        }
199    };
200}
201
202/// Assert a condition that should sometimes be true, tracking the success rate.
203///
204/// This macro is used for probabilistic properties in distributed systems,
205/// such as "consensus should usually be reached quickly" or "the system should
206/// be available most of the time". The assertion result is tracked for statistical
207/// analysis without causing the simulation to fail.
208///
209/// The macro automatically registers the assertion at compile time and tracks
210/// module execution to enable unreachable code detection.
211///
212/// # Parameters
213///
214/// * `name` - An identifier for this assertion (for tracking purposes)
215/// * `condition` - The expression to evaluate (must be boolean)
216/// * `message` - A descriptive message about what this assertion tests
217///
218/// Assert that a condition should sometimes be true. Records statistics for analysis.
219#[macro_export]
220macro_rules! sometimes_assert {
221    ($name:ident, $condition:expr, $message:expr) => {
222        // Runtime execution - simplified to just record the result
223        let result = $condition;
224        $crate::chaos::assertions::record_assertion(stringify!($name), result);
225    };
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_assertion_stats_new() {
234        let stats = AssertionStats::new();
235        assert_eq!(stats.total_checks, 0);
236        assert_eq!(stats.successes, 0);
237        assert_eq!(stats.success_rate(), 0.0);
238    }
239
240    #[test]
241    fn test_assertion_stats_record() {
242        let mut stats = AssertionStats::new();
243
244        stats.record(true);
245        assert_eq!(stats.total_checks, 1);
246        assert_eq!(stats.successes, 1);
247        assert_eq!(stats.success_rate(), 100.0);
248
249        stats.record(false);
250        assert_eq!(stats.total_checks, 2);
251        assert_eq!(stats.successes, 1);
252        assert_eq!(stats.success_rate(), 50.0);
253
254        stats.record(true);
255        assert_eq!(stats.total_checks, 3);
256        assert_eq!(stats.successes, 2);
257        let expected = 200.0 / 3.0;
258        assert!((stats.success_rate() - expected).abs() < 1e-10);
259    }
260
261    #[test]
262    fn test_assertion_stats_success_rate_edge_cases() {
263        let mut stats = AssertionStats::new();
264        assert_eq!(stats.success_rate(), 0.0);
265
266        stats.record(false);
267        assert_eq!(stats.success_rate(), 0.0);
268
269        stats.record(true);
270        assert_eq!(stats.success_rate(), 50.0);
271    }
272
273    #[test]
274    fn test_record_assertion_and_get_results() {
275        reset_assertion_results();
276
277        record_assertion("test1", true);
278        record_assertion("test1", false);
279        record_assertion("test2", true);
280
281        let results = get_assertion_results();
282        assert_eq!(results.len(), 2);
283
284        let test1_stats = &results["test1"];
285        assert_eq!(test1_stats.total_checks, 2);
286        assert_eq!(test1_stats.successes, 1);
287        assert_eq!(test1_stats.success_rate(), 50.0);
288
289        let test2_stats = &results["test2"];
290        assert_eq!(test2_stats.total_checks, 1);
291        assert_eq!(test2_stats.successes, 1);
292        assert_eq!(test2_stats.success_rate(), 100.0);
293    }
294
295    #[test]
296    fn test_reset_assertion_results() {
297        record_assertion("test", true);
298        assert!(!get_assertion_results().is_empty());
299
300        reset_assertion_results();
301        assert!(get_assertion_results().is_empty());
302    }
303
304    #[test]
305    fn test_always_assert_success() {
306        reset_assertion_results();
307
308        let value = 42;
309        always_assert!(value_is_42, value == 42, "Value should be 42");
310
311        // always_assert! no longer tracks successful assertions
312        // It only panics on failure, so successful calls leave no trace
313        let results = get_assertion_results();
314        assert!(
315            results.is_empty(),
316            "always_assert! should not be tracked when successful"
317        );
318    }
319
320    #[test]
321    #[should_panic(
322        expected = "Always assertion 'impossible' failed (seed: 0): This should never happen"
323    )]
324    fn test_always_assert_failure() {
325        let value = 42;
326        always_assert!(impossible, value == 0, "This should never happen");
327    }
328
329    #[test]
330    fn test_sometimes_assert() {
331        reset_assertion_results();
332
333        let fast_time = 50;
334        let slow_time = 150;
335        let threshold = 100;
336
337        sometimes_assert!(
338            fast_operation,
339            fast_time < threshold,
340            "Operation should be fast"
341        );
342        sometimes_assert!(
343            fast_operation,
344            slow_time < threshold,
345            "Operation should be fast"
346        );
347
348        let results = get_assertion_results();
349        let stats = &results["fast_operation"];
350        assert_eq!(stats.total_checks, 2);
351        assert_eq!(stats.successes, 1);
352        assert_eq!(stats.success_rate(), 50.0);
353    }
354
355    #[test]
356    fn test_assertion_isolation_between_tests() {
357        // This test verifies that assertion results are isolated between tests
358        reset_assertion_results();
359
360        record_assertion("isolation_test", true);
361        let results = get_assertion_results();
362        assert_eq!(results["isolation_test"].total_checks, 1);
363
364        // The isolation is ensured by thread-local storage and explicit resets
365    }
366
367    #[test]
368    fn test_multiple_assertions_same_name() {
369        reset_assertion_results();
370
371        sometimes_assert!(reliability, true, "System should be reliable");
372        sometimes_assert!(reliability, false, "System should be reliable");
373        sometimes_assert!(reliability, true, "System should be reliable");
374        sometimes_assert!(reliability, true, "System should be reliable");
375
376        let results = get_assertion_results();
377        let stats = &results["reliability"];
378        assert_eq!(stats.total_checks, 4);
379        assert_eq!(stats.successes, 3);
380        assert_eq!(stats.success_rate(), 75.0);
381    }
382
383    #[test]
384    fn test_complex_assertion_conditions() {
385        reset_assertion_results();
386
387        let items = [1, 2, 3, 4, 5];
388        let sum: i32 = items.iter().sum();
389
390        sometimes_assert!(
391            sum_in_range,
392            (10..=20).contains(&sum),
393            "Sum should be in reasonable range"
394        );
395
396        always_assert!(
397            not_empty,
398            !items.is_empty(),
399            "Items list should not be empty"
400        );
401
402        let results = get_assertion_results();
403        // Only sometimes_assert! is tracked now
404        assert_eq!(results.len(), 1, "Only sometimes_assert should be tracked");
405        assert_eq!(results["sum_in_range"].success_rate(), 100.0);
406        // always_assert! no longer appears in results when successful
407        assert!(
408            !results.contains_key("not_empty"),
409            "always_assert should not be tracked"
410        );
411    }
412
413    #[test]
414    fn test_sometimes_assert_macro() {
415        reset_assertion_results();
416
417        // Test the simplified macro functionality
418        sometimes_assert!(macro_test, true, "Test assertion");
419        sometimes_assert!(macro_test, false, "Test assertion");
420
421        let results = get_assertion_results();
422        assert!(results.contains_key("macro_test"));
423        assert_eq!(results["macro_test"].total_checks, 2);
424        assert_eq!(results["macro_test"].successes, 1);
425
426        // Should not have violations (50% success rate is valid)
427        let violations = validate_assertion_contracts();
428        assert!(violations.is_empty());
429    }
430}