confium_coordinator/
chaos_testing.rs1use rand_core::{OsRng, RngCore};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum FailureMode {
9 DropConnection,
11 InvalidData,
13 Timeout,
15 BadSignature,
17 None,
19}
20
21#[derive(Debug, Clone)]
23pub struct ChaosConfig {
24 pub failure_rate: f64,
26 pub allowed_modes: Vec<FailureMode>,
28}
29
30impl Default for ChaosConfig {
31 fn default() -> Self {
32 Self {
33 failure_rate: 0.1,
34 allowed_modes: vec![
35 FailureMode::DropConnection,
36 FailureMode::InvalidData,
37 FailureMode::Timeout,
38 FailureMode::BadSignature,
39 ],
40 }
41 }
42}
43
44pub struct ChaosTester {
46 config: ChaosConfig,
47 injections: Vec<ChaosInjection>,
48}
49
50#[derive(Debug, Clone)]
51pub struct ChaosInjection {
52 pub operation: String,
53 pub mode: FailureMode,
54 pub timestamp: chrono::DateTime<chrono::Utc>,
55}
56
57impl ChaosTester {
58 pub fn new(config: ChaosConfig) -> Self {
59 Self {
60 config,
61 injections: Vec::new(),
62 }
63 }
64
65 pub fn inject(&mut self, operation: &str) -> FailureMode {
67 let mut rng_bytes = [0u8; 1];
68 OsRng.fill_bytes(&mut rng_bytes);
69 let p = rng_bytes[0] as f64 / 256.0;
70
71 if p >= self.config.failure_rate {
72 return FailureMode::None;
73 }
74
75 let mut idx_bytes = [0u8; 2];
76 OsRng.fill_bytes(&mut idx_bytes);
77 let idx = (u16::from_le_bytes(idx_bytes) as usize) % self.config.allowed_modes.len();
78 let mode = self.config.allowed_modes[idx];
79 self.injections.push(ChaosInjection {
80 operation: operation.into(),
81 mode,
82 timestamp: chrono::Utc::now(),
83 });
84 mode
85 }
86
87 pub fn injections(&self) -> &[ChaosInjection] {
89 &self.injections
90 }
91
92 pub fn count_by_mode(&self) -> HashMap<FailureMode, usize> {
94 let mut counts = HashMap::new();
95 for inj in &self.injections {
96 *counts.entry(inj.mode).or_insert(0) += 1;
97 }
98 counts
99 }
100
101 pub fn total_injections(&self) -> usize {
103 self.injections.len()
104 }
105
106 pub fn reset(&mut self) {
108 self.injections.clear();
109 }
110}
111
112pub fn simulate_operation(
114 tester: &mut ChaosTester,
115 operation: &str,
116 success_result: &str,
117) -> Result<String, String> {
118 let injection = tester.inject(operation);
119 match injection {
120 FailureMode::None => Ok(success_result.to_string()),
121 FailureMode::DropConnection => Err("connection dropped".into()),
122 FailureMode::InvalidData => Err("invalid data received".into()),
123 FailureMode::Timeout => Err("operation timed out".into()),
124 FailureMode::BadSignature => Err("signature verification failed".into()),
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn zero_failure_rate_no_injections() {
134 let config = ChaosConfig {
135 failure_rate: 0.0,
136 allowed_modes: vec![FailureMode::DropConnection],
137 };
138 let mut tester = ChaosTester::new(config);
139 for _ in 0..100 {
140 let mode = tester.inject("test");
141 assert_eq!(mode, FailureMode::None);
142 }
143 assert_eq!(tester.total_injections(), 0);
144 }
145
146 #[test]
147 fn full_failure_rate_injects() {
148 let config = ChaosConfig {
149 failure_rate: 1.0,
150 allowed_modes: vec![FailureMode::Timeout],
151 };
152 let mut tester = ChaosTester::new(config);
153 let mut non_none = 0;
154 for _ in 0..100 {
155 if tester.inject("test") != FailureMode::None {
156 non_none += 1;
157 }
158 }
159 assert!(
160 non_none > 50,
161 "expected most to inject, got {} non-none",
162 non_none
163 );
164 }
165
166 #[test]
167 fn injections_recorded() {
168 let config = ChaosConfig {
169 failure_rate: 1.0,
170 allowed_modes: vec![FailureMode::Timeout],
171 };
172 let mut tester = ChaosTester::new(config);
173 tester.inject("test1");
174 tester.inject("test2");
175 assert_eq!(tester.injections().len(), 2);
176 }
177
178 #[test]
179 fn count_by_mode() {
180 let config = ChaosConfig {
181 failure_rate: 1.0,
182 allowed_modes: vec![FailureMode::DropConnection, FailureMode::Timeout],
183 };
184 let mut tester = ChaosTester::new(config);
185 for _ in 0..10 {
186 tester.inject("op");
187 }
188 let counts = tester.count_by_mode();
189 let total: usize = counts.values().sum();
190 assert_eq!(total, 10);
191 }
192
193 #[test]
194 fn simulate_success_without_chaos() {
195 let config = ChaosConfig {
196 failure_rate: 0.0,
197 allowed_modes: vec![],
198 };
199 let mut tester = ChaosTester::new(config);
200 let result = simulate_operation(&mut tester, "op", "ok");
201 assert!(result.is_ok());
202 }
203
204 #[test]
205 fn simulate_failure_with_chaos() {
206 let config = ChaosConfig {
207 failure_rate: 1.0,
208 allowed_modes: vec![FailureMode::DropConnection],
209 };
210 let mut tester = ChaosTester::new(config);
211 let result = simulate_operation(&mut tester, "op", "ok");
212 assert!(result.is_err());
213 }
214
215 #[test]
216 fn reset_clears_history() {
217 let config = ChaosConfig {
218 failure_rate: 1.0,
219 allowed_modes: vec![FailureMode::Timeout],
220 };
221 let mut tester = ChaosTester::new(config);
222 tester.inject("op1");
223 tester.inject("op2");
224 assert_eq!(tester.total_injections(), 2);
225 tester.reset();
226 assert_eq!(tester.total_injections(), 0);
227 }
228
229 #[test]
230 fn each_mode_unique_failure() {
231 for mode in [
232 FailureMode::DropConnection,
233 FailureMode::InvalidData,
234 FailureMode::Timeout,
235 FailureMode::BadSignature,
236 ] {
237 let config = ChaosConfig {
238 failure_rate: 1.0,
239 allowed_modes: vec![mode],
240 };
241 let mut tester = ChaosTester::new(config);
242 let result = simulate_operation(&mut tester, "op", "ok");
243 assert!(result.is_err(), "expected error for {:?}", mode);
244 }
245 }
246}