moonpool_sim/chaos/
buggify.rs1use crate::sim::rng::sim_random;
7use rand::distr::uniform::SampleUniform;
8use std::cell::RefCell;
9use std::collections::BTreeMap;
10
11thread_local! {
12 static STATE: RefCell<State> = RefCell::new(State::default());
13}
14
15#[derive(Default)]
16struct State {
17 enabled: bool,
18 active_locations: BTreeMap<String, bool>,
19 activation_prob: f64,
20}
21
22pub fn buggify_init(activation_prob: f64, _firing_prob: f64) {
24 STATE.with(|state| {
25 let mut state = state.borrow_mut();
26 state.enabled = true;
27 state.active_locations.clear();
28 state.activation_prob = activation_prob;
29 });
30}
31
32pub fn buggify_reset() {
34 STATE.with(|state| {
35 let mut state = state.borrow_mut();
36 state.enabled = false;
37 state.active_locations.clear();
38 state.activation_prob = 0.0;
39 });
40}
41
42#[must_use]
44pub fn buggify_internal(prob: f64, location: &'static str) -> bool {
45 STATE.with(|state| {
46 let mut state = state.borrow_mut();
47
48 if !state.enabled || prob <= 0.0 {
49 return false;
50 }
51
52 let location_str = location.to_string();
53 let activation_prob = state.activation_prob;
54
55 let is_active = *state
57 .active_locations
58 .entry(location_str)
59 .or_insert_with(|| sim_random::<f64>() < activation_prob);
60
61 is_active && sim_random::<f64>() < prob
63 })
64}
65
66#[must_use]
75pub fn buggify_knob_internal<T>(default: T, range: std::ops::Range<T>, location: &'static str) -> T
76where
77 T: SampleUniform + PartialOrd + Clone,
78{
79 if buggify_internal(0.25, location) {
80 crate::sim::sim_random_range_or_default(range)
81 } else {
82 default
83 }
84}
85
86#[macro_export]
88macro_rules! buggify {
89 () => {
90 $crate::chaos::buggify::buggify_internal(0.25, concat!(file!(), ":", line!()))
91 };
92}
93
94#[macro_export]
96macro_rules! buggify_with_prob {
97 ($prob:expr) => {
98 $crate::chaos::buggify::buggify_internal($prob as f64, concat!(file!(), ":", line!()))
99 };
100}
101
102#[macro_export]
110macro_rules! buggify_knob {
111 ($default:expr, $range:expr) => {
112 $crate::chaos::buggify::buggify_knob_internal(
113 $default,
114 $range,
115 concat!(file!(), ":", line!()),
116 )
117 };
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::sim::rng::{reset_sim_rng, set_sim_seed};
124
125 #[test]
126 fn test_disabled_by_default() {
127 buggify_reset();
128 for _ in 0..10 {
129 assert!(!buggify_internal(1.0, "test"));
130 }
131 }
132
133 #[test]
134 fn test_activation_consistency() {
135 set_sim_seed(12345);
136 buggify_init(0.5, 1.0);
137
138 let location = "test_location";
139 let first = buggify_internal(1.0, location);
140 let second = buggify_internal(1.0, location);
141
142 assert_eq!(first, second);
144 buggify_reset();
145 }
146
147 #[test]
148 fn test_deterministic() {
149 const SEED: u64 = 54321;
150 let mut results1 = Vec::new();
151 let mut results2 = Vec::new();
152
153 for run in 0..2 {
154 set_sim_seed(SEED);
155 buggify_init(0.5, 0.5);
156
157 let results = if run == 0 {
158 &mut results1
159 } else {
160 &mut results2
161 };
162
163 for i in 0..5 {
164 let location = format!("loc_{i}");
165 results.push(buggify_internal(0.5, Box::leak(location.into_boxed_str())));
166 }
167
168 buggify_reset();
169 reset_sim_rng();
170 }
171
172 assert_eq!(results1, results2);
173 }
174
175 fn knob_sequence(seed: u64) -> Vec<u64> {
177 reset_sim_rng();
178 set_sim_seed(seed);
179 buggify_init(0.8, 0.8);
180 let mut out = Vec::new();
181 for i in 0..20 {
182 let location = Box::leak(format!("knob_{i}").into_boxed_str());
184 out.push(buggify_knob_internal::<u64>(100, 1_000..2_000, location));
185 }
186 buggify_reset();
187 out
188 }
189
190 #[test]
191 fn test_buggify_knob_deterministic() {
192 const SEED: u64 = 98765;
193 assert_eq!(knob_sequence(SEED), knob_sequence(SEED));
194 }
195
196 #[test]
197 fn test_buggify_knob_varies_across_seeds() {
198 let sequences: Vec<Vec<u64>> = [111u64, 222, 333, 444, 555]
199 .iter()
200 .map(|s| knob_sequence(*s))
201 .collect();
202 let unique = sequences
203 .iter()
204 .collect::<std::collections::HashSet<_>>()
205 .len();
206 assert!(
207 unique > 1,
208 "different seeds should yield different knob spikes"
209 );
210 }
211
212 #[test]
213 fn test_buggify_knob_disabled_returns_default() {
214 reset_sim_rng();
215 set_sim_seed(42);
216 buggify_reset(); for _ in 0..10 {
218 assert_eq!(buggify_knob_internal::<u64>(100, 1_000..2_000, "loc"), 100);
219 }
220 }
221
222 #[test]
223 fn test_buggify_knob_spiked_value_in_range() {
224 reset_sim_rng();
225 set_sim_seed(7);
226 buggify_init(1.0, 1.0); let v = buggify_knob_internal::<u64>(100, 1_000..2_000, "always");
228 buggify_reset();
229 assert!(
230 (1_000..2_000).contains(&v),
231 "spiked value must be in range, got {v}"
232 );
233 }
234}