use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use std::cell::RefCell;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FaultType {
StorageCorruption,
StorageLatency {
micros: u64,
},
CrashAtOffset {
byte_offset: u64,
},
ClockJump {
delta_micros: i64,
},
}
#[derive(Debug, Clone)]
pub struct FaultConfig {
pub corruption_rate: f64,
pub crash_at_byte_offset: Option<u64>,
}
impl Default for FaultConfig {
fn default() -> Self {
Self {
corruption_rate: 0.0,
crash_at_byte_offset: None,
}
}
}
impl FaultConfig {
pub fn with_corruption_rate(mut self, rate: f64) -> Self {
assert!(
(0.0..=1.0).contains(&rate),
"corruption_rate must be in [0.0, 1.0]"
);
self.corruption_rate = rate;
self
}
pub fn with_crash_at_offset(mut self, offset: u64) -> Self {
self.crash_at_byte_offset = Some(offset);
self
}
}
#[derive(Debug)]
pub struct FaultInjector {
config: FaultConfig,
rng: RefCell<SmallRng>,
}
impl FaultInjector {
pub fn new(config: FaultConfig, seed: u64) -> Self {
Self {
config,
rng: RefCell::new(SmallRng::seed_from_u64(seed)),
}
}
pub fn config(&self) -> &FaultConfig {
&self.config
}
pub fn try_corrupt(&self, data: &mut [u8]) {
if data.is_empty() || self.config.corruption_rate == 0.0 {
return;
}
let mut rng = self.rng.borrow_mut();
if self.config.corruption_rate >= 1.0
|| rng.gen_range(0.0_f64..1.0_f64) < self.config.corruption_rate
{
for byte in data.iter_mut() {
let mask: u8 = rng.gen_range(1_u8..=255_u8);
*byte ^= mask;
}
}
}
pub fn should_crash_at(&self, bytes_written_so_far: u64) -> bool {
match self.config.crash_at_byte_offset {
Some(threshold) => bytes_written_so_far >= threshold,
None => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_rate_never_corrupts() {
let fi = FaultInjector::new(FaultConfig::default(), 0);
let mut data = vec![0xAB_u8; 32];
let original = data.clone();
for _ in 0..100 {
fi.try_corrupt(&mut data);
}
assert_eq!(data, original);
}
#[test]
fn full_rate_always_corrupts() {
let cfg = FaultConfig::default().with_corruption_rate(1.0);
let fi = FaultInjector::new(cfg, 1);
let mut data = vec![0x00_u8; 8];
fi.try_corrupt(&mut data);
assert!(data.iter().any(|&b| b != 0));
}
#[test]
fn crash_threshold_triggers_at_offset() {
let cfg = FaultConfig::default().with_crash_at_offset(100);
let fi = FaultInjector::new(cfg, 0);
assert!(!fi.should_crash_at(99));
assert!(fi.should_crash_at(100));
assert!(fi.should_crash_at(101));
}
#[test]
fn same_seed_same_output() {
let cfg = FaultConfig::default().with_corruption_rate(0.5);
let fi_a = FaultInjector::new(cfg.clone(), 42);
let fi_b = FaultInjector::new(cfg, 42);
let mut da = vec![0xFF_u8; 16];
let mut db = da.clone();
fi_a.try_corrupt(&mut da);
fi_b.try_corrupt(&mut db);
assert_eq!(da, db);
}
}