use std::io::Write;
use std::process::{Command, Stdio};
fn target_bin_dir() -> std::path::PathBuf {
let mut p = std::env::current_exe().expect("current_exe");
p.pop(); p.pop(); p
}
fn binary_path(stem: &str) -> std::path::PathBuf {
let mut p = target_bin_dir();
if cfg!(windows) {
p.push(format!("{stem}.exe"));
} else {
p.push(stem);
}
p
}
fn stop_daemon(zccache: &std::path::Path, cache_dir: &std::path::Path) {
let _ = Command::new(zccache)
.arg("stop")
.env("ZCCACHE_CACHE_DIR", cache_dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
fn run_one_wrapper(
zccache: &std::path::Path,
echo_shim: &std::path::Path,
cache_dir: &std::path::Path,
payload: &[u8],
) -> std::process::Output {
let mut cmd = Command::new(zccache);
cmd.arg(echo_shim)
.arg("0")
.env("ZCCACHE_CACHE_DIR", cache_dir)
.env_remove("ZCCACHE_SESSION_ID")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("spawn zccache wrapper");
{
let mut stdin = child.stdin.take().expect("piped stdin");
stdin.write_all(payload).expect("write stdin payload");
}
child.wait_with_output().expect("wait_with_output")
}
fn list_spawn_logs(logs_dir: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(logs_dir) else {
return out;
};
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with("daemon-spawn-") && name.ends_with(".log") {
out.push(entry.path());
}
}
out.sort();
out
}
fn parse_lifecycle_events(logs_dir: &std::path::Path) -> Vec<serde_json::Value> {
let path = logs_dir.join("daemon-lifecycle.log");
let Ok(contents) = std::fs::read_to_string(&path) else {
return Vec::new();
};
contents
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| serde_json::from_str(l).expect("daemon-lifecycle.log line must be valid JSON"))
.collect()
}
#[test]
#[ignore] fn n_serial_wrappers_share_one_daemon() {
let zccache = binary_path("zccache");
let echo_shim = binary_path("echo_shim");
if !zccache.exists() || !echo_shim.exists() {
eprintln!(
"skipping: required binaries not built ({} or {})",
zccache.display(),
echo_shim.display()
);
return;
}
let cache_dir = tempfile::Builder::new()
.prefix("zccache-single-daemon-")
.tempdir()
.expect("tempdir");
stop_daemon(&zccache, cache_dir.path());
let n = 10;
let payload = b"single-daemon-per-session\n";
for i in 0..n {
let output = run_one_wrapper(&zccache, &echo_shim, cache_dir.path(), payload);
assert!(
output.status.success(),
"wrapper invocation {i} failed: stderr={}",
String::from_utf8_lossy(&output.stderr),
);
}
stop_daemon(&zccache, cache_dir.path());
let logs_dir = cache_dir.path().join("logs");
assert!(
logs_dir.exists(),
"logs/ directory must exist after running the wrapper"
);
let spawn_logs = list_spawn_logs(&logs_dir);
assert_eq!(
spawn_logs.len(),
1,
"expected exactly one daemon-spawn-*.log, got {}: {:?}",
spawn_logs.len(),
spawn_logs
.iter()
.map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect::<Vec<_>>()
);
let events = parse_lifecycle_events(&logs_dir);
let spawn_events: Vec<&serde_json::Value> =
events.iter().filter(|e| e["event"] == "spawn").collect();
assert_eq!(
spawn_events.len(),
1,
"expected exactly one \"event\":\"spawn\" line in daemon-lifecycle.log, got {} (events: {:#?})",
spawn_events.len(),
events
);
let attempt_events: Vec<&serde_json::Value> = events
.iter()
.filter(|e| e["event"] == "spawn-attempt")
.collect();
assert_eq!(
attempt_events.len(),
1,
"expected exactly one \"event\":\"spawn-attempt\" line — every wrapper after the first must reuse the live daemon. Got {} (events: {:#?})",
attempt_events.len(),
events
);
assert_eq!(
attempt_events[0]["reason"], "initial-start",
"first (and only) spawn must be reason=initial-start, got {}",
attempt_events[0]
);
let died_shutdown = events.iter().any(|e| e["event"] == "died-shutdown");
assert!(
died_shutdown,
"expected a \"died-shutdown\" event after `zccache stop`, got: {events:#?}"
);
}