Skip to main content

confium_coordinator/
chaos_testing.rs

1//! Chaos testing harness — random failure injection.
2
3use rand_core::{OsRng, RngCore};
4use std::collections::HashMap;
5
6/// Failure modes to inject.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum FailureMode {
9    /// Signer drops the connection.
10    DropConnection,
11    /// Signer sends invalid data.
12    InvalidData,
13    /// Signer takes too long.
14    Timeout,
15    /// Signer sends an incorrect partial signature.
16    BadSignature,
17    /// No failure — control case.
18    None,
19}
20
21/// Configuration for chaos testing.
22#[derive(Debug, Clone)]
23pub struct ChaosConfig {
24    /// Probability of injecting a failure per operation.
25    pub failure_rate: f64,
26    /// Allowed failure modes.
27    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
44/// Chaos tester: tracks failures injected and verifies resilience.
45pub 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    /// Decide whether to inject a failure for the given operation.
66    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    /// All injections recorded.
88    pub fn injections(&self) -> &[ChaosInjection] {
89        &self.injections
90    }
91
92    /// Count injections by mode.
93    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    /// Number of total operations.
102    pub fn total_injections(&self) -> usize {
103        self.injections.len()
104    }
105
106    /// Reset injection history.
107    pub fn reset(&mut self) {
108        self.injections.clear();
109    }
110}
111
112/// Simulate a network operation, injecting a failure if chaos decides.
113pub 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}