Skip to main content

moonpool_sim/chaos/
assertions.rs

1//! Antithesis-style assertion macros and result tracking for simulation testing.
2//!
3//! This module provides 15 assertion macros for testing distributed system
4//! properties. Assertions are tracked in shared memory via moonpool-explorer,
5//! enabling cross-process tracking across forked exploration timelines.
6//!
7//! Following the Antithesis principle: **assertions never crash your program**.
8//! Always-type assertions log violations at ERROR level and record them via a
9//! thread-local flag, allowing the simulation to continue running and discover
10//! cascading failures. The simulation runner checks `has_always_violations()`
11//! after each iteration to report failures through the normal result pipeline.
12//!
13//! # Assertion Kinds
14//!
15//! | Macro | Tracks | Panics | Forks |
16//! |-------|--------|--------|-------|
17//! | `assert_always!` | yes | no | no |
18//! | `assert_always_or_unreachable!` | yes | no | no |
19//! | `assert_sometimes!` | yes | no | on first success |
20//! | `assert_reachable!` | yes | no | on first reach |
21//! | `assert_unreachable!` | yes | no | no |
22//! | `assert_always_greater_than!` | yes | no | no |
23//! | `assert_always_greater_than_or_equal_to!` | yes | no | no |
24//! | `assert_always_less_than!` | yes | no | no |
25//! | `assert_always_less_than_or_equal_to!` | yes | no | no |
26//! | `assert_sometimes_greater_than!` | yes | no | on watermark improvement |
27//! | `assert_sometimes_greater_than_or_equal_to!` | yes | no | on watermark improvement |
28//! | `assert_sometimes_less_than!` | yes | no | on watermark improvement |
29//! | `assert_sometimes_less_than_or_equal_to!` | yes | no | on watermark improvement |
30//! | `assert_sometimes_all!` | yes | no | on frontier advance |
31//! | `assert_sometimes_each!` | yes | no | on discovery/quality |
32
33use std::cell::Cell;
34use std::collections::HashMap;
35
36// =============================================================================
37// Thread-local violation tracking (Antithesis-style: never panic)
38// =============================================================================
39
40thread_local! {
41    static ALWAYS_VIOLATION_COUNT: Cell<u64> = const { Cell::new(0) };
42    /// When set, the next call to [`reset_assertion_results`] is skipped.
43    /// Used by multi-seed exploration to prevent `SimWorld::create` from
44    /// zeroing assertion state that [`moonpool_explorer::prepare_next_seed`]
45    /// already selectively reset.
46    static SKIP_NEXT_ASSERTION_RESET: Cell<bool> = const { Cell::new(false) };
47}
48
49/// Record that an always-type assertion was violated during this iteration.
50///
51/// Called by always-type macros instead of panicking. The simulation runner
52/// checks `has_always_violations()` after each iteration to report failures.
53pub fn record_always_violation() {
54    ALWAYS_VIOLATION_COUNT.with(|c| c.set(c.get() + 1));
55}
56
57/// Reset the violation counter. Must be called at the start of each iteration.
58pub fn reset_always_violations() {
59    ALWAYS_VIOLATION_COUNT.with(|c| c.set(0));
60}
61
62/// Check whether any always-type assertion was violated during this iteration.
63pub fn has_always_violations() -> bool {
64    ALWAYS_VIOLATION_COUNT.with(|c| c.get() > 0)
65}
66
67/// Statistics for a tracked assertion.
68///
69/// Records the total number of times an assertion was checked and how many
70/// times it succeeded, enabling calculation of success rates for probabilistic
71/// properties in distributed systems.
72#[derive(Debug, Clone, PartialEq)]
73pub struct AssertionStats {
74    /// Total number of times this assertion was evaluated
75    pub total_checks: usize,
76    /// Number of times the assertion condition was true
77    pub successes: usize,
78}
79
80impl AssertionStats {
81    /// Create new assertion statistics starting at zero.
82    pub fn new() -> Self {
83        Self {
84            total_checks: 0,
85            successes: 0,
86        }
87    }
88
89    /// Calculate the success rate as a percentage (0.0 to 100.0).
90    ///
91    /// Returns 0.0 if no checks have been performed yet.
92    pub fn success_rate(&self) -> f64 {
93        if self.total_checks == 0 {
94            0.0
95        } else {
96            (self.successes as f64 / self.total_checks as f64) * 100.0
97        }
98    }
99
100    /// Record a new assertion check with the given result.
101    ///
102    /// Increments total_checks and successes (if the result was true).
103    pub fn record(&mut self, success: bool) {
104        self.total_checks += 1;
105        if success {
106            self.successes += 1;
107        }
108    }
109}
110
111impl Default for AssertionStats {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117// =============================================================================
118// Thin backing wrappers (for $crate:: macro hygiene)
119// =============================================================================
120
121/// Boolean assertion backing wrapper.
122///
123/// Delegates to `moonpool_explorer::assertion_bool`.
124/// Accepts both `&str` and `String` message arguments.
125pub fn on_assertion_bool(
126    msg: impl AsRef<str>,
127    condition: bool,
128    kind: moonpool_explorer::AssertKind,
129    must_hit: bool,
130) {
131    moonpool_explorer::assertion_bool(kind, must_hit, condition, msg.as_ref());
132}
133
134/// Numeric assertion backing wrapper.
135///
136/// Delegates to `moonpool_explorer::assertion_numeric`.
137/// Accepts both `&str` and `String` message arguments.
138pub fn on_assertion_numeric(
139    msg: impl AsRef<str>,
140    value: i64,
141    cmp: moonpool_explorer::AssertCmp,
142    threshold: i64,
143    kind: moonpool_explorer::AssertKind,
144    maximize: bool,
145) {
146    moonpool_explorer::assertion_numeric(kind, cmp, maximize, value, threshold, msg.as_ref());
147}
148
149/// Compound boolean assertion backing wrapper.
150///
151/// Delegates to `moonpool_explorer::assertion_sometimes_all`.
152pub fn on_assertion_sometimes_all(msg: impl AsRef<str>, named_bools: &[(&str, bool)]) {
153    moonpool_explorer::assertion_sometimes_all(msg.as_ref(), named_bools);
154}
155
156/// Notify the exploration framework of a per-value bucketed assertion.
157///
158/// This delegates to moonpool-explorer's EachBucket infrastructure for
159/// fork-based exploration with identity keys and quality watermarks.
160pub fn on_sometimes_each(msg: &str, keys: &[(&str, i64)], quality: &[(&str, i64)]) {
161    moonpool_explorer::assertion_sometimes_each(msg, keys, quality);
162}
163
164// =============================================================================
165// Shared-memory-based result collection
166// =============================================================================
167
168/// Get current assertion statistics for all tracked assertions.
169///
170/// Reads from shared memory assertion slots. Returns a snapshot of assertion
171/// results for reporting and validation.
172pub fn get_assertion_results() -> HashMap<String, AssertionStats> {
173    let slots = moonpool_explorer::assertion_read_all();
174    let mut results = HashMap::new();
175
176    for slot in &slots {
177        let total = slot.pass_count.saturating_add(slot.fail_count) as usize;
178        if total == 0 {
179            continue;
180        }
181        results.insert(
182            slot.msg.clone(),
183            AssertionStats {
184                total_checks: total,
185                successes: slot.pass_count as usize,
186            },
187        );
188    }
189
190    results
191}
192
193/// Request that the next call to [`reset_assertion_results`] be skipped.
194///
195/// Used by multi-seed exploration: [`moonpool_explorer::prepare_next_seed`]
196/// does a selective reset (preserving explored map and watermarks), so the
197/// full zero in `SimWorld::create` must be suppressed.
198pub fn skip_next_assertion_reset() {
199    SKIP_NEXT_ASSERTION_RESET.with(|c| c.set(true));
200}
201
202/// Reset all assertion statistics.
203///
204/// Zeros the shared memory assertion table unless a skip was requested via
205/// [`skip_next_assertion_reset`]. Should be called before each simulation
206/// run to ensure clean state between consecutive simulations.
207pub fn reset_assertion_results() {
208    let skip = SKIP_NEXT_ASSERTION_RESET.with(|c| {
209        let v = c.get();
210        c.set(false); // always consume the flag
211        v
212    });
213    if !skip {
214        moonpool_explorer::reset_assertions();
215    }
216}
217
218/// Panic if the report contains assertion violations.
219///
220/// Uses the pre-collected `assertion_violations` from the report rather than
221/// re-reading shared memory (which may already be freed by the time this is
222/// called).
223///
224/// # Panics
225///
226/// Panics if `report.assertion_violations` is non-empty.
227pub fn panic_on_assertion_violations(report: &crate::runner::SimulationReport) {
228    if !report.assertion_violations.is_empty() {
229        eprintln!("Assertion violations found:");
230        for violation in &report.assertion_violations {
231            eprintln!("  - {}", violation);
232        }
233        panic!("Unexpected assertion violations detected!");
234    }
235}
236
237/// Validate all assertion contracts based on their kind.
238///
239/// Returns two vectors:
240/// - **always_violations**: Definite bugs — always-type assertions that failed,
241///   or unreachable code that was reached.  Safe to check with any iteration count.
242/// - **coverage_violations**: Statistical — sometimes-type assertions that were
243///   never satisfied, or reachable code that was never reached.  Only meaningful
244///   with enough iterations for statistical coverage.
245pub fn validate_assertion_contracts() -> (Vec<String>, Vec<String>) {
246    let mut always_violations = Vec::new();
247    let mut coverage_violations = Vec::new();
248    let slots = moonpool_explorer::assertion_read_all();
249
250    for slot in &slots {
251        let total = slot.pass_count.saturating_add(slot.fail_count);
252        let kind = moonpool_explorer::AssertKind::from_u8(slot.kind);
253
254        match kind {
255            Some(moonpool_explorer::AssertKind::Always) => {
256                if slot.fail_count > 0 {
257                    always_violations.push(format!(
258                        "assert_always!('{}') failed {} times out of {}",
259                        slot.msg, slot.fail_count, total
260                    ));
261                }
262                if slot.must_hit != 0 && total == 0 {
263                    always_violations
264                        .push(format!("assert_always!('{}') was never reached", slot.msg));
265                }
266            }
267            Some(moonpool_explorer::AssertKind::AlwaysOrUnreachable) => {
268                if slot.fail_count > 0 {
269                    always_violations.push(format!(
270                        "assert_always_or_unreachable!('{}') failed {} times out of {}",
271                        slot.msg, slot.fail_count, total
272                    ));
273                }
274            }
275            Some(moonpool_explorer::AssertKind::Sometimes) => {
276                if total > 0 && slot.pass_count == 0 {
277                    coverage_violations.push(format!(
278                        "assert_sometimes!('{}') has 0% success rate ({} checks)",
279                        slot.msg, total
280                    ));
281                }
282            }
283            Some(moonpool_explorer::AssertKind::Reachable) => {
284                if slot.pass_count == 0 {
285                    coverage_violations.push(format!(
286                        "assert_reachable!('{}') was never reached",
287                        slot.msg
288                    ));
289                }
290            }
291            Some(moonpool_explorer::AssertKind::Unreachable) => {
292                if slot.pass_count > 0 {
293                    always_violations.push(format!(
294                        "assert_unreachable!('{}') was reached {} times",
295                        slot.msg, slot.pass_count
296                    ));
297                }
298            }
299            Some(moonpool_explorer::AssertKind::NumericAlways) => {
300                if slot.fail_count > 0 {
301                    always_violations.push(format!(
302                        "numeric assert_always ('{}') failed {} times out of {}",
303                        slot.msg, slot.fail_count, total
304                    ));
305                }
306            }
307            Some(moonpool_explorer::AssertKind::NumericSometimes) => {
308                if total > 0 && slot.pass_count == 0 {
309                    coverage_violations.push(format!(
310                        "numeric assert_sometimes ('{}') has 0% success rate ({} checks)",
311                        slot.msg, total
312                    ));
313                }
314            }
315            Some(moonpool_explorer::AssertKind::BooleanSometimesAll) | None => {
316                // BooleanSometimesAll: no simple pass/fail violation contract
317                // (the frontier tracking is the guidance mechanism)
318            }
319        }
320    }
321
322    (always_violations, coverage_violations)
323}
324
325// =============================================================================
326// Assertion Macros
327// =============================================================================
328
329/// Assert that a condition is always true.
330///
331/// Tracks pass/fail in shared memory for cross-process visibility.
332/// Does **not** panic — records the violation via `record_always_violation()`
333/// and logs at ERROR level with the seed, following the Antithesis principle
334/// that assertions never crash the program.
335#[macro_export]
336macro_rules! assert_always {
337    ($condition:expr, $message:expr) => {
338        let __msg = $message;
339        let cond = $condition;
340        $crate::chaos::assertions::on_assertion_bool(
341            &__msg,
342            cond,
343            $crate::chaos::assertions::_re_export::AssertKind::Always,
344            true,
345        );
346        if !cond {
347            let seed = $crate::sim::get_current_sim_seed();
348            tracing::error!("[ALWAYS FAILED] seed={} — {}", seed, __msg);
349            $crate::chaos::assertions::record_always_violation();
350        }
351    };
352}
353
354/// Assert that a condition is always true when reached, but the code path
355/// need not be reached. Does not panic if never evaluated.
356///
357/// Does **not** panic on failure — records the violation and logs at ERROR level.
358#[macro_export]
359macro_rules! assert_always_or_unreachable {
360    ($condition:expr, $message:expr) => {
361        let __msg = $message;
362        let cond = $condition;
363        $crate::chaos::assertions::on_assertion_bool(
364            &__msg,
365            cond,
366            $crate::chaos::assertions::_re_export::AssertKind::AlwaysOrUnreachable,
367            false,
368        );
369        if !cond {
370            let seed = $crate::sim::get_current_sim_seed();
371            tracing::error!("[ALWAYS_OR_UNREACHABLE FAILED] seed={} — {}", seed, __msg);
372            $crate::chaos::assertions::record_always_violation();
373        }
374    };
375}
376
377/// Assert a condition that should sometimes be true, tracking stats and triggering exploration.
378///
379/// Does not panic. On first success, triggers a fork to explore alternate timelines.
380#[macro_export]
381macro_rules! assert_sometimes {
382    ($condition:expr, $message:expr) => {
383        $crate::chaos::assertions::on_assertion_bool(
384            &$message,
385            $condition,
386            $crate::chaos::assertions::_re_export::AssertKind::Sometimes,
387            true,
388        );
389    };
390}
391
392/// Assert that a code path is reachable (should be reached at least once).
393///
394/// Does not panic. On first reach, triggers a fork.
395#[macro_export]
396macro_rules! assert_reachable {
397    ($message:expr) => {
398        $crate::chaos::assertions::on_assertion_bool(
399            &$message,
400            true,
401            $crate::chaos::assertions::_re_export::AssertKind::Reachable,
402            true,
403        );
404    };
405}
406
407/// Assert that a code path should never be reached.
408///
409/// Does **not** panic — records the violation and logs at ERROR level.
410/// Tracks in shared memory for reporting.
411#[macro_export]
412macro_rules! assert_unreachable {
413    ($message:expr) => {
414        let __msg = $message;
415        $crate::chaos::assertions::on_assertion_bool(
416            &__msg,
417            true,
418            $crate::chaos::assertions::_re_export::AssertKind::Unreachable,
419            false,
420        );
421        let seed = $crate::sim::get_current_sim_seed();
422        tracing::error!("[UNREACHABLE REACHED] seed={} — {}", seed, __msg);
423        $crate::chaos::assertions::record_always_violation();
424    };
425}
426
427/// Assert that `val > threshold` always holds.
428///
429/// Does **not** panic on failure — records the violation and logs at ERROR level.
430#[macro_export]
431macro_rules! assert_always_greater_than {
432    ($val:expr, $thresh:expr, $message:expr) => {
433        let __msg = $message;
434        let __v = $val as i64;
435        let __t = $thresh as i64;
436        $crate::chaos::assertions::on_assertion_numeric(
437            &__msg,
438            __v,
439            $crate::chaos::assertions::_re_export::AssertCmp::Gt,
440            __t,
441            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
442            false,
443        );
444        if !(__v > __t) {
445            let seed = $crate::sim::get_current_sim_seed();
446            tracing::error!(
447                "[NUMERIC ALWAYS FAILED] seed={} — {} (val={}, thresh={})",
448                seed,
449                __msg,
450                __v,
451                __t
452            );
453            $crate::chaos::assertions::record_always_violation();
454        }
455    };
456}
457
458/// Assert that `val >= threshold` always holds.
459///
460/// Does **not** panic on failure — records the violation and logs at ERROR level.
461#[macro_export]
462macro_rules! assert_always_greater_than_or_equal_to {
463    ($val:expr, $thresh:expr, $message:expr) => {
464        let __msg = $message;
465        let __v = $val as i64;
466        let __t = $thresh as i64;
467        $crate::chaos::assertions::on_assertion_numeric(
468            &__msg,
469            __v,
470            $crate::chaos::assertions::_re_export::AssertCmp::Ge,
471            __t,
472            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
473            false,
474        );
475        if !(__v >= __t) {
476            let seed = $crate::sim::get_current_sim_seed();
477            tracing::error!(
478                "[NUMERIC ALWAYS FAILED] seed={} — {} (val={}, thresh={})",
479                seed,
480                __msg,
481                __v,
482                __t
483            );
484            $crate::chaos::assertions::record_always_violation();
485        }
486    };
487}
488
489/// Assert that `val < threshold` always holds.
490///
491/// Does **not** panic on failure — records the violation and logs at ERROR level.
492#[macro_export]
493macro_rules! assert_always_less_than {
494    ($val:expr, $thresh:expr, $message:expr) => {
495        let __msg = $message;
496        let __v = $val as i64;
497        let __t = $thresh as i64;
498        $crate::chaos::assertions::on_assertion_numeric(
499            &__msg,
500            __v,
501            $crate::chaos::assertions::_re_export::AssertCmp::Lt,
502            __t,
503            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
504            true,
505        );
506        if !(__v < __t) {
507            let seed = $crate::sim::get_current_sim_seed();
508            tracing::error!(
509                "[NUMERIC ALWAYS FAILED] seed={} — {} (val={}, thresh={})",
510                seed,
511                __msg,
512                __v,
513                __t
514            );
515            $crate::chaos::assertions::record_always_violation();
516        }
517    };
518}
519
520/// Assert that `val <= threshold` always holds.
521///
522/// Does **not** panic on failure — records the violation and logs at ERROR level.
523#[macro_export]
524macro_rules! assert_always_less_than_or_equal_to {
525    ($val:expr, $thresh:expr, $message:expr) => {
526        let __msg = $message;
527        let __v = $val as i64;
528        let __t = $thresh as i64;
529        $crate::chaos::assertions::on_assertion_numeric(
530            &__msg,
531            __v,
532            $crate::chaos::assertions::_re_export::AssertCmp::Le,
533            __t,
534            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
535            true,
536        );
537        if !(__v <= __t) {
538            let seed = $crate::sim::get_current_sim_seed();
539            tracing::error!(
540                "[NUMERIC ALWAYS FAILED] seed={} — {} (val={}, thresh={})",
541                seed,
542                __msg,
543                __v,
544                __t
545            );
546            $crate::chaos::assertions::record_always_violation();
547        }
548    };
549}
550
551/// Assert that `val > threshold` sometimes holds. Forks on watermark improvement.
552#[macro_export]
553macro_rules! assert_sometimes_greater_than {
554    ($val:expr, $thresh:expr, $message:expr) => {
555        $crate::chaos::assertions::on_assertion_numeric(
556            &$message,
557            $val as i64,
558            $crate::chaos::assertions::_re_export::AssertCmp::Gt,
559            $thresh as i64,
560            $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
561            true,
562        );
563    };
564}
565
566/// Assert that `val >= threshold` sometimes holds. Forks on watermark improvement.
567#[macro_export]
568macro_rules! assert_sometimes_greater_than_or_equal_to {
569    ($val:expr, $thresh:expr, $message:expr) => {
570        $crate::chaos::assertions::on_assertion_numeric(
571            &$message,
572            $val as i64,
573            $crate::chaos::assertions::_re_export::AssertCmp::Ge,
574            $thresh as i64,
575            $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
576            true,
577        );
578    };
579}
580
581/// Assert that `val < threshold` sometimes holds. Forks on watermark improvement.
582#[macro_export]
583macro_rules! assert_sometimes_less_than {
584    ($val:expr, $thresh:expr, $message:expr) => {
585        $crate::chaos::assertions::on_assertion_numeric(
586            &$message,
587            $val as i64,
588            $crate::chaos::assertions::_re_export::AssertCmp::Lt,
589            $thresh as i64,
590            $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
591            false,
592        );
593    };
594}
595
596/// Assert that `val <= threshold` sometimes holds. Forks on watermark improvement.
597#[macro_export]
598macro_rules! assert_sometimes_less_than_or_equal_to {
599    ($val:expr, $thresh:expr, $message:expr) => {
600        $crate::chaos::assertions::on_assertion_numeric(
601            &$message,
602            $val as i64,
603            $crate::chaos::assertions::_re_export::AssertCmp::Le,
604            $thresh as i64,
605            $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
606            false,
607        );
608    };
609}
610
611/// Compound boolean assertion: all named bools should sometimes be true simultaneously.
612///
613/// Tracks a frontier (max number of simultaneously true bools). Forks when
614/// the frontier advances.
615///
616/// # Usage
617///
618/// ```ignore
619/// assert_sometimes_all!("all_nodes_healthy", [
620///     ("node_a", node_a_healthy),
621///     ("node_b", node_b_healthy),
622///     ("node_c", node_c_healthy),
623/// ]);
624/// ```
625#[macro_export]
626macro_rules! assert_sometimes_all {
627    ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ]) => {
628        $crate::chaos::assertions::on_assertion_sometimes_all($msg, &[ $(($name, $val)),+ ])
629    };
630}
631
632/// Per-value bucketed sometimes assertion with optional quality watermarks.
633///
634/// Each unique combination of identity keys gets its own bucket. On first
635/// discovery of a new bucket, a fork is triggered for exploration. If quality
636/// keys are provided, re-forks when quality improves.
637///
638/// # Usage
639///
640/// ```ignore
641/// // Identity keys only
642/// assert_sometimes_each!("gate", [("lock", lock_id), ("depth", depth)]);
643///
644/// // With quality watermarks
645/// assert_sometimes_each!("descended", [("to_floor", floor)], [("health", hp)]);
646/// ```
647#[macro_export]
648macro_rules! assert_sometimes_each {
649    ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ]) => {
650        $crate::chaos::assertions::on_sometimes_each($msg, &[ $(($name, $val as i64)),+ ], &[])
651    };
652    ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ], [ $(($qname:expr, $qval:expr)),+ $(,)? ]) => {
653        $crate::chaos::assertions::on_sometimes_each(
654            $msg,
655            &[ $(($name, $val as i64)),+ ],
656            &[ $(($qname, $qval as i64)),+ ],
657        )
658    };
659}
660
661/// Re-exports for macro hygiene (`$crate::chaos::assertions::_re_export::*`).
662pub mod _re_export {
663    pub use moonpool_explorer::{AssertCmp, AssertKind};
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn test_assertion_stats_new() {
672        let stats = AssertionStats::new();
673        assert_eq!(stats.total_checks, 0);
674        assert_eq!(stats.successes, 0);
675        assert_eq!(stats.success_rate(), 0.0);
676    }
677
678    #[test]
679    fn test_assertion_stats_record() {
680        let mut stats = AssertionStats::new();
681
682        stats.record(true);
683        assert_eq!(stats.total_checks, 1);
684        assert_eq!(stats.successes, 1);
685        assert_eq!(stats.success_rate(), 100.0);
686
687        stats.record(false);
688        assert_eq!(stats.total_checks, 2);
689        assert_eq!(stats.successes, 1);
690        assert_eq!(stats.success_rate(), 50.0);
691
692        stats.record(true);
693        assert_eq!(stats.total_checks, 3);
694        assert_eq!(stats.successes, 2);
695        let expected = 200.0 / 3.0;
696        assert!((stats.success_rate() - expected).abs() < 1e-10);
697    }
698
699    #[test]
700    fn test_assertion_stats_success_rate_edge_cases() {
701        let mut stats = AssertionStats::new();
702        assert_eq!(stats.success_rate(), 0.0);
703
704        stats.record(false);
705        assert_eq!(stats.success_rate(), 0.0);
706
707        stats.record(true);
708        assert_eq!(stats.success_rate(), 50.0);
709    }
710
711    #[test]
712    fn test_get_assertion_results_empty() {
713        // When no assertions have been tracked, results should be empty
714        // (assertion table not initialized = empty)
715        let results = get_assertion_results();
716        // May or may not be empty depending on prior test state,
717        // but should not panic
718        let _ = results;
719    }
720
721    #[test]
722    fn test_validate_contracts_empty() {
723        // Should produce no violations when no assertions tracked
724        let violations = validate_assertion_contracts();
725        // May or may not be empty, but should not panic
726        let _ = violations;
727    }
728}