use zccache::daemon::DaemonServer;
use zccache::protocol::{Request, Response};
#[cfg(unix)]
type ClientConn = zccache::ipc::IpcConnection;
#[cfg(windows)]
type ClientConn = zccache::ipc::IpcClientConnection;
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)
}
async fn start_session(client: &mut ClientConn, cwd: &str, log_file: &str) -> String {
client
.send(&Request::SessionStart {
client_pid: std::process::id(),
working_dir: cwd.to_string().into(),
log_file: Some(log_file.to_string().into()),
track_stats: false,
journal_path: None,
profile: false,
private_daemon: None,
})
.await
.unwrap();
match client.recv().await.unwrap() {
Some(Response::SessionStarted { session_id, .. }) => session_id,
other => panic!("expected SessionStarted, got: {other:?}"),
}
}
async fn compile_raw(
client: &mut ClientConn,
session_id: &str,
compiler: &str,
args: Vec<String>,
cwd: &str,
) -> (i32, bool, Vec<u8>) {
client
.send(&Request::Compile {
session_id: session_id.to_string(),
args,
cwd: cwd.to_string().into(),
compiler: compiler.to_string().into(),
env: None,
stdin: Vec::new(),
})
.await
.unwrap();
match client.recv().await.unwrap() {
Some(Response::CompileResult {
exit_code,
cached,
stderr,
..
}) => (exit_code, cached, (*stderr).clone()),
Some(Response::Error { message }) => panic!("compile error: {message}"),
other => panic!("expected CompileResult, got: {other:?}"),
}
}
#[tokio::test]
#[ignore] async fn pch_sub_header_change_invalidates_cache() {
let clang = match zccache::test_support::find_clang() {
Some(p) => p,
None => {
eprintln!("skipping test: clang not found");
return;
}
};
let tmp = tempfile::tempdir().unwrap();
let cwd = tmp.path().to_string_lossy().into_owned();
let log_dir = tmp.path().join("logs");
std::fs::create_dir_all(&log_dir).unwrap();
let log = log_dir.join("session.log");
let compiler = clang.to_string_lossy().into_owned();
let sub_header = tmp.path().join("sub.h");
let pch_header = tmp.path().join("pch.h");
let pch_file = tmp.path().join("pch.h.pch");
let source = tmp.path().join("main.cpp");
let obj = tmp.path().join("main.o");
std::fs::write(
&sub_header,
"#ifndef SUB_H\n#define SUB_H\n#define SUB_VALUE 42\n#endif\n",
)
.unwrap();
std::fs::write(
&pch_header,
"#ifndef PCH_H\n#define PCH_H\n#include \"sub.h\"\n#endif\n",
)
.unwrap();
std::fs::write(&source, "int main() { return SUB_VALUE; }\n").unwrap();
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
let sid = start_session(&mut client, &cwd, &log.to_string_lossy()).await;
let pch_gen_args = || {
vec![
"-x".to_string(),
"c++-header".to_string(),
"-c".to_string(),
pch_header.to_string_lossy().into_owned(),
"-o".to_string(),
pch_file.to_string_lossy().into_owned(),
]
};
let compile_args = || {
vec![
"-c".to_string(),
source.to_string_lossy().into_owned(),
"-o".to_string(),
obj.to_string_lossy().into_owned(),
"-include-pch".to_string(),
pch_file.to_string_lossy().into_owned(),
]
};
let (exit_code, _, stderr) =
compile_raw(&mut client, &sid, &compiler, pch_gen_args(), &cwd).await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(exit_code, 0, "PCH generation failed: {stderr_str}");
let (exit_code, cached, stderr) =
compile_raw(&mut client, &sid, &compiler, compile_args(), &cwd).await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(exit_code, 0, "compile with PCH failed: {stderr_str}");
assert!(!cached, "first compile should be a cache miss");
let first_obj = std::fs::read(&obj).unwrap();
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, compile_args(), &cwd).await;
assert_eq!(exit_code, 0);
assert!(cached, "second compile should be a cache hit");
std::thread::sleep(std::time::Duration::from_millis(100));
std::fs::write(
&sub_header,
"#ifndef SUB_H\n#define SUB_H\n#define SUB_VALUE 99\n\
typedef struct { int x; int y; } SubExtra;\n\
#endif\n",
)
.unwrap();
let pch_content = std::fs::read_to_string(&pch_header).unwrap();
assert!(
pch_content.contains("#include \"sub.h\""),
"pch.h should be unchanged"
);
std::thread::sleep(std::time::Duration::from_millis(2000));
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, pch_gen_args(), &cwd).await;
assert_eq!(exit_code, 0, "PCH regen should succeed");
assert!(
!cached,
"PCH regen after sub-header change MUST be a cache miss"
);
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, compile_args(), &cwd).await;
assert_eq!(exit_code, 0, "compile with updated PCH should succeed");
assert!(
!cached,
"compile after sub-header change MUST be a cache miss (not a stale hit)"
);
let second_obj = std::fs::read(&obj).unwrap();
assert_ne!(
first_obj, second_obj,
"different SUB_VALUE should produce different .o"
);
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, compile_args(), &cwd).await;
assert_eq!(exit_code, 0);
assert!(
cached,
"re-compile with unchanged PCH should be a cache hit"
);
let pch_before = std::fs::read(&pch_file).unwrap();
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, pch_gen_args(), &cwd).await;
assert_eq!(exit_code, 0);
assert!(
cached,
"PCH regen with no further changes should be a cache hit"
);
let pch_after = std::fs::read(&pch_file).unwrap();
assert_eq!(
pch_before, pch_after,
"cached PCH should be identical to previous"
);
let log_text = std::fs::read_to_string(&log).unwrap();
eprintln!("=== sub-header test log ===\n{log_text}");
shutdown.notify_one();
server_handle.await.unwrap();
}
#[tokio::test]
#[ignore] async fn pch_build_dir_separation_sub_header_change() {
let clang = match zccache::test_support::find_clang() {
Some(p) => p,
None => {
eprintln!("skipping test: clang not found");
return;
}
};
let tmp = tempfile::tempdir().unwrap();
let log_dir = tmp.path().join("logs");
std::fs::create_dir_all(&log_dir).unwrap();
let log = log_dir.join("session.log");
let compiler = clang.to_string_lossy().into_owned();
let src_dir = tmp.path().join("src");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::create_dir_all(&build_dir).unwrap();
let cwd = tmp.path().to_string_lossy().into_owned();
let sub_header = src_dir.join("sub.h");
let pch_header = src_dir.join("pch.h");
let pch_file = build_dir.join("pch.h.pch");
let source = src_dir.join("main.cpp");
let obj = build_dir.join("main.o");
std::fs::write(
&sub_header,
"#ifndef SUB_H\n#define SUB_H\n#define SUB_VALUE 42\n#endif\n",
)
.unwrap();
std::fs::write(
&pch_header,
"#ifndef PCH_H\n#define PCH_H\n#include \"sub.h\"\n#endif\n",
)
.unwrap();
std::fs::write(&source, "int main() { return SUB_VALUE; }\n").unwrap();
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
let sid = start_session(&mut client, &cwd, &log.to_string_lossy()).await;
let pch_gen_args = || {
vec![
"-x".to_string(),
"c++-header".to_string(),
"-c".to_string(),
pch_header.to_string_lossy().into_owned(),
"-o".to_string(),
pch_file.to_string_lossy().into_owned(),
"-I".to_string(),
src_dir.to_string_lossy().into_owned(),
]
};
let compile_args = || {
vec![
"-c".to_string(),
source.to_string_lossy().into_owned(),
"-o".to_string(),
obj.to_string_lossy().into_owned(),
"-include-pch".to_string(),
pch_file.to_string_lossy().into_owned(),
"-I".to_string(),
src_dir.to_string_lossy().into_owned(),
]
};
let (exit_code, _, stderr) =
compile_raw(&mut client, &sid, &compiler, pch_gen_args(), &cwd).await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(exit_code, 0, "PCH gen failed: {stderr_str}");
let (exit_code, cached, stderr) =
compile_raw(&mut client, &sid, &compiler, compile_args(), &cwd).await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(exit_code, 0, "compile failed: {stderr_str}");
assert!(!cached, "first compile should miss");
let first_obj = std::fs::read(&obj).unwrap();
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, compile_args(), &cwd).await;
assert_eq!(exit_code, 0);
assert!(cached, "second compile should hit");
std::thread::sleep(std::time::Duration::from_millis(100));
std::fs::write(
&sub_header,
"#ifndef SUB_H\n#define SUB_H\n#define SUB_VALUE 99\n\
inline int sub_extra() { return 7; }\n\
#endif\n",
)
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(2000));
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, pch_gen_args(), &cwd).await;
assert_eq!(exit_code, 0);
assert!(
!cached,
"PCH regen after sub.h change must miss (build-dir separation)"
);
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, compile_args(), &cwd).await;
assert_eq!(exit_code, 0);
assert!(
!cached,
"compile after sub.h change MUST miss even with build-dir-separated PCH"
);
let second_obj = std::fs::read(&obj).unwrap();
assert_ne!(
first_obj, second_obj,
"different SUB_VALUE must produce different .o"
);
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) =
compile_raw(&mut client, &sid, &compiler, compile_args(), &cwd).await;
assert_eq!(exit_code, 0);
assert!(cached, "re-compile with no changes should hit");
let log_text = std::fs::read_to_string(&log).unwrap();
eprintln!("=== build-dir separation log ===\n{log_text}");
shutdown.notify_one();
server_handle.await.unwrap();
}
#[tokio::test]
#[ignore] async fn pch_chained_sub_header_change() {
let clang = match zccache::test_support::find_clang() {
Some(p) => p,
None => {
eprintln!("skipping test: clang not found");
return;
}
};
let tmp = tempfile::tempdir().unwrap();
let cwd = tmp.path().to_string_lossy().into_owned();
let log_dir = tmp.path().join("logs");
std::fs::create_dir_all(&log_dir).unwrap();
let log = log_dir.join("session.log");
let compiler = clang.to_string_lossy().into_owned();
let sub_h = tmp.path().join("sub.h");
let base_h = tmp.path().join("base.h");
let base_pch = tmp.path().join("base.h.pch");
let test_h = tmp.path().join("test_pch.h");
let test_pch = tmp.path().join("test_pch.h.pch");
let source = tmp.path().join("main.cpp");
let obj = tmp.path().join("main.o");
std::fs::write(
&sub_h,
"#ifndef SUB_H\n#define SUB_H\n#define SUB_VALUE 42\n#endif\n",
)
.unwrap();
std::fs::write(
&base_h,
"#ifndef BASE_H\n#define BASE_H\n#include \"sub.h\"\n#endif\n",
)
.unwrap();
std::fs::write(
&test_h,
"#ifndef TEST_PCH_H\n#define TEST_PCH_H\n#define TEST_EXTRA 1\n#endif\n",
)
.unwrap();
std::fs::write(&source, "int main() { return SUB_VALUE + TEST_EXTRA; }\n").unwrap();
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
let sid = start_session(&mut client, &cwd, &log.to_string_lossy()).await;
let (exit_code, _, stderr) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-x".into(),
"c++-header".into(),
"-c".into(),
base_h.to_string_lossy().into_owned(),
"-o".into(),
base_pch.to_string_lossy().into_owned(),
],
&cwd,
)
.await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(exit_code, 0, "base PCH gen failed: {stderr_str}");
let (exit_code, _, stderr) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-x".into(),
"c++-header".into(),
"-c".into(),
test_h.to_string_lossy().into_owned(),
"-o".into(),
test_pch.to_string_lossy().into_owned(),
"-include-pch".into(),
base_pch.to_string_lossy().into_owned(),
],
&cwd,
)
.await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(exit_code, 0, "test PCH gen failed: {stderr_str}");
let (exit_code, cached, stderr) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-c".into(),
source.to_string_lossy().into_owned(),
"-o".into(),
obj.to_string_lossy().into_owned(),
"-include-pch".into(),
test_pch.to_string_lossy().into_owned(),
],
&cwd,
)
.await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(
exit_code, 0,
"compile with chained PCH failed: {stderr_str}"
);
assert!(!cached, "first compile should miss");
let first_obj = std::fs::read(&obj).unwrap();
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-c".into(),
source.to_string_lossy().into_owned(),
"-o".into(),
obj.to_string_lossy().into_owned(),
"-include-pch".into(),
test_pch.to_string_lossy().into_owned(),
],
&cwd,
)
.await;
assert_eq!(exit_code, 0);
assert!(cached, "second compile should hit");
std::thread::sleep(std::time::Duration::from_millis(100));
std::fs::write(
&sub_h,
"#ifndef SUB_H\n#define SUB_H\n#define SUB_VALUE 99\n#endif\n",
)
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(2000));
let (exit_code, cached, _) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-x".into(),
"c++-header".into(),
"-c".into(),
base_h.to_string_lossy().into_owned(),
"-o".into(),
base_pch.to_string_lossy().into_owned(),
],
&cwd,
)
.await;
assert_eq!(exit_code, 0);
assert!(!cached, "base PCH regen must miss after sub.h change");
let (exit_code, cached, _) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-x".into(),
"c++-header".into(),
"-c".into(),
test_h.to_string_lossy().into_owned(),
"-o".into(),
test_pch.to_string_lossy().into_owned(),
"-include-pch".into(),
base_pch.to_string_lossy().into_owned(),
],
&cwd,
)
.await;
assert_eq!(exit_code, 0);
assert!(
!cached,
"chained test PCH regen must miss after sub.h change"
);
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-c".into(),
source.to_string_lossy().into_owned(),
"-o".into(),
obj.to_string_lossy().into_owned(),
"-include-pch".into(),
test_pch.to_string_lossy().into_owned(),
],
&cwd,
)
.await;
assert_eq!(exit_code, 0);
assert!(
!cached,
"compile with changed chained PCH MUST miss (sub.h changed transitively)"
);
let second_obj = std::fs::read(&obj).unwrap();
assert_ne!(
first_obj, second_obj,
"different SUB_VALUE must produce different .o"
);
std::fs::remove_file(&obj).unwrap();
let (exit_code, cached, _) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-c".into(),
source.to_string_lossy().into_owned(),
"-o".into(),
obj.to_string_lossy().into_owned(),
"-include-pch".into(),
test_pch.to_string_lossy().into_owned(),
],
&cwd,
)
.await;
assert_eq!(exit_code, 0);
assert!(cached, "re-compile after stabilization should hit");
let log_text = std::fs::read_to_string(&log).unwrap();
eprintln!("=== chained PCH log ===\n{log_text}");
shutdown.notify_one();
server_handle.await.unwrap();
}
#[tokio::test]
#[ignore] async fn pch_rebuild_no_spurious_output_in_source_tree() {
let clang = match zccache::test_support::find_clang() {
Some(p) => p,
None => {
eprintln!("skipping test: clang not found");
return;
}
};
let tmp = tempfile::tempdir().unwrap();
let log_dir = tmp.path().join("logs");
std::fs::create_dir_all(&log_dir).unwrap();
let log = log_dir.join("session.log");
let compiler = clang.to_string_lossy().into_owned();
let src_dir = tmp.path().join("src");
let build_dir = tmp.path().join("build");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::create_dir_all(&build_dir).unwrap();
let cwd = tmp.path().to_string_lossy().into_owned();
let sub_h = src_dir.join("sub.h");
std::fs::write(&sub_h, "#pragma once\ninline int sub() { return 1; }\n").unwrap();
let pch_h = src_dir.join("pch.h");
std::fs::write(&pch_h, "#pragma once\n#include \"sub.h\"\n").unwrap();
let main_cpp = src_dir.join("main.cpp");
std::fs::write(
&main_cpp,
"#include \"sub.h\"\nint main() { return sub(); }\n",
)
.unwrap();
let pch_output = build_dir.join("pch.h.pch");
let main_obj = build_dir.join("main.o");
let (endpoint, server_handle, shutdown) = start_daemon().await;
let mut client = zccache::ipc::connect(&endpoint).await.unwrap();
let sid = start_session(&mut client, &cwd, &log.to_string_lossy()).await;
let isrc = format!("-I{}", src_dir.to_string_lossy());
let (exit_code, _, stderr) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-x".into(),
"c++-header".into(),
pch_h.to_string_lossy().into_owned(),
"-o".into(),
pch_output.to_string_lossy().into_owned(),
isrc.clone(),
],
&cwd,
)
.await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(exit_code, 0, "PCH gen failed: {stderr_str}");
assert!(pch_output.exists(), "PCH file should be created");
let (exit_code, _, stderr) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-c".into(),
main_cpp.to_string_lossy().into_owned(),
"-o".into(),
main_obj.to_string_lossy().into_owned(),
"-include-pch".into(),
pch_output.to_string_lossy().into_owned(),
isrc.clone(),
],
&cwd,
)
.await;
let stderr_str = String::from_utf8_lossy(&stderr);
assert_eq!(exit_code, 0, "compile with PCH failed: {stderr_str}");
std::thread::sleep(std::time::Duration::from_millis(100));
std::fs::write(&sub_h, "#pragma once\ninline int sub() { return 2; }\n").unwrap();
std::thread::sleep(std::time::Duration::from_millis(2000));
let (exit_code, _, _) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-x".into(),
"c++-header".into(),
pch_h.to_string_lossy().into_owned(),
"-o".into(),
pch_output.to_string_lossy().into_owned(),
isrc.clone(),
],
&cwd,
)
.await;
assert_eq!(exit_code, 0, "PCH rebuild failed");
let (exit_code, _, _) = compile_raw(
&mut client,
&sid,
&compiler,
vec![
"-c".into(),
main_cpp.to_string_lossy().into_owned(),
"-o".into(),
main_obj.to_string_lossy().into_owned(),
"-include-pch".into(),
pch_output.to_string_lossy().into_owned(),
isrc.clone(),
],
&cwd,
)
.await;
assert_eq!(exit_code, 0, "recompile with PCH failed");
for entry in std::fs::read_dir(&src_dir).unwrap() {
let path = entry.unwrap().path();
assert!(
path.extension().and_then(|e| e.to_str()) != Some("pch"),
"BUG: spurious PCH file found in source tree: {}",
path.display()
);
}
let log_text = std::fs::read_to_string(&log).unwrap();
eprintln!("=== spurious PCH test log ===\n{log_text}");
shutdown.notify_one();
server_handle.await.unwrap();
}