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}