use std::io::{Read as _, Write as _};
use std::net::TcpListener;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use super::*;
const UNREACHABLE_ADDR: &str = "127.0.0.1:1";
struct EnvGuard {
name: &'static str,
previous: Option<std::ffi::OsString>,
}
impl EnvGuard {
fn set(name: &'static str, value: &str) -> Self {
let previous = std::env::var_os(name);
unsafe {
std::env::set_var(name, value);
}
Self { name, previous }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match self.previous.take() {
Some(value) => std::env::set_var(self.name, value),
None => std::env::remove_var(self.name),
}
}
}
}
fn temp_home(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("moadim-cli-{tag}-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).expect("create temp home");
dir
}
struct FakeServer {
addr: String,
alive: Arc<AtomicBool>,
stop: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl FakeServer {
fn start(status: u16, body: String) -> Self {
Self::start_with_liveness(status, body, true)
}
fn start_after(status: u16, body: String, delay: Duration) -> Self {
let server = Self::start_with_liveness(status, body, false);
let alive = Arc::clone(&server.alive);
std::thread::spawn(move || {
std::thread::sleep(delay);
alive.store(true, Ordering::SeqCst);
});
server
}
fn start_with_liveness(status: u16, body: String, initial_alive: bool) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
let addr = listener.local_addr().expect("local addr").to_string();
listener.set_nonblocking(true).expect("set nonblocking");
let alive = Arc::new(AtomicBool::new(initial_alive));
let stop = Arc::new(AtomicBool::new(false));
let alive_loop = Arc::clone(&alive);
let stop_loop = Arc::clone(&stop);
let handle = std::thread::spawn(move || {
let response = format!(
"HTTP/1.1 {status} OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
while !stop_loop.load(Ordering::SeqCst) {
match listener.accept() {
Ok((mut stream, _)) => {
let mut buf = [0_u8; 1024];
let _ = stream.read(&mut buf);
if alive_loop.load(Ordering::SeqCst) {
let _ = stream.write_all(response.as_bytes());
}
}
Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(2));
}
Err(_) => break,
}
}
});
Self {
addr,
alive,
stop,
handle: Some(handle),
}
}
}
impl Drop for FakeServer {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
fn shape_keys(shape: &str) -> Vec<String> {
shape
.trim_start_matches('{')
.trim_end_matches('}')
.split(',')
.map(|field| {
field
.split(':')
.next()
.unwrap_or_default()
.trim()
.trim_matches('"')
.to_string()
})
.collect()
}
fn readme_json_shape(command: &str) -> String {
let readme = include_str!("../../README.md");
let marker = format!("`moadim {command} --json`");
let line = readme
.lines()
.find(|line| line.contains(&marker))
.unwrap_or_else(|| panic!("README scripting table has no row for {marker}"));
let start = line.find('{').expect("shape literal starts with `{`");
let end = line[start..]
.find('}')
.map(|offset| start + offset)
.expect("shape literal ends with `}`");
line[start..=end].to_string()
}
fn actual_keys(json: &str) -> Vec<String> {
let value: serde_json::Value = serde_json::from_str(json).expect("formatter emits valid JSON");
let mut keys: Vec<String> = value
.as_object()
.expect("formatter emits a JSON object")
.keys()
.cloned()
.collect();
keys.sort();
keys
}
#[test]
fn readme_status_json_shape_matches_actual_keys() {
let mut documented = shape_keys(&readme_json_shape("status"));
documented.sort();
let health = HealthInfo {
uptime_secs: 42,
version: "0.1.0".to_string(),
};
assert_eq!(
documented,
actual_keys(&status_json(true, Some(7), Some(&health))),
"README `moadim status --json` shape has drifted from status_json's actual keys"
);
}
#[test]
fn readme_cleanup_json_shape_matches_actual_keys() {
let mut documented = shape_keys(&readme_json_shape("cleanup"));
documented.sort();
assert_eq!(
documented,
actual_keys(&cleanup_json(3, 12345, true)),
"README `moadim cleanup --json` shape has drifted from cleanup_json's actual keys"
);
}
#[test]
fn readme_stop_json_shape_matches_actual_keys() {
let mut documented = shape_keys(&readme_json_shape("stop"));
documented.sort();
assert_eq!(
documented,
actual_keys(&stop_json(true, Some(7))),
"README `moadim stop --json` shape has drifted from stop_json's actual keys"
);
}
#[test]
fn print_help_and_version_emit_without_panicking() {
print_help();
print_version();
}
#[test]
fn stop_reports_not_running_when_no_server() {
let home = temp_home("stop-down");
let _home = EnvGuard::set("MOADIM_HOME_OVERRIDE", home.to_str().unwrap());
let _addr = EnvGuard::set(BIND_ADDR_ENV, UNREACHABLE_ADDR);
assert_eq!(stop(false, false).unwrap(), EXIT_NOT_RUNNING);
assert_eq!(stop(true, false).unwrap(), EXIT_NOT_RUNNING);
assert_eq!(stop(false, true).unwrap(), EXIT_NOT_RUNNING);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn stop_signals_running_server() {
let server = FakeServer::start(200, String::new());
let home = temp_home("stop-up");
let _home = EnvGuard::set("MOADIM_HOME_OVERRIDE", home.to_str().unwrap());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert_eq!(stop(false, false).unwrap(), 0);
assert_eq!(stop(true, false).unwrap(), 0);
assert_eq!(stop(false, true).unwrap(), 0);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn stop_errors_on_unexpected_status() {
let server = FakeServer::start(500, String::new());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert!(stop(false, false).is_err());
}
#[test]
fn status_reports_down_when_no_server() {
let home = temp_home("status-down");
let _home = EnvGuard::set("MOADIM_HOME_OVERRIDE", home.to_str().unwrap());
let _addr = EnvGuard::set(BIND_ADDR_ENV, UNREACHABLE_ADDR);
assert_eq!(status(false, None).unwrap(), EXIT_NOT_RUNNING);
assert_eq!(status(true, None).unwrap(), EXIT_NOT_RUNNING);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn status_reports_running_with_pid() {
let server = FakeServer::start(200, String::new());
let home = temp_home("status-up");
let _home = EnvGuard::set("MOADIM_HOME_OVERRIDE", home.to_str().unwrap());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
write_pid_file().unwrap();
assert_eq!(status(false, None).unwrap(), 0);
assert_eq!(status(true, None).unwrap(), 0);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn status_wait_times_out_when_server_never_comes_up() {
let home = temp_home("status-wait-timeout");
let _home = EnvGuard::set("MOADIM_HOME_OVERRIDE", home.to_str().unwrap());
let _addr = EnvGuard::set(BIND_ADDR_ENV, UNREACHABLE_ADDR);
assert_eq!(status(false, Some(0)).unwrap(), EXIT_NOT_RUNNING);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn status_wait_succeeds_once_server_comes_up() {
let server = FakeServer::start_after(200, String::new(), Duration::from_millis(100));
let home = temp_home("status-wait-success");
let _home = EnvGuard::set("MOADIM_HOME_OVERRIDE", home.to_str().unwrap());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert_eq!(status(false, None).unwrap(), EXIT_NOT_RUNNING);
assert_eq!(status(false, Some(5)).unwrap(), 0);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn cleanup_reports_removed_counts_when_running() {
let home = temp_home("cleanup-up");
let _home = EnvGuard::set("MOADIM_HOME_OVERRIDE", home.to_str().unwrap());
{
let server = FakeServer::start(200, "{\"removed\":1}".to_string());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert_eq!(cleanup(false).unwrap(), 0);
assert_eq!(cleanup(true).unwrap(), 0);
}
{
let server = FakeServer::start(200, "{\"removed\":2}".to_string());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert_eq!(cleanup(false).unwrap(), 0);
}
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn cleanup_reports_not_running_when_no_server() {
let _addr = EnvGuard::set(BIND_ADDR_ENV, UNREACHABLE_ADDR);
assert_eq!(cleanup(false).unwrap(), EXIT_NOT_RUNNING);
assert_eq!(cleanup(true).unwrap(), EXIT_NOT_RUNNING);
}
#[test]
fn cleanup_errors_on_unexpected_status() {
let server = FakeServer::start(500, String::new());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert!(cleanup(false).is_err());
}
#[test]
fn trigger_triggers_routine_when_server_responds() {
let server = FakeServer::start(200, String::new());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert_eq!(trigger("some-id").unwrap(), 0);
}
#[test]
fn trigger_reports_unknown_routine_on_404() {
let server = FakeServer::start(404, String::new());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert!(trigger("missing").is_err());
}
#[test]
fn trigger_errors_on_unexpected_status() {
let server = FakeServer::start(500, String::new());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert!(trigger("some-id").is_err());
}
#[test]
fn trigger_reports_not_running_when_no_server() {
let _addr = EnvGuard::set(BIND_ADDR_ENV, UNREACHABLE_ADDR);
assert_eq!(trigger("some-id").unwrap(), EXIT_NOT_RUNNING);
}
#[test]
fn logs_prints_the_response_body_when_server_responds() {
let server = FakeServer::start(200, "line one\nline two\n".to_string());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert_eq!(logs("some-id").unwrap(), 0);
}
#[test]
fn logs_succeeds_on_an_empty_body() {
let server = FakeServer::start(200, String::new());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert_eq!(logs("some-id").unwrap(), 0);
}
#[test]
fn logs_reports_unknown_routine_on_404() {
let server = FakeServer::start(404, String::new());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert!(logs("missing").is_err());
}
#[test]
fn logs_errors_on_unexpected_status() {
let server = FakeServer::start(500, String::new());
let _addr = EnvGuard::set(BIND_ADDR_ENV, &server.addr);
assert!(logs("some-id").is_err());
}
#[test]
fn logs_reports_not_running_when_no_server() {
let _addr = EnvGuard::set(BIND_ADDR_ENV, UNREACHABLE_ADDR);
assert_eq!(logs("some-id").unwrap(), EXIT_NOT_RUNNING);
}