mod base64;
mod group;
mod scenario;
mod server;
mod store;
#[cfg(test)]
mod tests;
use std::io;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub use scenario::{Scenario, SCENARIOS};
pub use store::{Outcome, ReqLog};
use store::{lock, Shared};
#[derive(Debug)]
pub struct MockNexus {
shared: Arc<Shared>,
addr: SocketAddr,
stop: Arc<AtomicBool>,
}
impl MockNexus {
pub fn start(scenario: Scenario) -> std::io::Result<Self> {
Self::start_on(scenario, SocketAddr::from(([127, 0, 0, 1], 0)))
}
pub fn start_on(scenario: Scenario, addr: SocketAddr) -> std::io::Result<Self> {
let listener = TcpListener::bind(addr)?;
let bound = listener.local_addr()?;
let shared = Arc::new(Shared::hosted(scenario));
let stop = Arc::new(AtomicBool::new(false));
spawn_acceptor(listener, Arc::clone(&shared), Arc::clone(&stop));
Ok(Self {
shared,
addr: bound,
stop,
})
}
pub fn start_group(members: &[&MockNexus]) -> std::io::Result<Self> {
if members.len() < 2 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"a group repository needs at least two members",
));
}
let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))?;
let bound = listener.local_addr()?;
let shared = Arc::new(Shared::group(members));
let stop = Arc::new(AtomicBool::new(false));
spawn_acceptor(listener, Arc::clone(&shared), Arc::clone(&stop));
Ok(Self {
shared,
addr: bound,
stop,
})
}
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn base_url(&self) -> String {
format!("http://{}/", self.addr)
}
pub fn store_get(&self, path: &str) -> Option<Vec<u8>> {
lock(&self.shared.store).get(path).cloned()
}
pub fn insert(&self, path: &str, bytes: &[u8]) {
lock(&self.shared.store).insert(path.to_owned(), bytes.to_vec());
}
pub fn requests(&self) -> Vec<ReqLog> {
lock(&self.shared.log).clone()
}
pub fn put_count(&self, path: &str) -> usize {
self.requests()
.iter()
.filter(|req| {
req.method == "PUT"
&& req.path == path
&& matches!(req.outcome, Outcome::Status(200..=299))
})
.count()
}
pub fn enable_drift(&self) {
self.shared.drift.store(true, Ordering::Relaxed);
}
pub fn disable_drift(&self) {
self.shared.drift.store(false, Ordering::Relaxed);
}
}
fn spawn_acceptor(listener: TcpListener, shared: Arc<Shared>, stop: Arc<AtomicBool>) {
std::thread::spawn(move || loop {
let Ok((conn, _)) = listener.accept() else {
break;
};
if stop.load(Ordering::Relaxed) {
break;
}
let conn_shared = Arc::clone(&shared);
std::thread::spawn(move || scenario::serve(&conn_shared, conn));
});
}
impl Drop for MockNexus {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
let _ = TcpStream::connect(self.addr);
}
}