Skip to main content

moonpool_sim/chaos/
invariant_trait.rs

1//! Trait-based invariant checking for simulation correctness verification.
2//!
3//! Invariants validate cross-workload properties after every simulation event.
4//! They receive a [`StateHandle`] containing workload-published state and
5//! the current simulation time.
6//!
7//! # Usage
8//!
9//! Implement the trait directly or use the [`invariant_fn`] closure adapter:
10//!
11//! ```ignore
12//! use moonpool_sim::{Invariant, invariant_fn, StateHandle};
13//!
14//! // Trait implementation
15//! struct ConservationLaw;
16//! impl Invariant for ConservationLaw {
17//!     fn name(&self) -> &str { "conservation" }
18//!     fn check(&self, state: &StateHandle, _sim_time_ms: u64) {
19//!         // validate...
20//!     }
21//! }
22//!
23//! // Closure shorthand
24//! let inv = invariant_fn("no_negative_balance", |state, _t| {
25//!     // validate...
26//! });
27//! ```
28
29use super::state_handle::StateHandle;
30
31/// An invariant that validates system-wide properties during simulation.
32///
33/// Invariants are checked after every simulation event. They should panic
34/// with a descriptive message if the invariant is violated.
35pub trait Invariant: 'static {
36    /// Name of this invariant for reporting.
37    fn name(&self) -> &str;
38
39    /// Check the invariant against current state and simulation time.
40    ///
41    /// Should panic with a descriptive message if the invariant is violated.
42    fn check(&self, state: &StateHandle, sim_time_ms: u64);
43}
44
45/// Type alias for invariant check closures.
46type InvariantCheckFn = Box<dyn Fn(&StateHandle, u64)>;
47
48/// Closure-based invariant adapter.
49struct FnInvariant {
50    name: String,
51    check_fn: InvariantCheckFn,
52}
53
54impl Invariant for FnInvariant {
55    fn name(&self) -> &str {
56        &self.name
57    }
58
59    fn check(&self, state: &StateHandle, sim_time_ms: u64) {
60        (self.check_fn)(state, sim_time_ms);
61    }
62}
63
64/// Create an invariant from a closure.
65///
66/// # Example
67///
68/// ```ignore
69/// let inv = invariant_fn("balance_positive", |state, _t| {
70///     let balance: i64 = state.get("balance").unwrap_or(0);
71///     assert!(balance >= 0, "balance went negative: {}", balance);
72/// });
73/// ```
74pub fn invariant_fn(
75    name: impl Into<String>,
76    f: impl Fn(&StateHandle, u64) + 'static,
77) -> Box<dyn Invariant> {
78    Box::new(FnInvariant {
79        name: name.into(),
80        check_fn: Box::new(f),
81    })
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    struct TestInvariant;
89
90    impl Invariant for TestInvariant {
91        fn name(&self) -> &str {
92            "test"
93        }
94
95        fn check(&self, state: &StateHandle, _sim_time_ms: u64) {
96            if let Some(val) = state.get::<i64>("value") {
97                assert!(val >= 0, "value went negative: {}", val);
98            }
99        }
100    }
101
102    #[test]
103    fn test_trait_impl() {
104        let inv = TestInvariant;
105        let state = StateHandle::new();
106        state.publish("value", 42i64);
107        inv.check(&state, 0);
108        assert_eq!(inv.name(), "test");
109    }
110
111    #[test]
112    fn test_invariant_fn() {
113        let inv = invariant_fn("check_positive", |state, _t| {
114            if let Some(val) = state.get::<i64>("val") {
115                assert!(val >= 0, "negative: {}", val);
116            }
117        });
118        let state = StateHandle::new();
119        state.publish("val", 10i64);
120        inv.check(&state, 100);
121        assert_eq!(inv.name(), "check_positive");
122    }
123
124    #[test]
125    #[should_panic(expected = "negative")]
126    fn test_invariant_violation_panics() {
127        let inv = invariant_fn("must_be_positive", |state, _t| {
128            let val: i64 = state.get("val").unwrap_or(0);
129            assert!(val >= 0, "negative: {}", val);
130        });
131        let state = StateHandle::new();
132        state.publish("val", -1i64);
133        inv.check(&state, 0);
134    }
135}