use std::process::Command;
use std::time::{Duration, Instant};
use zccache_core::NormalizedPath;
fn zccache_bin() -> NormalizedPath {
let mut path = std::env::current_exe()
.expect("current_exe")
.parent()
.expect("parent of test binary")
.parent()
.expect("target dir")
.to_path_buf();
if cfg!(windows) {
path.push("zccache.exe");
} else {
path.push("zccache");
}
assert!(
path.exists(),
"zccache binary not found at {path:?}. Run `cargo build` first."
);
NormalizedPath::new(path)
}
fn stop_daemon_and_wait(bin: &std::path::Path) {
let _ = Command::new(bin)
.arg("stop")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
for _ in 0..30 {
std::thread::sleep(Duration::from_millis(200));
let status = Command::new(bin)
.arg("status")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
match status {
Ok(s) if !s.success() => return, Err(_) => return, _ => {} }
}
}
fn spawn_sleepy_process() -> std::process::Child {
#[cfg(windows)]
{
Command::new("powershell")
.args(["-NoProfile", "-Command", "Start-Sleep -Seconds 30"])
.spawn()
.expect("spawn sleeper")
}
#[cfg(unix)]
{
Command::new("sleep")
.arg("30")
.spawn()
.expect("spawn sleeper")
}
}
#[test]
#[ignore] fn start_completes_with_captured_pipes() {
let bin = zccache_bin();
stop_daemon_and_wait(&bin);
let start = Instant::now();
let output = Command::new(&bin)
.arg("start")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("failed to run zccache start");
let elapsed = start.elapsed();
stop_daemon_and_wait(&bin);
assert!(
output.status.success(),
"zccache start failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
elapsed < Duration::from_secs(10),
"zccache start took {elapsed:?} — likely hanging due to handle inheritance"
);
}
#[test]
#[ignore] fn concurrent_starts_all_complete() {
let bin = zccache_bin();
stop_daemon_and_wait(&bin);
std::thread::sleep(Duration::from_secs(1));
let n = 5;
let handles: Vec<_> = (0..n)
.map(|_| {
let bin = bin.clone();
std::thread::spawn(move || {
let start = Instant::now();
let output = Command::new(&bin)
.arg("start")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("failed to run zccache start");
(start.elapsed(), output)
})
})
.collect();
let mut failures = Vec::new();
for (i, handle) in handles.into_iter().enumerate() {
let (elapsed, output) = handle.join().expect("thread panicked");
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
failures.push(format!(
" thread {i}: exit={}, elapsed={elapsed:?}, stderr={stderr}",
output.status,
));
}
assert!(
elapsed < Duration::from_secs(20),
"thread {i} took {elapsed:?} — hanging due to handle inheritance"
);
}
stop_daemon_and_wait(&bin);
assert!(
failures.is_empty(),
"Some concurrent starts failed:\n{}",
failures.join("\n")
);
}
#[test]
#[ignore] fn stop_kills_locked_process_when_ipc_is_unreachable() {
let bin = zccache_bin();
stop_daemon_and_wait(&bin);
let lock_path = zccache_ipc::lock_file_path();
let mut child = spawn_sleepy_process();
std::fs::write(&lock_path, child.id().to_string()).expect("write daemon lock");
let output = Command::new(&bin)
.arg("stop")
.output()
.expect("failed to run zccache stop");
let status = child.wait().expect("wait for killed process");
let _ = std::fs::remove_file(&lock_path);
assert!(
output.status.success(),
"zccache stop failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!status.success(),
"expected stop to terminate the locked process, got exit status {status}"
);
assert!(
!lock_path.exists(),
"lock file should be removed after forced stop"
);
}