use super::super::*;
#[test]
fn write_cached_output_overwrites_same_size_different_content() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("output.o");
let cache = dir.path().join("cached.o");
let old_content = b"AAAA_symbols_v1_xxxx";
std::fs::write(&out, old_content).unwrap();
let new_content = b"BBBB_symbols_v2_yyyy";
assert_eq!(
old_content.len(),
new_content.len(),
"test requires same size"
);
std::fs::write(&cache, new_content).unwrap();
write_cached_output(&out, &cache, new_content).unwrap();
let result = std::fs::read(&out).unwrap();
assert_eq!(
result, new_content,
"output must contain new content, not stale old content"
);
}
#[test]
fn write_cached_output_creates_new_file() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("output.o");
let cache = dir.path().join("cached.o");
let content = b"fresh object file data";
std::fs::write(&cache, content).unwrap();
write_cached_output(&out, &cache, content).unwrap();
let result = std::fs::read(&out).unwrap();
assert_eq!(result, content.as_slice());
}
#[test]
fn write_cached_output_fallback_to_memory_copy() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("output.o");
let cache = dir.path().join("nonexistent_cache.o");
let content = b"data from memory";
write_cached_output(&out, &cache, content).unwrap();
let result = std::fs::read(&out).unwrap();
assert_eq!(result, content.as_slice());
}
#[test]
fn write_cached_output_skips_when_already_hardlinked() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cached.o");
let out = dir.path().join("output.o");
let content = b"cached artifact content";
std::fs::write(&cache, content).unwrap();
write_cached_output(&out, &cache, content).unwrap();
assert_eq!(std::fs::read(&out).unwrap(), content.as_slice());
assert!(
same_file(&out, &cache),
"output should be a hardlink to cache file after first write"
);
write_cached_output(&out, &cache, content).unwrap();
assert_eq!(std::fs::read(&out).unwrap(), content.as_slice());
}
#[test]
fn persist_artifact_output_does_not_mutate_existing_hardlink() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("artifact-key_0");
let out = dir.path().join("output.rlib");
persist_artifact_output(&cache, b"first").unwrap();
write_cached_output(&out, &cache, b"first").unwrap();
assert!(
same_file(&out, &cache),
"cache hit should initially hardlink output to cache payload"
);
persist_artifact_output(&cache, b"second").unwrap();
assert_eq!(
std::fs::read(&out).unwrap(),
b"first",
"publishing a later cache payload must not mutate existing target outputs"
);
assert_eq!(std::fs::read(&cache).unwrap(), b"second");
assert!(
!same_file(&out, &cache),
"cache path replacement should break the hardlink relationship"
);
}
#[test]
fn persist_artifact_file_reports_hardlink_snapshot_stats() {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("libunit.rlib");
let cache = dir.path().join("artifact-key_0");
let content = b"compiled rust artifact";
std::fs::write(&source, content).unwrap();
let stats = persist_artifact_file(&cache, &source).unwrap();
assert_eq!(std::fs::read(&cache).unwrap(), content);
assert!(
same_file(&source, &cache),
"same-directory snapshots should use a hardlink"
);
assert_eq!(stats.hardlink_count, 1);
assert_eq!(stats.copy_count, 0);
assert_eq!(stats.copy_bytes, 0);
}
#[test]
fn break_output_hardlink_before_compile_prevents_cache_poisoning() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cached.rlib");
let out = dir.path().join("libapp.rlib");
let cached_content = b"cached artifact from worktree a";
let rebuilt_content = b"rebuilt artifact in worktree b";
std::fs::write(&cache, cached_content).unwrap();
write_cached_output(&out, &cache, cached_content).unwrap();
assert!(same_file(&out, &cache), "cache hit should hardlink output");
break_output_hardlink_before_compile(&out).unwrap();
assert!(
!same_file(&out, &cache),
"compile miss must detach output from cache hardlink first"
);
std::fs::write(&out, rebuilt_content).unwrap();
assert_eq!(
std::fs::read(&cache).unwrap(),
cached_content,
"compiler overwrite of output must not mutate shared cache artifact"
);
assert_eq!(std::fs::read(&out).unwrap(), rebuilt_content);
}
#[test]
fn write_cached_output_preserves_cache_mtime_on_hardlink() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cached.rlib");
let out = dir.path().join("output.rlib");
let content = b"cached rlib data";
std::fs::write(&cache, content).unwrap();
let old_time = filetime::FileTime::from_unix_time(1_000_000_000, 0); filetime::set_file_mtime(&cache, old_time).unwrap();
write_cached_output(&out, &cache, content).unwrap();
let out_mtime =
filetime::FileTime::from_last_modification_time(&std::fs::metadata(&out).unwrap());
assert_eq!(
out_mtime.unix_seconds(),
old_time.unix_seconds(),
"cache hit must preserve cache file mtime (cargo's fingerprint depends on it); \
got {out_mtime:?}, expected {old_time:?}"
);
}
#[test]
fn write_cached_output_preserves_mtime_on_existing_hardlink() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cached.rlib");
let out = dir.path().join("output.rlib");
let content = b"cached rlib data";
std::fs::write(&cache, content).unwrap();
write_cached_output(&out, &cache, content).unwrap();
let old_time = filetime::FileTime::from_unix_time(1_000_000_000, 0);
filetime::set_file_mtime(&out, old_time).unwrap();
write_cached_output(&out, &cache, content).unwrap();
let out_mtime =
filetime::FileTime::from_last_modification_time(&std::fs::metadata(&out).unwrap());
assert_eq!(
out_mtime.unix_seconds(),
old_time.unix_seconds(),
"mtime must be preserved across repeated cache hits on the same file"
);
}
#[test]
fn write_cached_output_fallback_has_fresh_mtime() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("output.rlib");
let cache = dir.path().join("nonexistent_cache.rlib");
let content = b"data from memory";
write_cached_output(&out, &cache, content).unwrap();
let out_mtime =
filetime::FileTime::from_last_modification_time(&std::fs::metadata(&out).unwrap());
let now = filetime::FileTime::now();
let diff = now.unix_seconds() - out_mtime.unix_seconds();
assert!(
diff < 5,
"fallback path should produce fresh mtime — {diff}s old"
);
}
#[cfg(windows)]
#[test]
fn replace_artifact_cache_file_retries_through_av_scanner_lock() {
use std::os::windows::fs::OpenOptionsExt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("output.obj");
let tmp = dir.path().join("output.obj.tmp");
std::fs::write(&target, b"OLD").unwrap();
std::fs::write(&tmp, b"NEW").unwrap();
let handle = std::fs::OpenOptions::new()
.read(true)
.share_mode(0x1)
.open(&target)
.expect("open lock handle");
let released = Arc::new(AtomicBool::new(false));
let released_clone = Arc::clone(&released);
let worker = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(150));
drop(handle);
released_clone.store(true, Ordering::SeqCst);
});
replace_artifact_cache_file(&tmp, &target).expect("replace must absorb the simulated AV lock");
worker.join().unwrap();
assert!(
released.load(Ordering::SeqCst),
"worker must have released the lock before the call returned",
);
assert_eq!(std::fs::read(&target).unwrap(), b"NEW");
}
#[cfg(windows)]
#[test]
fn replace_artifact_cache_file_does_not_retry_non_transient_errors() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("output.obj");
let tmp = dir.path().join("missing.obj.tmp");
let start = std::time::Instant::now();
let err = replace_artifact_cache_file(&tmp, &target).expect_err("must propagate NotFound");
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(45),
"non-transient errors must not enter the retry sleep — took {elapsed:?}"
);
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}