use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::Duration;
fn get_zinit_binary() -> PathBuf {
let mut path = std::env::current_exe().unwrap();
path.pop(); path.pop(); path.push("zinit");
if !path.exists() {
path.pop();
path.pop();
path.push("release");
path.push("zinit");
}
path
}
fn process_exists(pid: u32) -> bool {
#[cfg(unix)]
{
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
kill(Pid::from_raw(pid as i32), Signal::SIGCONT).is_ok()
}
#[cfg(not(unix))]
{
false
}
}
struct TestProcess {
child: Child,
pid: u32,
name_hint: String,
}
impl TestProcess {
fn start_sleep(duration_secs: u64) -> Result<Self, String> {
let duration_str = duration_secs.to_string();
let marker = format!(
"ZINIT_TEST_SLEEP_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.subsec_nanos()
);
let script = format!("exec -a {} sleep {}", marker, duration_str);
let child = Command::new("bash")
.arg("-c")
.arg(&script)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start sleep process: {}", e))?;
let pid = child.id();
Ok(Self {
child,
pid,
name_hint: marker,
})
}
fn pid(&self) -> u32 {
self.pid
}
fn name_hint(&self) -> &str {
&self.name_hint
}
fn is_running(&self) -> bool {
process_exists(self.pid)
}
}
impl Drop for TestProcess {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
struct ZinitServer {
child: Child,
socket_path: PathBuf,
}
impl ZinitServer {
fn start() -> Result<Self, String> {
let socket_path = PathBuf::from(format!(
"/tmp/zinit-proc-filter-test-{}.sock",
std::process::id()
));
let config_dir = PathBuf::from(format!(
"/tmp/zinit-proc-filter-test-{}-cfg",
std::process::id()
));
let _ = std::fs::remove_file(&socket_path);
let _ = std::fs::remove_dir_all(&config_dir);
std::fs::create_dir_all(&config_dir)
.map_err(|e| format!("Failed to create config dir: {}", e))?;
let zinit_bin = get_zinit_binary();
if !zinit_bin.exists() {
return Err(format!(
"Zinit binary not found at {:?}. Run 'cargo build' first.",
zinit_bin
));
}
let child = Command::new(&zinit_bin)
.arg("server")
.arg("--socket")
.arg(&socket_path)
.arg("--config-dir")
.arg(&config_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to start zinit server: {}", e))?;
let start = std::time::Instant::now();
while !socket_path.exists() {
if start.elapsed().as_secs() > 5 {
return Err("Timeout waiting for zinit server to start".to_string());
}
thread::sleep(Duration::from_millis(100));
}
thread::sleep(Duration::from_millis(200));
Ok(Self { child, socket_path })
}
fn socket_path(&self) -> &PathBuf {
&self.socket_path
}
}
impl Drop for ZinitServer {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let _ = std::fs::remove_file(&self.socket_path);
let config_dir = PathBuf::from(format!(
"/tmp/zinit-proc-filter-test-{}-cfg",
std::process::id()
));
let _ = std::fs::remove_dir_all(&config_dir);
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_can_start_test_process() {
let process = TestProcess::start_sleep(3).expect("Should start sleep process");
assert!(process.is_running(), "Process should be running");
assert!(
process.name_hint().contains("ZINIT_TEST_SLEEP"),
"Process should have unique test marker"
);
}
#[test]
fn test_find_processes_by_name_with_sleep() {
let _process = match TestProcess::start_sleep(60) {
Ok(p) => p,
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
thread::sleep(Duration::from_millis(100));
let processes = zinit::server::graph::find_processes_by_name("ZINIT_TEST_SLEEP");
assert!(
!processes.is_empty(),
"Should find at least one process matching 'ZINIT_TEST_SLEEP'"
);
}
#[test]
fn test_find_processes_case_insensitive() {
let _process = match TestProcess::start_sleep(60) {
Ok(p) => p,
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
thread::sleep(Duration::from_millis(100));
let lower = zinit::server::graph::find_processes_by_name("zinit_test_sleep");
let upper = zinit::server::graph::find_processes_by_name("ZINIT_TEST_SLEEP");
let mixed = zinit::server::graph::find_processes_by_name("ZiNiT_tEsT_sLeEp");
assert!(!lower.is_empty(), "Should find with lowercase");
assert!(!upper.is_empty(), "Should find with uppercase");
assert!(!mixed.is_empty(), "Should find with mixed case");
assert!(
lower
.iter()
.any(|p| p.name.to_lowercase().contains("zinit_test_sleep")),
"Should find matching process with lowercase"
);
assert!(
upper
.iter()
.any(|p| p.name.to_lowercase().contains("zinit_test_sleep")),
"Should find matching process with uppercase"
);
assert!(
mixed
.iter()
.any(|p| p.name.to_lowercase().contains("zinit_test_sleep")),
"Should find matching process with mixed case"
);
}
#[test]
fn test_find_processes_empty_filter() {
let processes = zinit::server::graph::find_processes_by_name("");
assert!(processes.is_empty(), "Empty filter should match nothing");
}
#[test]
fn test_kill_process_tree_sleep() {
let process = match TestProcess::start_sleep(60) {
Ok(p) => p,
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
let pid = process.pid();
assert!(process_exists(pid), "Process should be running before drop");
drop(process);
thread::sleep(Duration::from_millis(100));
let _new_process = match TestProcess::start_sleep(3) {
Ok(p) => {
assert!(p.is_running(), "Should be able to start a new process");
p
}
Err(e) => {
eprintln!("Could not start new process: {}", e);
return;
}
};
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
use zinit::ZinitHandle;
use zinit::client::client::ServiceConfigBuilder;
fn connect_to_server(socket_path: &PathBuf) -> Result<ZinitHandle, String> {
use zinit::client::client::ZinitClient;
let client = ZinitClient::unix(socket_path);
ZinitHandle::with_client(client).map_err(|e| format!("Failed to connect to zinit: {}", e))
}
#[test]
#[ignore = "Requires building zinit binary first (cargo build)"]
fn test_process_filter_detects_conflict() {
println!("\n=== Integration Test: process_filter detects conflict ===\n");
let server = match ZinitServer::start() {
Ok(s) => {
println!("Started zinit server on {:?}", s.socket_path());
s
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
let handle = match connect_to_server(server.socket_path()) {
Ok(h) => {
println!("Connected to zinit server");
h
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
match handle.ping() {
Ok(resp) => println!("Ping successful: version {}", resp.version),
Err(e) => {
eprintln!("Skipping test: ping failed: {}", e);
return;
}
}
let test_process = match TestProcess::start_sleep(60) {
Ok(p) => {
println!("Started test process: {} (PID: {})", p.name_hint(), p.pid());
p
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
assert!(test_process.is_running(), "Test process should be running");
let service_name = format!("process-filter-test-{}", std::process::id());
let config = ServiceConfigBuilder::new(&service_name)
.exec("echo 'test service'")
.process_filter("sleep") .build();
println!(
"Creating service '{}' with process_filter='sleep' (no kill_others)",
service_name
);
match handle.service_set(config) {
Ok(_) => println!("Service created successfully"),
Err(e) => {
eprintln!("Failed to create service: {}", e);
return;
}
}
println!("Starting service...");
match handle.start(&service_name) {
Ok(_) => println!("Start returned OK"),
Err(e) => println!("Start returned error: {}", e),
}
thread::sleep(Duration::from_millis(500));
println!("Checking service status...");
match handle.status(&service_name) {
Ok(status) => {
println!("Service state: {:?}", status.state);
if let Some(error) = &status.error {
println!("Service error: {}", error);
}
}
Err(e) => println!("Could not get status: {}", e),
}
println!("Checking why service is blocked...");
match handle.why(&service_name) {
Ok(why) => {
println!("Blocked: {}", why.blocked);
if let Some(_conflict) = &why.process_conflict {
println!("Process conflict detected (field populated)");
}
}
Err(e) => println!("Could not get why: {}", e),
}
println!("Cleaning up...");
let _ = handle.service_delete(&service_name);
println!("Service deleted\n");
}
#[test]
#[ignore = "Requires building zinit binary first (cargo build)"]
fn test_kill_others_with_process_filter() {
println!("\n=== Integration Test: kill_others with process_filter ===\n");
let server = match ZinitServer::start() {
Ok(s) => {
println!("Started zinit server on {:?}", s.socket_path());
s
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
let handle = match connect_to_server(server.socket_path()) {
Ok(h) => {
println!("Connected to zinit server");
h
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
if let Err(e) = handle.ping() {
eprintln!("Skipping test: ping failed: {}", e);
return;
}
let test_process = match TestProcess::start_sleep(60) {
Ok(p) => {
println!("Started test process: {} (PID: {})", p.name_hint(), p.pid());
p
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
let original_pid = test_process.pid();
assert!(
process_exists(original_pid),
"Test process should be running"
);
println!("Verified test process is running (PID: {})", original_pid);
let service_name = format!("kill-proc-filter-test-{}", std::process::id());
let config = ServiceConfigBuilder::new(&service_name)
.exec("echo 'Service started after killing sleep process'")
.process_filter("sleep") .kill_others()
.build();
println!(
"Creating service '{}' with process_filter='sleep' AND kill_others=true",
service_name
);
match handle.service_set(config) {
Ok(_) => println!("Service created successfully"),
Err(e) => {
eprintln!("Failed to create service: {}", e);
return;
}
}
println!("Starting service (should kill matching processes)...");
match handle.start(&service_name) {
Ok(_) => println!("Service start succeeded"),
Err(e) => println!(
"Service start returned: {} (may be expected for oneshot)",
e
),
}
thread::sleep(Duration::from_millis(1000));
let process_dead = !process_exists(original_pid);
println!(
"Original process (PID {}) dead: {}",
original_pid, process_dead
);
println!("Checking final service status...");
match handle.status(&service_name) {
Ok(status) => {
println!("Service state: {:?}", status.state);
if let Some(error) = &status.error {
println!("Service error: {}", error);
}
}
Err(e) => println!("Could not get status: {}", e),
}
println!("Cleaning up...");
let _ = handle.stop(&service_name);
let _ = handle.service_delete(&service_name);
println!("Service deleted");
assert!(
process_dead,
"Process should have been killed by kill_others"
);
println!("\nTest completed successfully!\n");
}
#[test]
#[ignore = "Requires building zinit binary first (cargo build)"]
fn test_multiple_process_matches() {
println!("\n=== Integration Test: multiple matching processes ===\n");
let server = match ZinitServer::start() {
Ok(s) => {
println!("Started zinit server on {:?}", s.socket_path());
s
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
let handle = match connect_to_server(server.socket_path()) {
Ok(h) => h,
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
if let Err(e) = handle.ping() {
eprintln!("Skipping test: ping failed: {}", e);
return;
}
println!("Starting multiple test processes...");
let process1 = match TestProcess::start_sleep(60) {
Ok(p) => {
println!(" Started process 1 (PID: {})", p.pid());
p
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
let process2 = match TestProcess::start_sleep(60) {
Ok(p) => {
println!(" Started process 2 (PID: {})", p.pid());
p
}
Err(e) => {
eprintln!("Skipping test: {}", e);
return;
}
};
let pid1 = process1.pid();
let pid2 = process2.pid();
let service_name = format!("multi-kill-test-{}", std::process::id());
let config = ServiceConfigBuilder::new(&service_name)
.exec("echo 'Service with multiple killed processes'")
.process_filter("sleep")
.kill_others()
.build();
println!("Creating service with kill_others (will kill both sleep processes)...");
match handle.service_set(config) {
Ok(_) => println!("Service created"),
Err(e) => {
eprintln!("Failed to create service: {}", e);
return;
}
}
println!("Starting service...");
let _ = handle.start(&service_name);
thread::sleep(Duration::from_millis(1000));
let pid1_dead = !process_exists(pid1);
let pid2_dead = !process_exists(pid2);
println!("Process 1 (PID {}) dead: {}", pid1, pid1_dead);
println!("Process 2 (PID {}) dead: {}", pid2, pid2_dead);
let _ = handle.stop(&service_name);
let _ = handle.service_delete(&service_name);
assert!(pid1_dead, "Process 1 should be killed");
assert!(pid2_dead, "Process 2 should be killed");
println!("\nTest completed successfully!\n");
}
}