mod cli_smoke;
mod harness;
mod parser;
use std::path::PathBuf;
use walkdir::WalkDir;
fn discover_scenarios() -> Vec<PathBuf> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let scenarios_dir = PathBuf::from(&manifest_dir).join("scenarios");
if !scenarios_dir.exists() {
eprintln!(
"Warning: scenarios directory not found at {}",
scenarios_dir.display()
);
return Vec::new();
}
let mut paths: Vec<PathBuf> = WalkDir::new(&scenarios_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.map(|ext| ext == "toml")
.unwrap_or(false)
})
.map(|e| e.path().to_path_buf())
.collect();
paths.sort();
paths
}
fn get_scenario_filter() -> Option<String> {
std::env::var("SCENARIO_FILTER").ok()
}
#[test]
fn run_all_scenarios() {
let scenarios = discover_scenarios();
let filter = get_scenario_filter();
if scenarios.is_empty() {
println!("No scenarios found. Make sure scenarios/ directory exists with .toml files.");
return;
}
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
let mut failures: Vec<(String, String)> = Vec::new();
for path in &scenarios {
std::thread::sleep(std::time::Duration::from_millis(100));
let scenario_name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
if let Some(ref f) = filter {
if !scenario_name.contains(f) {
skipped += 1;
continue;
}
}
print!("Running scenario: {} ... ", scenario_name);
match parser::parse_scenario_file(path) {
Ok(scenario) => match harness::run_scenario(&scenario) {
Ok(()) => {
println!("PASSED");
passed += 1;
}
Err(e) => {
println!("FAILED");
println!(" Error: {}", e);
failed += 1;
failures.push((scenario_name.to_string(), e));
}
},
Err(e) => {
println!("PARSE ERROR");
println!(" Error: {}", e);
failed += 1;
failures.push((scenario_name.to_string(), e));
}
}
}
println!();
println!("=== Test Summary ===");
println!("Passed: {}", passed);
println!("Failed: {}", failed);
println!("Skipped: {}", skipped);
println!("Total: {}", scenarios.len());
if !failures.is_empty() {
println!();
println!("=== Failures ===");
for (name, error) in &failures {
println!();
println!(" {}: {}", name, error);
}
panic!("{} scenario(s) failed", failed);
}
}
#[test]
fn test_echo_hello() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(&manifest_dir).join("scenarios/basic/echo_hello.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_cat_echo() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(&manifest_dir).join("scenarios/basic/cat_echo.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_simple_reattach() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(&manifest_dir).join("scenarios/reattach/simple_reattach.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_multi_cycle() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(&manifest_dir).join("scenarios/reattach/multi_cycle.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_multiline_paste() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(&manifest_dir).join("scenarios/edge_cases/multiline_paste.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_rapid_reattach() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(&manifest_dir).join("scenarios/reattach/rapid_reattach.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_reattach_large_buffer() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path =
PathBuf::from(&manifest_dir).join("scenarios/reattach/reattach_with_large_buffer.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_output_during_attach() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(&manifest_dir).join("scenarios/edge_cases/output_during_attach.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_multiple_raw_disconnects() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path =
PathBuf::from(&manifest_dir).join("scenarios/edge_cases/multiple_raw_disconnects.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn test_exit_during_attach() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(&manifest_dir).join("scenarios/reattach/exit_during_attach.toml");
if !path.exists() {
println!("Scenario file not found, skipping: {}", path.display());
return;
}
let scenario = parser::parse_scenario_file(&path).expect("Failed to parse scenario");
harness::run_scenario(&scenario).expect("Scenario failed");
}
#[test]
fn zombie_client_does_not_block_daemon_cleanup() {
use keep_running::protocol::{encode_message, ClientMessage};
use std::io::Write;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
use tempfile::TempDir;
fn pid_alive(pid: i32) -> bool {
unsafe { libc::kill(pid, 0) == 0 }
}
let temp = TempDir::new().unwrap();
let session_dir = temp.path().join("sessions");
let socket_dir = temp.path().join("sockets");
std::fs::create_dir_all(&session_dir).unwrap();
std::fs::create_dir_all(&socket_dir).unwrap();
let session_name = format!("zombie-{}", std::process::id());
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let binary = PathBuf::from(&manifest_dir)
.join("target")
.join("debug")
.join("keep-running");
let status = Command::new(&binary)
.args(["start", "--name", &session_name, "--", "sleep", "0.3"])
.env("KEEP_RUNNING_SESSION_DIR", &session_dir)
.env("KEEP_RUNNING_SOCKET_DIR", &socket_dir)
.env("KEEP_RUNNING_GRACE_SECS", "1")
.env("KEEP_RUNNING_ZOMBIE_SECS", "1")
.status()
.unwrap();
assert!(status.success(), "start command failed");
let socket_path = socket_dir.join(format!("{}.sock", session_name));
let session_file = session_dir.join(format!("{}.json", session_name));
let deadline = Instant::now() + Duration::from_secs(5);
while !socket_path.exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(20));
}
assert!(socket_path.exists(), "socket never appeared");
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&session_file).unwrap()).unwrap();
let daemon_pid = json["pid"].as_i64().unwrap() as i32;
assert!(pid_alive(daemon_pid), "daemon should be alive after start");
let mut stream = UnixStream::connect(&socket_path).unwrap();
let attach = ClientMessage::Attach { cols: 80, rows: 24 };
stream.write_all(&encode_message(&attach).unwrap()).unwrap();
let deadline = Instant::now() + Duration::from_secs(10);
while pid_alive(daemon_pid) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(100));
}
let final_alive = pid_alive(daemon_pid);
drop(stream);
if final_alive {
unsafe {
libc::kill(daemon_pid, libc::SIGKILL);
}
panic!(
"daemon (pid {}) was still alive after 10s with a silent zombie client; \
zombie-detection didn't kick in",
daemon_pid
);
}
}