zccache 1.12.16

Local-first compiler cache for C/C++/Rust/Emscripten
Documentation
//! Regression test locking in the mutation-isolation property that
//! `restore_bundle_file` (rust_plan/local.rs) provides via reflink-or-copy:
//! a live-target mutation of a restored file must never corrupt the
//! cached content-addressed bundle, nor any other worktree/target sharing
//! that cache dir.
//!
//! No prior test exercised this end-to-end through the real restore path.

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");

    // Two sibling "worktrees" sharing one content-addressed cache dir.
    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 from worktree A, seeding the shared cache bundle.
    save_rust_plan_local(&plan_a, &cache).unwrap();

    // Both worktrees restore from the same bundle (identical plan inputs
    // produce the same cache key).
    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");

    // Simulate a build script re-running and rewriting its OUT_DIR output
    // in place through the restored path (open + write, not remove+create
    // -- the corruption-prone pattern many build scripts and post-link
    // tools use).
    {
        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"
    );

    // Sibling worktree B's restored copy must be unaffected.
    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"
    );

    // The cached bundle payload itself must be unaffected: restoring again
    // into a fresh target dir must still produce the original bytes.
    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();

    // Confirm the restored copy does not share an inode with any file
    // under the cache dir (reflink-or-copy, never a hardlink into the
    // shared bundle).
    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"
            );
        }
    }

    // Belt-and-suspenders: writing through the restored path must not
    // change any file under the cache dir.
    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
}