//! Tests of the `sausage` command line. use std::path::PathBuf; use std::process::{Command, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; fn empty_dir(label: &str) -> PathBuf { let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("target/test-cli") .join(format!("{label}-{}-{nonce}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); dir } /// Bare `sausage` (as in `nix run `) without a world starts `init` /// for ./world instead of printing a usage error. #[test] fn bare_command_without_a_world_starts_init() { let dir = empty_dir("bare"); let output = Command::new(env!("CARGO_BIN_EXE_sausage")) .current_dir(&dir) .env_remove("SAUSAGE_API") .env_remove("SAUSAGE_BASE_URL") .env_remove("SAUSAGE_MODEL") .env_remove("SAUSAGE_API_TOKEN") .stdin(Stdio::null()) .output() .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // With no answers on stdin, init stops at the first required value. assert!(!output.status.success(), "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stdout.contains("No world in ./world yet"), "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stderr.contains("SAUSAGE_BASE_URL"), "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(!stderr.contains("Usage:"), "stderr:\n{stderr}"); // It asks before it creates anything. assert!(!dir.join("world").exists()); } /// Start a headless host on a fresh world with stdin at EOF; returns once it /// printed its ready line (printed after the signal handlers are installed). fn start_headless(label: &str) -> (std::process::Child, std::io::Lines>) { use std::io::{BufRead, BufReader}; let dir = empty_dir(label); let world = dir.join("world"); sausage::World::create(&world).unwrap(); let mut child = Command::new(env!("CARGO_BIN_EXE_sausage")) .args(["run", "--headless"]) .arg(&world) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .unwrap(); let mut lines = BufReader::new(child.stdout.take().unwrap()).lines(); let line = lines.next().expect("host exited before it started").unwrap(); assert!(line.contains("headless"), "unexpected first line: {line}"); (child, lines) } /// With stdin at EOF (as soon as the host starts), a headless host stays up. #[test] fn headless_ignores_stdin_eof() { let (mut child, _lines) = start_headless("headless-eof"); std::thread::sleep(std::time::Duration::from_millis(500)); let alive = child.try_wait().unwrap().is_none(); let _ = child.kill(); let _ = child.wait(); assert!(alive, "headless host exited on stdin EOF"); } /// SIGTERM right after the ready line stops the actors and exits with 0. #[test] fn headless_stops_cleanly_on_sigterm_right_after_ready() { let (mut child, lines) = start_headless("headless-term"); let pid = child.id().to_string(); assert!(Command::new("kill").args(["-TERM", &pid]).status().unwrap().success()); let rest: Vec = lines.map(|l| l.unwrap()).collect(); let status = child.wait().unwrap(); assert!(status.success(), "exit status {status:?}, output {rest:?}"); assert!(rest.iter().any(|l| l.contains("Stopping actors")), "output {rest:?}"); }