#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use std::{
collections::VecDeque,
sync::{Arc, LazyLock, Mutex},
time::Duration,
};
use simvar::{Sim, switchy::tcp::TcpStream};
pub mod client;
pub mod host;
pub mod http;
static ACTIONS: LazyLock<Arc<Mutex<VecDeque<Action>>>> =
LazyLock::new(|| Arc::new(Mutex::new(VecDeque::new())));
enum Action {
Bounce(String),
}
pub fn queue_bounce(host: impl Into<String>) {
ACTIONS
.lock()
.unwrap()
.push_back(Action::Bounce(host.into()));
}
pub fn handle_actions(sim: &mut impl Sim) {
let actions = ACTIONS.lock().unwrap().drain(..).collect::<Vec<_>>();
for action in actions {
match action {
Action::Bounce(host) => {
log::debug!("bouncing '{host}'");
sim.bounce(host);
}
}
}
}
#[cfg(test)]
fn clear_actions() {
ACTIONS.lock().unwrap().clear();
}
pub async fn try_connect(addr: &str, max_attempts: usize) -> Result<TcpStream, std::io::Error> {
let mut count = 0;
Ok(loop {
tokio::select! {
resp = TcpStream::connect(addr) => {
match resp {
Ok(x) => break x,
Err(e) => {
count += 1;
log::debug!("failed to bind tcp: {e:?} (attempt {count}/{max_attempts})");
if !matches!(e.kind(), std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::ConnectionReset)
|| count >= max_attempts
{
return Err(e);
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
}
() = tokio::time::sleep(Duration::from_secs(5)) => {
return Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "Timed out after 5000ms"));
}
}
})
}
#[cfg(test)]
mod tests {
use std::future::Future;
use serial_test::serial;
use simvar::{Sim, client::ClientResult, host::HostResult};
use super::*;
struct MockSim {
bounced_hosts: Vec<String>,
}
impl MockSim {
fn new() -> Self {
Self {
bounced_hosts: vec![],
}
}
}
impl Sim for MockSim {
fn bounce(&mut self, host: impl Into<String>) {
self.bounced_hosts.push(host.into());
}
fn host<F: Fn() -> Fut + 'static, Fut: Future<Output = HostResult> + 'static>(
&mut self,
_name: impl Into<String>,
_action: F,
) {
}
fn client(
&mut self,
_name: impl Into<String>,
_action: impl Future<Output = ClientResult> + 'static,
) {
}
}
mod queue_bounce_tests {
use super::*;
#[test_log::test]
#[serial]
fn queues_single_bounce_action() {
clear_actions();
queue_bounce("test_host");
let actions = ACTIONS.lock().unwrap();
assert_eq!(actions.len(), 1);
assert!(matches!(&actions[0], Action::Bounce(h) if h == "test_host"));
drop(actions);
}
#[test_log::test]
#[serial]
fn queues_multiple_bounce_actions_in_order() {
clear_actions();
queue_bounce("host1");
queue_bounce("host2");
queue_bounce("host3");
let actions = ACTIONS.lock().unwrap();
assert_eq!(actions.len(), 3);
assert!(matches!(&actions[0], Action::Bounce(h) if h == "host1"));
assert!(matches!(&actions[1], Action::Bounce(h) if h == "host2"));
assert!(matches!(&actions[2], Action::Bounce(h) if h == "host3"));
drop(actions);
}
#[test_log::test]
#[serial]
fn accepts_string_and_str_inputs() {
clear_actions();
queue_bounce("str_host");
queue_bounce(String::from("string_host"));
let actions = ACTIONS.lock().unwrap();
assert_eq!(actions.len(), 2);
assert!(matches!(&actions[0], Action::Bounce(h) if h == "str_host"));
assert!(matches!(&actions[1], Action::Bounce(h) if h == "string_host"));
drop(actions);
}
}
mod handle_actions_tests {
use super::*;
#[test_log::test]
#[serial]
fn handles_empty_action_queue() {
clear_actions();
let mut sim = MockSim::new();
handle_actions(&mut sim);
assert!(sim.bounced_hosts.is_empty());
}
#[test_log::test]
#[serial]
fn drains_and_processes_all_bounce_actions() {
clear_actions();
queue_bounce("host_a");
queue_bounce("host_b");
let mut sim = MockSim::new();
handle_actions(&mut sim);
assert_eq!(sim.bounced_hosts.len(), 2);
assert_eq!(sim.bounced_hosts[0], "host_a");
assert_eq!(sim.bounced_hosts[1], "host_b");
assert!(ACTIONS.lock().unwrap().is_empty());
}
#[test_log::test]
#[serial]
fn clears_queue_after_handling() {
clear_actions();
queue_bounce("host1");
queue_bounce("host2");
let mut sim = MockSim::new();
handle_actions(&mut sim);
assert!(ACTIONS.lock().unwrap().is_empty());
handle_actions(&mut sim);
assert_eq!(sim.bounced_hosts.len(), 2); }
#[test_log::test]
#[serial]
fn processes_actions_in_fifo_order() {
clear_actions();
queue_bounce("first");
queue_bounce("second");
queue_bounce("third");
let mut sim = MockSim::new();
handle_actions(&mut sim);
assert_eq!(sim.bounced_hosts, vec!["first", "second", "third"]);
}
}
}