use super::super::*;
use super::{sample_plan, synthetic_target};
#[test]
fn mutating_a_restored_build_script_output_does_not_corrupt_cached_bundle_or_sibling_worktree() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cache");
let worktree_a = dir.path().join("worktree-a");
let worktree_b = dir.path().join("worktree-b");
synthetic_target(&worktree_a);
synthetic_target(&worktree_b);
let plan_a = sample_plan(&worktree_a, RustPlanMode::Thin);
let plan_b = sample_plan(&worktree_b, RustPlanMode::Thin);
save_rust_plan_local(&plan_a, &cache).unwrap();
std::fs::remove_dir_all(plan_a.target_dir.as_path()).unwrap();
restore_rust_plan_local(&plan_a, &cache).unwrap();
restore_rust_plan_local(&plan_b, &cache).unwrap();
let out_rel = "debug/build/serde-abc/out/gen.rs";
let path_a = plan_a.target_dir.join(out_rel);
let path_b = plan_b.target_dir.join(out_rel);
assert_eq!(std::fs::read(&path_a).unwrap(), b"generated");
assert_eq!(std::fs::read(&path_b).unwrap(), b"generated");
{
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&path_a)
.unwrap();
file.write_all(b"regenerated by build script").unwrap();
}
assert_eq!(
std::fs::read(&path_a).unwrap(),
b"regenerated by build script"
);
assert_eq!(
std::fs::read(&path_b).unwrap(),
b"generated",
"mutating worktree A's restored path corrupted worktree B's copy \
-- cache blast radius regression"
);
std::fs::remove_dir_all(plan_a.target_dir.as_path()).unwrap();
restore_rust_plan_local(&plan_a, &cache).unwrap();
assert_eq!(
std::fs::read(&path_a).unwrap(),
b"generated",
"mutating a restored path corrupted the cached bundle payload"
);
}
#[cfg(unix)]
#[test]
fn restored_files_never_share_an_inode_with_the_cached_bundle() {
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cache");
let worktree = dir.path().join("worktree");
synthetic_target(&worktree);
let plan = sample_plan(&worktree, RustPlanMode::Thin);
save_rust_plan_local(&plan, &cache).unwrap();
std::fs::remove_dir_all(plan.target_dir.as_path()).unwrap();
restore_rust_plan_local(&plan, &cache).unwrap();
let restored = plan.target_dir.join("debug/build/serde-abc/out/gen.rs");
let restored_inode = std::fs::metadata(&restored).unwrap().ino();
let bundle_root = cache.join("rust-plan");
for entry in walkdir_files(&bundle_root) {
if let Ok(bundle_meta) = std::fs::metadata(&entry) {
assert_ne!(
bundle_meta.ino(),
restored_inode,
"restored file at {restored:?} shares an inode with cached \
payload {entry:?} -- hardlink corruption regression"
);
}
}
let cache_snapshot_before: Vec<(std::path::PathBuf, u64)> = walkdir_files(&cache)
.into_iter()
.filter_map(|p| std::fs::metadata(&p).ok().map(|m| (p, m.len())))
.collect();
{
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&restored)
.unwrap();
file.write_all(b"mutated").unwrap();
}
let cache_snapshot_after: Vec<(std::path::PathBuf, u64)> = walkdir_files(&cache)
.into_iter()
.filter_map(|p| std::fs::metadata(&p).ok().map(|m| (p, m.len())))
.collect();
assert_eq!(
cache_snapshot_before, cache_snapshot_after,
"mutating a restored file changed cached bundle file sizes -- shared inode regression"
);
}
#[cfg(unix)]
fn walkdir_files(root: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else {
out.push(path);
}
}
}
out
}