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