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// Details formatting (log-only, never stored in shared memory)
119// =============================================================================
120
121/// Format key-value details for assertion logging.
122///
123/// Used by assertion macros to produce human-readable context on failure.
124/// Output format: `key1=val1, key2=val2, ...`
125pub fn format_details(pairs: &[(&str, &dyn std::fmt::Display)]) -> String {
126    pairs
127        .iter()
128        .map(|(k, v)| format!("{}={}", k, v))
129        .collect::<Vec<_>>()
130        .join(", ")
131}
132
133// =============================================================================
134// Thin backing wrappers (for $crate:: macro hygiene)
135// =============================================================================
136
137/// Boolean assertion backing wrapper.
138///
139/// Delegates to `moonpool_explorer::assertion_bool`.
140/// Accepts both `&str` and `String` message arguments.
141pub fn on_assertion_bool(
142    msg: impl AsRef<str>,
143    condition: bool,
144    kind: moonpool_explorer::AssertKind,
145    must_hit: bool,
146) {
147    moonpool_explorer::assertion_bool(kind, must_hit, condition, msg.as_ref());
148}
149
150/// Numeric assertion backing wrapper.
151///
152/// Delegates to `moonpool_explorer::assertion_numeric`.
153/// Accepts both `&str` and `String` message arguments.
154pub fn on_assertion_numeric(
155    msg: impl AsRef<str>,
156    value: i64,
157    cmp: moonpool_explorer::AssertCmp,
158    threshold: i64,
159    kind: moonpool_explorer::AssertKind,
160    maximize: bool,
161) {
162    moonpool_explorer::assertion_numeric(kind, cmp, maximize, value, threshold, msg.as_ref());
163}
164
165/// Compound boolean assertion backing wrapper.
166///
167/// Delegates to `moonpool_explorer::assertion_sometimes_all`.
168pub fn on_assertion_sometimes_all(msg: impl AsRef<str>, named_bools: &[(&str, bool)]) {
169    moonpool_explorer::assertion_sometimes_all(msg.as_ref(), named_bools);
170}
171
172/// Notify the exploration framework of a per-value bucketed assertion.
173///
174/// This delegates to moonpool-explorer's EachBucket infrastructure for
175/// fork-based exploration with identity keys and quality watermarks.
176pub fn on_sometimes_each(msg: &str, keys: &[(&str, i64)], quality: &[(&str, i64)]) {
177    moonpool_explorer::assertion_sometimes_each(msg, keys, quality);
178}
179
180// =============================================================================
181// Shared-memory-based result collection
182// =============================================================================
183
184/// Get current assertion statistics for all tracked assertions.
185///
186/// Reads from shared memory assertion slots. Returns a snapshot of assertion
187/// results for reporting and validation.
188pub fn get_assertion_results() -> HashMap<String, AssertionStats> {
189    let slots = moonpool_explorer::assertion_read_all();
190    let mut results = HashMap::new();
191
192    for slot in &slots {
193        let total = slot.pass_count.saturating_add(slot.fail_count) as usize;
194        if total == 0 {
195            continue;
196        }
197        results.insert(
198            slot.msg.clone(),
199            AssertionStats {
200                total_checks: total,
201                successes: slot.pass_count as usize,
202            },
203        );
204    }
205
206    results
207}
208
209/// Request that the next call to [`reset_assertion_results`] be skipped.
210///
211/// Used by multi-seed exploration: [`moonpool_explorer::prepare_next_seed`]
212/// does a selective reset (preserving explored map and watermarks), so the
213/// full zero in `SimWorld::create` must be suppressed.
214pub fn skip_next_assertion_reset() {
215    SKIP_NEXT_ASSERTION_RESET.with(|c| c.set(true));
216}
217
218/// Reset all assertion statistics.
219///
220/// Zeros the shared memory assertion table unless a skip was requested via
221/// [`skip_next_assertion_reset`]. Should be called before each simulation
222/// run to ensure clean state between consecutive simulations.
223pub fn reset_assertion_results() {
224    let skip = SKIP_NEXT_ASSERTION_RESET.with(|c| {
225        let v = c.get();
226        c.set(false); // always consume the flag
227        v
228    });
229    if !skip {
230        moonpool_explorer::reset_assertions();
231    }
232}
233
234/// Panic if the report contains assertion violations.
235///
236/// Uses the pre-collected `assertion_violations` from the report rather than
237/// re-reading shared memory (which may already be freed by the time this is
238/// called).
239///
240/// # Panics
241///
242/// Panics if `report.assertion_violations` is non-empty.
243pub fn panic_on_assertion_violations(report: &crate::runner::SimulationReport) {
244    if !report.assertion_violations.is_empty() {
245        eprintln!("Assertion violations found:");
246        for violation in &report.assertion_violations {
247            eprintln!("  - {}", violation);
248        }
249        panic!("Unexpected assertion violations detected!");
250    }
251}
252
253/// Validate all assertion contracts based on their kind.
254///
255/// Returns two vectors:
256/// - **always_violations**: Definite bugs — always-type assertions that failed,
257///   or unreachable code that was reached.  Safe to check with any iteration count.
258/// - **coverage_violations**: Statistical — sometimes-type assertions that were
259///   never satisfied, or reachable code that was never reached.  Only meaningful
260///   with enough iterations for statistical coverage.
261pub fn validate_assertion_contracts() -> (Vec<String>, Vec<String>) {
262    let mut always_violations = Vec::new();
263    let mut coverage_violations = Vec::new();
264    let slots = moonpool_explorer::assertion_read_all();
265
266    for slot in &slots {
267        let total = slot.pass_count.saturating_add(slot.fail_count);
268        let kind = moonpool_explorer::AssertKind::from_u8(slot.kind);
269
270        match kind {
271            Some(moonpool_explorer::AssertKind::Always) => {
272                if slot.fail_count > 0 {
273                    always_violations.push(format!(
274                        "assert_always!('{}') failed {} times out of {}",
275                        slot.msg, slot.fail_count, total
276                    ));
277                }
278                if slot.must_hit != 0 && total == 0 {
279                    always_violations
280                        .push(format!("assert_always!('{}') was never reached", slot.msg));
281                }
282            }
283            Some(moonpool_explorer::AssertKind::AlwaysOrUnreachable) => {
284                if slot.fail_count > 0 {
285                    always_violations.push(format!(
286                        "assert_always_or_unreachable!('{}') failed {} times out of {}",
287                        slot.msg, slot.fail_count, total
288                    ));
289                }
290            }
291            Some(moonpool_explorer::AssertKind::Sometimes) => {
292                if total > 0 && slot.pass_count == 0 {
293                    coverage_violations.push(format!(
294                        "assert_sometimes!('{}') has 0% success rate ({} checks)",
295                        slot.msg, total
296                    ));
297                }
298            }
299            Some(moonpool_explorer::AssertKind::Reachable) => {
300                if slot.pass_count == 0 {
301                    coverage_violations.push(format!(
302                        "assert_reachable!('{}') was never reached",
303                        slot.msg
304                    ));
305                }
306            }
307            Some(moonpool_explorer::AssertKind::Unreachable) => {
308                if slot.pass_count > 0 {
309                    always_violations.push(format!(
310                        "assert_unreachable!('{}') was reached {} times",
311                        slot.msg, slot.pass_count
312                    ));
313                }
314            }
315            Some(moonpool_explorer::AssertKind::NumericAlways) => {
316                if slot.fail_count > 0 {
317                    always_violations.push(format!(
318                        "numeric assert_always ('{}') failed {} times out of {}",
319                        slot.msg, slot.fail_count, total
320                    ));
321                }
322            }
323            Some(moonpool_explorer::AssertKind::NumericSometimes) => {
324                if total > 0 && slot.pass_count == 0 {
325                    coverage_violations.push(format!(
326                        "numeric assert_sometimes ('{}') has 0% success rate ({} checks)",
327                        slot.msg, total
328                    ));
329                }
330            }
331            Some(moonpool_explorer::AssertKind::BooleanSometimesAll) | None => {
332                // BooleanSometimesAll: no simple pass/fail violation contract
333                // (the frontier tracking is the guidance mechanism)
334            }
335        }
336    }
337
338    (always_violations, coverage_violations)
339}
340
341// =============================================================================
342// Assertion Macros
343// =============================================================================
344
345/// Assert that a condition is always true.
346///
347/// Tracks pass/fail in shared memory for cross-process visibility.
348/// Does **not** panic — records the violation via `record_always_violation()`
349/// and logs at ERROR level with the seed, following the Antithesis principle
350/// that assertions never crash the program.
351#[macro_export]
352macro_rules! assert_always {
353    ($condition:expr, $message:expr) => {
354        let __msg = $message;
355        let cond = $condition;
356        $crate::chaos::assertions::on_assertion_bool(
357            &__msg,
358            cond,
359            $crate::chaos::assertions::_re_export::AssertKind::Always,
360            true,
361        );
362        if !cond {
363            $crate::chaos::assertions::record_always_violation();
364        }
365    };
366    ($condition:expr, $message:expr, { $($key:expr => $val:expr),+ $(,)? }) => {
367        let __msg = $message;
368        let cond = $condition;
369        $crate::chaos::assertions::on_assertion_bool(
370            &__msg,
371            cond,
372            $crate::chaos::assertions::_re_export::AssertKind::Always,
373            true,
374        );
375        if !cond {
376            $crate::chaos::assertions::record_always_violation();
377            eprintln!(
378                "[ASSERTION FAILED] {} (seed={}) | {}",
379                __msg,
380                $crate::get_current_sim_seed(),
381                $crate::chaos::assertions::format_details(
382                    &[ $(($key, &$val as &dyn std::fmt::Display)),+ ]
383                )
384            );
385        }
386    };
387}
388
389/// Assert that a condition is always true when reached, but the code path
390/// need not be reached. Does not panic if never evaluated.
391///
392/// Does **not** panic on failure — records the violation and logs at ERROR level.
393#[macro_export]
394macro_rules! assert_always_or_unreachable {
395    ($condition:expr, $message:expr) => {
396        let __msg = $message;
397        let cond = $condition;
398        $crate::chaos::assertions::on_assertion_bool(
399            &__msg,
400            cond,
401            $crate::chaos::assertions::_re_export::AssertKind::AlwaysOrUnreachable,
402            false,
403        );
404        if !cond {
405            $crate::chaos::assertions::record_always_violation();
406        }
407    };
408    ($condition:expr, $message:expr, { $($key:expr => $val:expr),+ $(,)? }) => {
409        let __msg = $message;
410        let cond = $condition;
411        $crate::chaos::assertions::on_assertion_bool(
412            &__msg,
413            cond,
414            $crate::chaos::assertions::_re_export::AssertKind::AlwaysOrUnreachable,
415            false,
416        );
417        if !cond {
418            $crate::chaos::assertions::record_always_violation();
419            eprintln!(
420                "[ASSERTION FAILED] {} (seed={}) | {}",
421                __msg,
422                $crate::get_current_sim_seed(),
423                $crate::chaos::assertions::format_details(
424                    &[ $(($key, &$val as &dyn std::fmt::Display)),+ ]
425                )
426            );
427        }
428    };
429}
430
431/// Assert a condition that should sometimes be true, tracking stats and triggering exploration.
432///
433/// Does not panic. On first success, triggers a fork to explore alternate timelines.
434#[macro_export]
435macro_rules! assert_sometimes {
436    ($condition:expr, $message:expr) => {
437        $crate::chaos::assertions::on_assertion_bool(
438            &$message,
439            $condition,
440            $crate::chaos::assertions::_re_export::AssertKind::Sometimes,
441            true,
442        );
443    };
444}
445
446/// Assert that a code path is reachable (should be reached at least once).
447///
448/// Does not panic. On first reach, triggers a fork.
449#[macro_export]
450macro_rules! assert_reachable {
451    ($message:expr) => {
452        $crate::chaos::assertions::on_assertion_bool(
453            &$message,
454            true,
455            $crate::chaos::assertions::_re_export::AssertKind::Reachable,
456            true,
457        );
458    };
459}
460
461/// Assert that a code path should never be reached.
462///
463/// Does **not** panic — records the violation and logs at ERROR level.
464/// Tracks in shared memory for reporting.
465#[macro_export]
466macro_rules! assert_unreachable {
467    ($message:expr) => {
468        let __msg = $message;
469        $crate::chaos::assertions::on_assertion_bool(
470            &__msg,
471            true,
472            $crate::chaos::assertions::_re_export::AssertKind::Unreachable,
473            false,
474        );
475        $crate::chaos::assertions::record_always_violation();
476    };
477    ($message:expr, { $($key:expr => $val:expr),+ $(,)? }) => {
478        let __msg = $message;
479        $crate::chaos::assertions::on_assertion_bool(
480            &__msg,
481            true,
482            $crate::chaos::assertions::_re_export::AssertKind::Unreachable,
483            false,
484        );
485        $crate::chaos::assertions::record_always_violation();
486        eprintln!(
487            "[ASSERTION FAILED] {} | {}",
488            __msg,
489            $crate::chaos::assertions::format_details(
490                &[ $(($key, &$val as &dyn std::fmt::Display)),+ ]
491            )
492        );
493    };
494}
495
496/// Assert that `val > threshold` always holds.
497///
498/// Does **not** panic on failure — records the violation and logs at ERROR level.
499#[macro_export]
500macro_rules! assert_always_greater_than {
501    ($val:expr, $thresh:expr, $message:expr) => {
502        let __msg = $message;
503        let __v = $val as i64;
504        let __t = $thresh as i64;
505        $crate::chaos::assertions::on_assertion_numeric(
506            &__msg,
507            __v,
508            $crate::chaos::assertions::_re_export::AssertCmp::Gt,
509            __t,
510            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
511            false,
512        );
513        if !(__v > __t) {
514            $crate::chaos::assertions::record_always_violation();
515        }
516    };
517    ($val:expr, $thresh:expr, $message:expr, { $($key:expr => $dval:expr),+ $(,)? }) => {
518        let __msg = $message;
519        let __v = $val as i64;
520        let __t = $thresh as i64;
521        $crate::chaos::assertions::on_assertion_numeric(
522            &__msg,
523            __v,
524            $crate::chaos::assertions::_re_export::AssertCmp::Gt,
525            __t,
526            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
527            false,
528        );
529        if !(__v > __t) {
530            $crate::chaos::assertions::record_always_violation();
531            eprintln!(
532                "[ASSERTION FAILED] {} ({}>{} failed, seed={}) | {}",
533                __msg, __v, __t,
534                $crate::get_current_sim_seed(),
535                $crate::chaos::assertions::format_details(
536                    &[ $(($key, &$dval as &dyn std::fmt::Display)),+ ]
537                )
538            );
539        }
540    };
541}
542
543/// Assert that `val >= threshold` always holds.
544///
545/// Does **not** panic on failure — records the violation and logs at ERROR level.
546#[macro_export]
547macro_rules! assert_always_greater_than_or_equal_to {
548    ($val:expr, $thresh:expr, $message:expr) => {
549        let __msg = $message;
550        let __v = $val as i64;
551        let __t = $thresh as i64;
552        $crate::chaos::assertions::on_assertion_numeric(
553            &__msg,
554            __v,
555            $crate::chaos::assertions::_re_export::AssertCmp::Ge,
556            __t,
557            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
558            false,
559        );
560        if !(__v >= __t) {
561            $crate::chaos::assertions::record_always_violation();
562        }
563    };
564    ($val:expr, $thresh:expr, $message:expr, { $($key:expr => $dval:expr),+ $(,)? }) => {
565        let __msg = $message;
566        let __v = $val as i64;
567        let __t = $thresh as i64;
568        $crate::chaos::assertions::on_assertion_numeric(
569            &__msg,
570            __v,
571            $crate::chaos::assertions::_re_export::AssertCmp::Ge,
572            __t,
573            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
574            false,
575        );
576        if !(__v >= __t) {
577            $crate::chaos::assertions::record_always_violation();
578            eprintln!(
579                "[ASSERTION FAILED] {} ({}>={} failed, seed={}) | {}",
580                __msg, __v, __t,
581                $crate::get_current_sim_seed(),
582                $crate::chaos::assertions::format_details(
583                    &[ $(($key, &$dval as &dyn std::fmt::Display)),+ ]
584                )
585            );
586        }
587    };
588}
589
590/// Assert that `val < threshold` always holds.
591///
592/// Does **not** panic on failure — records the violation and logs at ERROR level.
593#[macro_export]
594macro_rules! assert_always_less_than {
595    ($val:expr, $thresh:expr, $message:expr) => {
596        let __msg = $message;
597        let __v = $val as i64;
598        let __t = $thresh as i64;
599        $crate::chaos::assertions::on_assertion_numeric(
600            &__msg,
601            __v,
602            $crate::chaos::assertions::_re_export::AssertCmp::Lt,
603            __t,
604            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
605            true,
606        );
607        if !(__v < __t) {
608            $crate::chaos::assertions::record_always_violation();
609        }
610    };
611    ($val:expr, $thresh:expr, $message:expr, { $($key:expr => $dval:expr),+ $(,)? }) => {
612        let __msg = $message;
613        let __v = $val as i64;
614        let __t = $thresh as i64;
615        $crate::chaos::assertions::on_assertion_numeric(
616            &__msg,
617            __v,
618            $crate::chaos::assertions::_re_export::AssertCmp::Lt,
619            __t,
620            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
621            true,
622        );
623        if !(__v < __t) {
624            $crate::chaos::assertions::record_always_violation();
625            eprintln!(
626                "[ASSERTION FAILED] {} ({}<{} failed, seed={}) | {}",
627                __msg, __v, __t,
628                $crate::get_current_sim_seed(),
629                $crate::chaos::assertions::format_details(
630                    &[ $(($key, &$dval as &dyn std::fmt::Display)),+ ]
631                )
632            );
633        }
634    };
635}
636
637/// Assert that `val <= threshold` always holds.
638///
639/// Does **not** panic on failure — records the violation and logs at ERROR level.
640#[macro_export]
641macro_rules! assert_always_less_than_or_equal_to {
642    ($val:expr, $thresh:expr, $message:expr) => {
643        let __msg = $message;
644        let __v = $val as i64;
645        let __t = $thresh as i64;
646        $crate::chaos::assertions::on_assertion_numeric(
647            &__msg,
648            __v,
649            $crate::chaos::assertions::_re_export::AssertCmp::Le,
650            __t,
651            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
652            true,
653        );
654        if !(__v <= __t) {
655            $crate::chaos::assertions::record_always_violation();
656        }
657    };
658    ($val:expr, $thresh:expr, $message:expr, { $($key:expr => $dval:expr),+ $(,)? }) => {
659        let __msg = $message;
660        let __v = $val as i64;
661        let __t = $thresh as i64;
662        $crate::chaos::assertions::on_assertion_numeric(
663            &__msg,
664            __v,
665            $crate::chaos::assertions::_re_export::AssertCmp::Le,
666            __t,
667            $crate::chaos::assertions::_re_export::AssertKind::NumericAlways,
668            true,
669        );
670        if !(__v <= __t) {
671            $crate::chaos::assertions::record_always_violation();
672            eprintln!(
673                "[ASSERTION FAILED] {} ({}<={} failed, seed={}) | {}",
674                __msg, __v, __t,
675                $crate::get_current_sim_seed(),
676                $crate::chaos::assertions::format_details(
677                    &[ $(($key, &$dval as &dyn std::fmt::Display)),+ ]
678                )
679            );
680        }
681    };
682}
683
684/// Assert that `val > threshold` sometimes holds. Forks on watermark improvement.
685#[macro_export]
686macro_rules! assert_sometimes_greater_than {
687    ($val:expr, $thresh:expr, $message:expr) => {
688        $crate::chaos::assertions::on_assertion_numeric(
689            &$message,
690            $val as i64,
691            $crate::chaos::assertions::_re_export::AssertCmp::Gt,
692            $thresh as i64,
693            $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
694            true,
695        );
696    };
697}
698
699/// Assert that `val >= threshold` sometimes holds. Forks on watermark improvement.
700#[macro_export]
701macro_rules! assert_sometimes_greater_than_or_equal_to {
702    ($val:expr, $thresh:expr, $message:expr) => {
703        $crate::chaos::assertions::on_assertion_numeric(
704            &$message,
705            $val as i64,
706            $crate::chaos::assertions::_re_export::AssertCmp::Ge,
707            $thresh as i64,
708            $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
709            true,
710        );
711    };
712}
713
714/// Assert that `val < threshold` sometimes holds. Forks on watermark improvement.
715#[macro_export]
716macro_rules! assert_sometimes_less_than {
717    ($val:expr, $thresh:expr, $message:expr) => {
718        $crate::chaos::assertions::on_assertion_numeric(
719            &$message,
720            $val as i64,
721            $crate::chaos::assertions::_re_export::AssertCmp::Lt,
722            $thresh as i64,
723            $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
724            false,
725        );
726    };
727}
728
729/// Assert that `val <= threshold` sometimes holds. Forks on watermark improvement.
730#[macro_export]
731macro_rules! assert_sometimes_less_than_or_equal_to {
732    ($val:expr, $thresh:expr, $message:expr) => {
733        $crate::chaos::assertions::on_assertion_numeric(
734            &$message,
735            $val as i64,
736            $crate::chaos::assertions::_re_export::AssertCmp::Le,
737            $thresh as i64,
738            $crate::chaos::assertions::_re_export::AssertKind::NumericSometimes,
739            false,
740        );
741    };
742}
743
744/// Compound boolean assertion: all named bools should sometimes be true simultaneously.
745///
746/// Tracks a frontier (max number of simultaneously true bools). Forks when
747/// the frontier advances.
748///
749/// # Usage
750///
751/// ```ignore
752/// assert_sometimes_all!("all_nodes_healthy", [
753///     ("node_a", node_a_healthy),
754///     ("node_b", node_b_healthy),
755///     ("node_c", node_c_healthy),
756/// ]);
757/// ```
758#[macro_export]
759macro_rules! assert_sometimes_all {
760    ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ]) => {
761        $crate::chaos::assertions::on_assertion_sometimes_all($msg, &[ $(($name, $val)),+ ])
762    };
763}
764
765/// Per-value bucketed sometimes assertion with optional quality watermarks.
766///
767/// Each unique combination of identity keys gets its own bucket. On first
768/// discovery of a new bucket, a fork is triggered for exploration. If quality
769/// keys are provided, re-forks when quality improves.
770///
771/// # Usage
772///
773/// ```ignore
774/// // Identity keys only
775/// assert_sometimes_each!("gate", [("lock", lock_id), ("depth", depth)]);
776///
777/// // With quality watermarks
778/// assert_sometimes_each!("descended", [("to_floor", floor)], [("health", hp)]);
779/// ```
780#[macro_export]
781macro_rules! assert_sometimes_each {
782    ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ]) => {
783        $crate::chaos::assertions::on_sometimes_each($msg, &[ $(($name, $val as i64)),+ ], &[])
784    };
785    ($msg:expr, [ $(($name:expr, $val:expr)),+ $(,)? ], [ $(($qname:expr, $qval:expr)),+ $(,)? ]) => {
786        $crate::chaos::assertions::on_sometimes_each(
787            $msg,
788            &[ $(($name, $val as i64)),+ ],
789            &[ $(($qname, $qval as i64)),+ ],
790        )
791    };
792}
793
794/// Re-exports for macro hygiene (`$crate::chaos::assertions::_re_export::*`).
795pub mod _re_export {
796    pub use moonpool_explorer::{AssertCmp, AssertKind};
797}
798
799#[cfg(test)]
800mod tests {
801    use super::*;
802
803    #[test]
804    fn test_assertion_stats_new() {
805        let stats = AssertionStats::new();
806        assert_eq!(stats.total_checks, 0);
807        assert_eq!(stats.successes, 0);
808        assert_eq!(stats.success_rate(), 0.0);
809    }
810
811    #[test]
812    fn test_assertion_stats_record() {
813        let mut stats = AssertionStats::new();
814
815        stats.record(true);
816        assert_eq!(stats.total_checks, 1);
817        assert_eq!(stats.successes, 1);
818        assert_eq!(stats.success_rate(), 100.0);
819
820        stats.record(false);
821        assert_eq!(stats.total_checks, 2);
822        assert_eq!(stats.successes, 1);
823        assert_eq!(stats.success_rate(), 50.0);
824
825        stats.record(true);
826        assert_eq!(stats.total_checks, 3);
827        assert_eq!(stats.successes, 2);
828        let expected = 200.0 / 3.0;
829        assert!((stats.success_rate() - expected).abs() < 1e-10);
830    }
831
832    #[test]
833    fn test_assertion_stats_success_rate_edge_cases() {
834        let mut stats = AssertionStats::new();
835        assert_eq!(stats.success_rate(), 0.0);
836
837        stats.record(false);
838        assert_eq!(stats.success_rate(), 0.0);
839
840        stats.record(true);
841        assert_eq!(stats.success_rate(), 50.0);
842    }
843
844    #[test]
845    fn test_get_assertion_results_empty() {
846        // When no assertions have been tracked, results should be empty
847        // (assertion table not initialized = empty)
848        let results = get_assertion_results();
849        // May or may not be empty depending on prior test state,
850        // but should not panic
851        let _ = results;
852    }
853
854    #[test]
855    fn test_validate_contracts_empty() {
856        // Should produce no violations when no assertions tracked
857        let violations = validate_assertion_contracts();
858        // May or may not be empty, but should not panic
859        let _ = violations;
860    }
861}