#![cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use super::super::*;
use super::CacheDirEnvGuard;
fn write_fake_cc(dir: &Path) -> PathBuf {
let tool = dir.join("cc");
std::fs::write(
&tool,
r#"#!/bin/sh
# Minimal cc shim: accept `-c <src> -o <out>` and write a deterministic
# byte payload derived from the source path so test assertions can verify
# "we got the right output" without depending on a real toolchain.
src=
out=
while [ "$#" -gt 0 ]; do
case "$1" in
-c) shift; src=$1 ;;
-o) shift; out=$1 ;;
esac
shift || true
done
if [ -z "$src" ] || [ -z "$out" ]; then
exit 2
fi
# Deterministic, source-dependent payload — different sources produce
# different bytes, so any "wrong hit" is observable.
printf 'object-for:%s\n' "$src" > "$out"
"#,
)
.unwrap();
let mut perms = std::fs::metadata(&tool).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&tool, perms).unwrap();
tool
}
#[tokio::test]
async fn cold_then_warm_returns_identical_content() {
let tmp = tempfile::tempdir().unwrap();
let _guard = CacheDirEnvGuard::set(&tmp.path().join("zccache-cache"));
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let cc = write_fake_cc(tmp.path());
let src = tmp.path().join("foo.c");
std::fs::write(&src, b"int foo(void) { return 1; }\n").unwrap();
let out = tmp.path().join("foo.o");
let args = vec![
"-c".to_string(),
src.to_string_lossy().into_owned(),
"-o".to_string(),
out.to_string_lossy().into_owned(),
];
let cold = handle_compile_ephemeral(
&server.state,
std::process::id(),
tmp.path(),
&cc,
&args,
tmp.path(),
None,
Vec::new(),
)
.await;
let (cold_exit, cold_cached) = match cold {
Response::CompileResult {
exit_code, cached, ..
} => (exit_code, cached),
other => panic!("expected CompileResult on cold path, got {other:?}"),
};
assert_eq!(cold_exit, 0, "cold compile must succeed");
assert!(!cold_cached, "first compile must report cached=false");
let cold_bytes = std::fs::read(&out).expect("cold output must exist on disk");
assert!(
!cold_bytes.is_empty(),
"cold output must not be empty (got {} bytes)",
cold_bytes.len()
);
std::fs::remove_file(&out).ok();
let warm = handle_compile_ephemeral(
&server.state,
std::process::id(),
tmp.path(),
&cc,
&args,
tmp.path(),
None,
Vec::new(),
)
.await;
let warm_exit = match warm {
Response::CompileResult { exit_code, .. } => exit_code,
other => panic!("expected CompileResult on warm path, got {other:?}"),
};
assert_eq!(warm_exit, 0, "warm compile must succeed");
let warm_bytes = std::fs::read(&out).expect("warm output must exist on disk");
assert_eq!(
cold_bytes, warm_bytes,
"warm-hit content must match cold-miss content byte-for-byte"
);
}
#[tokio::test]
async fn distinct_sources_have_distinct_cached_outputs() {
let tmp = tempfile::tempdir().unwrap();
let _guard = CacheDirEnvGuard::set(&tmp.path().join("zccache-cache"));
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let cc = write_fake_cc(tmp.path());
let src_a = tmp.path().join("a.c");
let src_b = tmp.path().join("b.c");
std::fs::write(&src_a, b"int a(void) { return 1; }\n").unwrap();
std::fs::write(&src_b, b"int b(void) { return 2; }\n").unwrap();
let out_a = tmp.path().join("a.o");
let out_b = tmp.path().join("b.o");
let args_a = vec![
"-c".to_string(),
src_a.to_string_lossy().into_owned(),
"-o".to_string(),
out_a.to_string_lossy().into_owned(),
];
let args_b = vec![
"-c".to_string(),
src_b.to_string_lossy().into_owned(),
"-o".to_string(),
out_b.to_string_lossy().into_owned(),
];
for args in [&args_a, &args_b] {
let resp = handle_compile_ephemeral(
&server.state,
std::process::id(),
tmp.path(),
&cc,
args,
tmp.path(),
None,
Vec::new(),
)
.await;
match resp {
Response::CompileResult { exit_code, .. } => assert_eq!(exit_code, 0),
other => panic!("expected CompileResult, got {other:?}"),
}
}
let bytes_a = std::fs::read(&out_a).expect("a.o must exist");
let bytes_b = std::fs::read(&out_b).expect("b.o must exist");
assert_ne!(
bytes_a, bytes_b,
"distinct sources must produce distinct cached outputs — got identical bytes which would indicate cross-key cache aliasing"
);
std::fs::remove_file(&out_a).ok();
std::fs::remove_file(&out_b).ok();
for (args, _expected) in [(&args_a, &bytes_a), (&args_b, &bytes_b)] {
let resp = handle_compile_ephemeral(
&server.state,
std::process::id(),
tmp.path(),
&cc,
args,
tmp.path(),
None,
Vec::new(),
)
.await;
match resp {
Response::CompileResult { exit_code, .. } => assert_eq!(exit_code, 0),
other => panic!("expected CompileResult, got {other:?}"),
}
}
let warm_a = std::fs::read(&out_a).expect("warm a.o");
let warm_b = std::fs::read(&out_b).expect("warm b.o");
assert_eq!(
warm_a, *bytes_a,
"warm a.o must match its own cold-miss content (no cross-key contamination)"
);
assert_eq!(
warm_b, *bytes_b,
"warm b.o must match its own cold-miss content (no cross-key contamination)"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_lookups_after_cold_miss_return_consistent_content() {
let tmp = tempfile::tempdir().unwrap();
let _guard = CacheDirEnvGuard::set(&tmp.path().join("zccache-cache"));
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let state = std::sync::Arc::clone(&server.state);
let cc = write_fake_cc(tmp.path());
let src = tmp.path().join("shared.c");
std::fs::write(&src, b"int shared(void) { return 7; }\n").unwrap();
let cold_out = tmp.path().join("shared.o");
let cold_args = vec![
"-c".to_string(),
src.to_string_lossy().into_owned(),
"-o".to_string(),
cold_out.to_string_lossy().into_owned(),
];
let cold_resp = handle_compile_ephemeral(
&state,
std::process::id(),
tmp.path(),
&cc,
&cold_args,
tmp.path(),
None,
Vec::new(),
)
.await;
match cold_resp {
Response::CompileResult { exit_code, .. } => assert_eq!(exit_code, 0),
other => panic!("expected CompileResult on cold path, got {other:?}"),
}
let expected = std::fs::read(&cold_out).expect("cold output present");
const N: usize = 16;
let mut handles = Vec::with_capacity(N);
for i in 0..N {
let state = std::sync::Arc::clone(&state);
let cc = cc.clone();
let src = src.clone();
let cwd = tmp.path().to_path_buf();
let out_path = tmp.path().join(format!("shared_{i}.o"));
let task_args = vec![
"-c".to_string(),
src.to_string_lossy().into_owned(),
"-o".to_string(),
out_path.to_string_lossy().into_owned(),
];
handles.push(tokio::spawn(async move {
let resp = handle_compile_ephemeral(
&state,
std::process::id(),
&cwd,
&cc,
&task_args,
&cwd,
None,
Vec::new(),
)
.await;
match resp {
Response::CompileResult { exit_code, .. } => {
assert_eq!(exit_code, 0, "task {i} must succeed");
}
other => panic!("task {i}: expected CompileResult, got {other:?}"),
}
std::fs::read(&out_path).unwrap_or_else(|e| panic!("task {i}: read {out_path:?}: {e}"))
}));
}
for (i, h) in handles.into_iter().enumerate() {
let got = h.await.unwrap_or_else(|e| panic!("task {i} join: {e}"));
assert_eq!(
got, expected,
"task {i}: content must match cold-miss output byte-for-byte (wrong-hit detected)"
);
}
}
#[tokio::test]
async fn source_edit_invalidates_cached_artifact() {
let tmp = tempfile::tempdir().unwrap();
let _guard = CacheDirEnvGuard::set(&tmp.path().join("zccache-cache"));
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let cc = tmp.path().join("cc-echo-content");
std::fs::write(
&cc,
r#"#!/bin/sh
src=
out=
while [ "$#" -gt 0 ]; do
case "$1" in
-c) shift; src=$1 ;;
-o) shift; out=$1 ;;
esac
shift || true
done
if [ -z "$src" ] || [ -z "$out" ]; then exit 2; fi
# Echo the SOURCE CONTENT into the object — distinct source contents
# produce distinct object bytes.
printf 'src-content:' > "$out"
cat "$src" >> "$out"
"#,
)
.unwrap();
let mut perms = std::fs::metadata(&cc).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&cc, perms).unwrap();
let src = tmp.path().join("evolving.c");
let out = tmp.path().join("evolving.o");
let args = vec![
"-c".to_string(),
src.to_string_lossy().into_owned(),
"-o".to_string(),
out.to_string_lossy().into_owned(),
];
std::fs::write(&src, b"int v(void) { return 1; }\n").unwrap();
let v1_resp = handle_compile_ephemeral(
&server.state,
std::process::id(),
tmp.path(),
&cc,
&args,
tmp.path(),
None,
Vec::new(),
)
.await;
match v1_resp {
Response::CompileResult { exit_code, .. } => assert_eq!(exit_code, 0),
other => panic!("expected CompileResult, got {other:?}"),
}
let v1_bytes = std::fs::read(&out).expect("v1 output");
assert!(
v1_bytes.starts_with(b"src-content:int v(void) { return 1; }"),
"v1 bytes must encode v1 source content, got: {:?}",
String::from_utf8_lossy(&v1_bytes[..v1_bytes.len().min(80)])
);
std::fs::write(&src, b"int v(void) { return 2; }\n").unwrap();
let later = filetime::FileTime::from_unix_time(filetime::FileTime::now().unix_seconds() + 5, 0);
filetime::set_file_mtime(&src, later).expect("set mtime forward");
std::fs::remove_file(&out).ok();
let v2_resp = handle_compile_ephemeral(
&server.state,
std::process::id(),
tmp.path(),
&cc,
&args,
tmp.path(),
None,
Vec::new(),
)
.await;
match v2_resp {
Response::CompileResult { exit_code, .. } => assert_eq!(exit_code, 0),
other => panic!("expected CompileResult, got {other:?}"),
}
let v2_bytes = std::fs::read(&out).expect("v2 output");
assert!(
v2_bytes.starts_with(b"src-content:int v(void) { return 2; }"),
"v2 bytes must encode v2 source content (cache invalidation broken), got: {:?}",
String::from_utf8_lossy(&v2_bytes[..v2_bytes.len().min(80)])
);
assert_ne!(
v1_bytes, v2_bytes,
"v1 and v2 bytes must differ — source edit between compiles must invalidate the cache key"
);
}
#[tokio::test]
async fn crash_mid_flight_recovery_never_surfaces_wrong_content() {
let tmp = tempfile::tempdir().unwrap();
let cache_root = tmp.path().join("zccache-cache");
let _guard = CacheDirEnvGuard::set(&cache_root);
let cc = write_fake_cc(tmp.path());
let src = tmp.path().join("crashy.c");
std::fs::write(&src, b"int crashy(void) { return 9; }\n").unwrap();
let out = tmp.path().join("crashy.o");
let args = vec![
"-c".to_string(),
src.to_string_lossy().into_owned(),
"-o".to_string(),
out.to_string_lossy().into_owned(),
];
let expected = {
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let resp = handle_compile_ephemeral(
&server.state,
std::process::id(),
tmp.path(),
&cc,
&args,
tmp.path(),
None,
Vec::new(),
)
.await;
match resp {
Response::CompileResult { exit_code, .. } => assert_eq!(exit_code, 0),
other => panic!("expected CompileResult on cold path, got {other:?}"),
}
std::fs::read(&out).expect("cold output present")
};
std::fs::remove_file(&out).ok();
let server2 = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let resp = handle_compile_ephemeral(
&server2.state,
std::process::id(),
tmp.path(),
&cc,
&args,
tmp.path(),
None,
Vec::new(),
)
.await;
match resp {
Response::CompileResult { exit_code, .. } => assert_eq!(exit_code, 0),
other => panic!("expected CompileResult on post-crash path, got {other:?}"),
}
let got = std::fs::read(&out).expect("post-crash output present");
assert_eq!(
got, expected,
"post-restart content must match the original cold-miss bytes — \
whether the cache was recovered or rebuilt is implementation \
detail; the only invariant is correctness"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn distinct_cold_misses_never_cross_contaminate() {
let tmp = tempfile::tempdir().unwrap();
let _guard = CacheDirEnvGuard::set(&tmp.path().join("zccache-cache"));
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let state = std::sync::Arc::clone(&server.state);
let cc = write_fake_cc(tmp.path());
const N: usize = 10;
let inputs: Vec<(PathBuf, PathBuf)> = (0..N)
.map(|i| {
let src = tmp.path().join(format!("cross_{i}.c"));
let out = tmp.path().join(format!("cross_{i}.o"));
std::fs::write(&src, format!("int cross_{i}(void) {{ return {i}; }}\n")).unwrap();
(src, out)
})
.collect();
let expected: Vec<Vec<u8>> = inputs
.iter()
.map(|(src, _)| format!("object-for:{}\n", src.display()).into_bytes())
.collect();
let started = std::time::Instant::now();
let mut handles = Vec::with_capacity(N);
for (i, (src, out)) in inputs.iter().enumerate() {
let state = std::sync::Arc::clone(&state);
let cc = cc.clone();
let cwd = tmp.path().to_path_buf();
let src = src.clone();
let out = out.clone();
let args = vec![
"-c".to_string(),
src.to_string_lossy().into_owned(),
"-o".to_string(),
out.to_string_lossy().into_owned(),
];
handles.push(tokio::spawn(async move {
let resp = handle_compile_ephemeral(
&state,
std::process::id(),
&cwd,
&cc,
&args,
&cwd,
None,
Vec::new(),
)
.await;
match resp {
Response::CompileResult { exit_code, .. } => {
assert_eq!(exit_code, 0, "task {i} cold-miss must succeed");
}
other => panic!("task {i}: expected CompileResult, got {other:?}"),
}
std::fs::read(&out).unwrap_or_else(|e| panic!("task {i}: read {out:?}: {e}"))
}));
}
let mut results: Vec<Vec<u8>> = Vec::with_capacity(N);
for (i, h) in handles.into_iter().enumerate() {
results.push(h.await.unwrap_or_else(|e| panic!("task {i} join: {e}")));
}
let elapsed = started.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(30),
"concurrent cold-misses took {elapsed:?} — possible deadlock between tasks"
);
for (i, got) in results.iter().enumerate() {
assert_eq!(
*got, expected[i],
"task {i}: content must encode this task's source path, not another's — \
cross-key wrong-hit detected (saw bytes derived from a different cache key)"
);
}
}