use zccache::daemon::DaemonServer;
use zccache::protocol::{Request, Response};
async fn start_daemon() -> (
String,
tokio::task::JoinHandle<()>,
std::sync::Arc<tokio::sync::Notify>,
) {
let endpoint = zccache::ipc::unique_test_endpoint();
let mut server = DaemonServer::bind(&endpoint).unwrap();
let shutdown = server.shutdown_handle();
let handle = tokio::spawn(async move {
server.run(0).await.unwrap();
});
(endpoint, handle, shutdown)
}
fn compile_object(gcc: &std::path::Path, dir: &std::path::Path, name: &str, body: &str) {
let src = dir.join(format!("{name}.c"));
let obj = dir.join(format!("{name}.o"));
std::fs::write(&src, body).unwrap();
let status = std::process::Command::new(gcc)
.args(["-c", &src.to_string_lossy(), "-o", &obj.to_string_lossy()])
.current_dir(dir)
.status()
.unwrap();
assert!(status.success(), "gcc -c should succeed for {name}.c");
}
#[tokio::test]
#[ignore] async fn test_dll_cache_miss_then_hit() {
let gcc_path = match zccache::test_support::find_on_path("gcc") {
Some(p) => p,
None => {
eprintln!("skipping test: gcc not found on PATH");
return;
}
};
let tmp = tempfile::tempdir().unwrap();
compile_object(
&gcc_path,
tmp.path(),
"add",
"__declspec(dllexport) int add(int a, int b) { return a + b; }\n",
);
compile_object(
&gcc_path,
tmp.path(),
"mul",
"__declspec(dllexport) int mul(int a, int b) { return a * b; }\n",
);
let output_dll = tmp.path().join("libmath.dll");
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
client.send(&Request::Clear).await.unwrap();
let _: Option<Response> = client.recv().await.unwrap();
let make_args = |dll: &std::path::Path, dir: &std::path::Path| -> Vec<String> {
vec![
"-shared".to_string(),
"-o".to_string(),
dll.to_string_lossy().into_owned(),
dir.join("add.o").to_string_lossy().into_owned(),
dir.join("mul.o").to_string_lossy().into_owned(),
]
};
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: gcc_path.to_string_lossy().into_owned().into(),
args: make_args(&output_dll, tmp.path()),
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match resp {
Some(Response::LinkResult {
exit_code,
cached,
warning,
..
}) => {
assert_eq!(exit_code, 0, "gcc -shared should succeed");
assert!(!cached, "first link should be a cache miss");
assert!(
warning.is_none(),
"deterministic link — no warning expected"
);
}
other => panic!("expected LinkResult, got: {other:?}"),
}
assert!(output_dll.exists(), "DLL should exist after first link");
let first_contents = std::fs::read(&output_dll).unwrap();
assert!(!first_contents.is_empty(), "DLL should not be empty");
std::fs::remove_file(&output_dll).unwrap();
assert!(!output_dll.exists(), "DLL should be deleted");
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: gcc_path.to_string_lossy().into_owned().into(),
args: make_args(&output_dll, tmp.path()),
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match resp {
Some(Response::LinkResult {
exit_code, cached, ..
}) => {
assert_eq!(exit_code, 0, "cached link should succeed");
assert!(cached, "second link should be a cache hit");
}
other => panic!("expected LinkResult, got: {other:?}"),
}
assert!(output_dll.exists(), "cache hit should restore the DLL");
let second_contents = std::fs::read(&output_dll).unwrap();
assert_eq!(
first_contents, second_contents,
"cached DLL should be byte-identical"
);
shutdown.notify_one();
server_handle.await.unwrap();
}
#[tokio::test]
#[ignore] async fn test_dll_cache_invalidated_on_input_change() {
let gcc_path = match zccache::test_support::find_on_path("gcc") {
Some(p) => p,
None => {
eprintln!("skipping test: gcc not found on PATH");
return;
}
};
let tmp = tempfile::tempdir().unwrap();
compile_object(
&gcc_path,
tmp.path(),
"func",
"__declspec(dllexport) int func(void) { return 42; }\n",
);
let output_dll = tmp.path().join("libfunc.dll");
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
client.send(&Request::Clear).await.unwrap();
let _: Option<Response> = client.recv().await.unwrap();
let make_args = |dll: &std::path::Path, dir: &std::path::Path| -> Vec<String> {
vec![
"-shared".to_string(),
"-o".to_string(),
dll.to_string_lossy().into_owned(),
dir.join("func.o").to_string_lossy().into_owned(),
]
};
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: gcc_path.to_string_lossy().into_owned().into(),
args: make_args(&output_dll, tmp.path()),
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match &resp {
Some(Response::LinkResult {
exit_code, cached, ..
}) => {
assert_eq!(*exit_code, 0);
assert!(!cached, "first link should miss");
}
other => panic!("expected LinkResult, got: {other:?}"),
}
let original_dll = std::fs::read(&output_dll).unwrap();
compile_object(
&gcc_path,
tmp.path(),
"func",
"__declspec(dllexport) int func(void) { return 99; }\n",
);
std::fs::remove_file(&output_dll).unwrap();
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: gcc_path.to_string_lossy().into_owned().into(),
args: make_args(&output_dll, tmp.path()),
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match resp {
Some(Response::LinkResult {
exit_code, cached, ..
}) => {
assert_eq!(exit_code, 0);
assert!(!cached, "link after input change should be a cache miss");
}
other => panic!("expected LinkResult, got: {other:?}"),
}
let new_dll = std::fs::read(&output_dll).unwrap();
assert_ne!(
original_dll, new_dll,
"DLL should differ after input change"
);
shutdown.notify_one();
server_handle.await.unwrap();
}
#[tokio::test]
#[ignore] async fn test_dll_non_deterministic_warning() {
let gcc_path = match zccache::test_support::find_on_path("gcc") {
Some(p) => p,
None => {
eprintln!("skipping test: gcc not found on PATH");
return;
}
};
let tmp = tempfile::tempdir().unwrap();
compile_object(
&gcc_path,
tmp.path(),
"warn",
"__declspec(dllexport) int warn_fn(void) { return 1; }\n",
);
let output_dll = tmp.path().join("libwarn.dll");
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
client.send(&Request::Clear).await.unwrap();
let _: Option<Response> = client.recv().await.unwrap();
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: gcc_path.to_string_lossy().into_owned().into(),
args: vec![
"-shared".to_string(),
"-Wl,--build-id=uuid".to_string(),
"-o".to_string(),
output_dll.to_string_lossy().into_owned(),
tmp.path().join("warn.o").to_string_lossy().into_owned(),
],
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match resp {
Some(Response::LinkResult {
exit_code,
cached,
warning,
..
}) => {
if exit_code == 0 {
assert!(!cached, "first invocation should be a cache miss");
assert!(
warning.is_some(),
"should warn about non-deterministic invocation"
);
let w = warning.unwrap();
assert!(
w.contains("non-deterministic"),
"warning should mention non-determinism: {w}"
);
}
}
other => panic!("expected LinkResult, got: {other:?}"),
}
shutdown.notify_one();
server_handle.await.unwrap();
}
#[tokio::test]
#[ignore] async fn test_exe_cache_miss_then_hit() {
let gcc_path = match zccache::test_support::find_on_path("gcc") {
Some(p) => p,
None => {
eprintln!("skipping test: gcc not found on PATH");
return;
}
};
let tmp = tempfile::tempdir().unwrap();
compile_object(
&gcc_path,
tmp.path(),
"main",
"int main(void) { return 0; }\n",
);
let output_exe = tmp.path().join("main.exe");
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
client.send(&Request::Clear).await.unwrap();
let _: Option<Response> = client.recv().await.unwrap();
let make_args = |exe: &std::path::Path, dir: &std::path::Path| -> Vec<String> {
vec![
"-o".to_string(),
exe.to_string_lossy().into_owned(),
dir.join("main.o").to_string_lossy().into_owned(),
]
};
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: gcc_path.to_string_lossy().into_owned().into(),
args: make_args(&output_exe, tmp.path()),
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match resp {
Some(Response::LinkResult {
exit_code, cached, ..
}) => {
assert_eq!(exit_code, 0, "gcc should succeed for exe linking");
assert!(!cached, "first exe link should be a cache miss");
}
other => panic!("expected LinkResult, got: {other:?}"),
}
assert!(
output_exe.exists(),
"executable should exist after first link"
);
let first_contents = std::fs::read(&output_exe).unwrap();
assert!(!first_contents.is_empty(), "executable should not be empty");
std::fs::remove_file(&output_exe).unwrap();
assert!(!output_exe.exists(), "executable should be deleted");
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: gcc_path.to_string_lossy().into_owned().into(),
args: make_args(&output_exe, tmp.path()),
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match resp {
Some(Response::LinkResult {
exit_code, cached, ..
}) => {
assert_eq!(exit_code, 0, "cached exe link should succeed");
assert!(cached, "second exe link should be a cache hit");
}
other => panic!("expected LinkResult, got: {other:?}"),
}
assert!(
output_exe.exists(),
"cache hit should restore the executable"
);
let second_contents = std::fs::read(&output_exe).unwrap();
assert_eq!(
first_contents, second_contents,
"cached executable should be byte-identical"
);
shutdown.notify_one();
server_handle.await.unwrap();
}
fn compile_object_with(compiler: &std::path::Path, dir: &std::path::Path, name: &str, body: &str) {
let src = dir.join(format!("{name}.c"));
let obj = dir.join(format!("{name}.o"));
std::fs::write(&src, body).unwrap();
let status = std::process::Command::new(compiler)
.args(["-c", &src.to_string_lossy(), "-o", &obj.to_string_lossy()])
.current_dir(dir)
.status()
.unwrap();
assert!(
status.success(),
"compile should succeed for {name}.c with {}",
compiler.display()
);
}
#[tokio::test]
#[ignore] async fn test_clang_link_cache_miss_then_hit() {
let clang = match zccache::test_support::find_clang() {
Some(p) => p,
None => return,
};
let tmp = tempfile::tempdir().unwrap();
compile_object_with(&clang, tmp.path(), "main", "int main(void) { return 0; }\n");
let output_exe = tmp
.path()
.join(if cfg!(windows) { "main.exe" } else { "main" });
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
client.send(&Request::Clear).await.unwrap();
let _: Option<Response> = client.recv().await.unwrap();
let make_args = |exe: &std::path::Path, dir: &std::path::Path| -> Vec<String> {
vec![
"-o".to_string(),
exe.to_string_lossy().into_owned(),
dir.join("main.o").to_string_lossy().into_owned(),
]
};
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: clang.to_string_lossy().into_owned().into(),
args: make_args(&output_exe, tmp.path()),
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match resp {
Some(Response::LinkResult {
exit_code, cached, ..
}) => {
assert_eq!(exit_code, 0, "clang should succeed for exe linking");
assert!(!cached, "first link should be a cache miss");
}
other => panic!("expected LinkResult, got: {other:?}"),
}
assert!(
output_exe.exists(),
"executable should exist after first link"
);
let first_contents = std::fs::read(&output_exe).unwrap();
assert!(!first_contents.is_empty(), "executable should not be empty");
std::fs::remove_file(&output_exe).unwrap();
client
.send(&Request::LinkEphemeral {
client_pid: std::process::id(),
tool: clang.to_string_lossy().into_owned().into(),
args: make_args(&output_exe, tmp.path()),
cwd: tmp.path().to_string_lossy().into_owned().into(),
env: None,
})
.await
.unwrap();
let resp = client.recv().await.unwrap();
match resp {
Some(Response::LinkResult {
exit_code, cached, ..
}) => {
assert_eq!(exit_code, 0, "cached link should succeed");
assert!(cached, "second link should be a cache hit");
}
other => panic!("expected LinkResult, got: {other:?}"),
}
assert!(
output_exe.exists(),
"cache hit should restore the executable"
);
let second_contents = std::fs::read(&output_exe).unwrap();
assert_eq!(
first_contents, second_contents,
"cached executable should be byte-identical"
);
shutdown.notify_one();
server_handle.await.unwrap();
}