use std::time::Duration;
use simvar::{
plan::InteractionPlan,
switchy::{
random::{rand::rand::seq::IteratorRandom as _, rng},
time::simulator::step_multiplier,
},
};
use strum::{EnumDiscriminants, EnumIter, IntoEnumIterator as _};
use crate::host::moosicbox_server::HOST;
pub struct InteractionPlanContext {}
impl Default for InteractionPlanContext {
fn default() -> Self {
Self::new()
}
}
impl InteractionPlanContext {
#[must_use]
pub const fn new() -> Self {
Self {}
}
}
pub struct FaultInjectionInteractionPlan {
#[allow(unused)]
context: InteractionPlanContext,
step: u64,
pub plan: Vec<Interaction>,
}
impl Default for FaultInjectionInteractionPlan {
fn default() -> Self {
Self::new()
}
}
impl FaultInjectionInteractionPlan {
#[must_use]
pub const fn new() -> Self {
Self {
context: InteractionPlanContext::new(),
step: 0,
plan: vec![],
}
}
}
#[derive(Clone, Debug, EnumDiscriminants)]
#[strum_discriminants(derive(EnumIter))]
#[strum_discriminants(name(InteractionType))]
pub enum Interaction {
Sleep(Duration),
Bounce(String),
}
impl InteractionPlan<Interaction> for FaultInjectionInteractionPlan {
fn step(&mut self) -> Option<&Interaction> {
#[allow(clippy::cast_possible_truncation)]
if let Some(item) = self.plan.get(self.step as usize) {
self.step += 1;
log::debug!("step: {}", self.step);
Some(item)
} else {
None
}
}
fn gen_interactions(&mut self, count: u64) {
let len = self.plan.len() as u64;
let mut rng = rng();
for i in 1..=count {
loop {
let interaction_type = InteractionType::iter().choose(&mut rng).unwrap();
log::trace!(
"gen_interactions: generating interaction {i}/{count} ({}) interaction_type={interaction_type:?}",
i + len
);
match interaction_type {
InteractionType::Sleep => {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
self.add_interaction(Interaction::Sleep(Duration::from_millis(
rng.gen_range_dist(0..100_000, 0.1) * step_multiplier(),
)));
break;
}
InteractionType::Bounce => {
if rng.gen_bool(0.99) {
continue;
}
self.add_interaction(Interaction::Bounce(HOST.to_string()));
break;
}
}
}
}
drop(rng);
}
fn add_interaction(&mut self, interaction: Interaction) {
log::trace!("add_interaction: adding interaction interaction={interaction:?}");
match &interaction {
Interaction::Sleep(..) | Interaction::Bounce(..) => {}
}
self.plan.push(interaction);
}
}
#[cfg(test)]
mod tests {
use super::*;
mod fault_injection_interaction_plan {
use super::*;
#[test_log::test]
fn step_returns_none_for_empty_plan() {
let mut plan = FaultInjectionInteractionPlan::new();
assert!(plan.step().is_none());
}
#[test_log::test]
fn step_iterates_through_plan_sequentially() {
let mut plan = FaultInjectionInteractionPlan::new();
plan.add_interaction(Interaction::Sleep(Duration::from_millis(500)));
plan.add_interaction(Interaction::Bounce("test_host".to_string()));
plan.add_interaction(Interaction::Sleep(Duration::from_secs(1)));
let first = plan.step();
assert!(first.is_some());
assert!(
matches!(first.unwrap(), Interaction::Sleep(d) if *d == Duration::from_millis(500))
);
let second = plan.step();
assert!(second.is_some());
assert!(matches!(second.unwrap(), Interaction::Bounce(h) if h == "test_host"));
let third = plan.step();
assert!(third.is_some());
assert!(
matches!(third.unwrap(), Interaction::Sleep(d) if *d == Duration::from_secs(1))
);
assert!(plan.step().is_none());
}
#[test_log::test]
fn step_returns_none_after_all_interactions_exhausted() {
let mut plan = FaultInjectionInteractionPlan::new();
plan.add_interaction(Interaction::Sleep(Duration::from_millis(100)));
assert!(plan.step().is_some());
assert!(plan.step().is_none());
assert!(plan.step().is_none());
}
#[test_log::test]
fn add_interaction_appends_to_plan() {
let mut plan = FaultInjectionInteractionPlan::new();
assert!(plan.plan.is_empty());
plan.add_interaction(Interaction::Sleep(Duration::from_millis(100)));
assert_eq!(plan.plan.len(), 1);
plan.add_interaction(Interaction::Bounce("host1".to_string()));
assert_eq!(plan.plan.len(), 2);
plan.add_interaction(Interaction::Sleep(Duration::from_secs(5)));
assert_eq!(plan.plan.len(), 3);
}
#[test_log::test]
fn add_interaction_preserves_interaction_data() {
let mut plan = FaultInjectionInteractionPlan::new();
let sleep_duration = Duration::from_millis(12345);
plan.add_interaction(Interaction::Sleep(sleep_duration));
let bounce_host = "my_server".to_string();
plan.add_interaction(Interaction::Bounce(bounce_host.clone()));
if let Interaction::Sleep(d) = &plan.plan[0] {
assert_eq!(*d, sleep_duration);
} else {
panic!("Expected Sleep interaction");
}
if let Interaction::Bounce(h) = &plan.plan[1] {
assert_eq!(h, &bounce_host);
} else {
panic!("Expected Bounce interaction");
}
}
#[test_log::test]
fn plan_does_not_clear_when_generating_more_interactions() {
let mut plan = FaultInjectionInteractionPlan::new();
plan.add_interaction(Interaction::Sleep(Duration::from_millis(100)));
plan.add_interaction(Interaction::Bounce("initial".to_string()));
let initial_len = plan.plan.len();
assert_eq!(plan.plan.len(), initial_len);
}
}
}