use std::path::{Path, PathBuf};
use haz_cache::{
CHAPTER_REVISION, CacheKey, CacheWriter, HashFunctionLabel, Manifest, StoreInputs,
StoredOutput, layout,
};
use haz_domain::settings::cache::HashAlgo;
use haz_vfs::StdFilesystem;
use tempfile::TempDir;
fn sample_key() -> CacheKey {
let mut bytes = [0u8; 32];
bytes[0] = 0xAB;
bytes[1] = 0xCD;
CacheKey::from_bytes(bytes)
}
fn key_with_first_byte(first: u8) -> CacheKey {
let mut bytes = [0u8; 32];
bytes[0] = first;
CacheKey::from_bytes(bytes)
}
fn fresh_workspace() -> TempDir {
tempfile::tempdir().expect("create workspace tempdir")
}
fn make_cache(workspace_root: &Path, algo: HashAlgo) -> CacheWriter<StdFilesystem> {
CacheWriter::new(StdFilesystem::new(), workspace_root, algo)
}
fn write_workspace_file(workspace_root: &Path, rel: &str, bytes: &[u8]) -> PathBuf {
let target = workspace_root.join(rel);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).expect("create parent dir");
}
std::fs::write(&target, bytes).expect("write workspace file");
target
}
fn len_u64(s: &[u8]) -> u64 {
u64::try_from(s.len()).expect("len fits u64")
}
#[cfg(unix)]
fn read_unix_mode(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.expect("metadata")
.permissions()
.mode()
& 0o777
}
#[test]
fn store_then_lookup_then_restore_round_trip() {
let workspace = fresh_workspace();
let root = workspace.path();
let on_disk = write_workspace_file(root, "proj/out", b"hello-world");
let cache = make_cache(root, HashAlgo::Blake3);
let key = sample_key();
let outs = [StoredOutput {
workspace_absolute_path: "/proj/out",
on_disk_path: &on_disk,
mode: 0o644,
}];
cache
.store(
&key,
&StoreInputs {
outputs: &outs,
stdout: b"out-bytes",
stderr: b"err-bytes",
created_at_unix: 1_715_700_000,
},
)
.expect("store succeeds");
let manifest = cache.reader().lookup(&key).expect("lookup hits");
assert_eq!(manifest.outputs.len(), 1);
assert_eq!(manifest.stdout_len, len_u64(b"out-bytes"));
assert_eq!(manifest.stderr_len, len_u64(b"err-bytes"));
std::fs::write(&on_disk, b"divergent").expect("mutate on_disk");
let restored = cache.restore(&manifest).expect("restore succeeds");
assert_eq!(restored.stdout, b"out-bytes");
assert_eq!(restored.stderr, b"err-bytes");
assert_eq!(
std::fs::read(&on_disk).expect("read on_disk"),
b"hello-world"
);
}
#[cfg(unix)]
#[test]
fn output_blob_preserves_unix_mode_bits() {
let workspace = fresh_workspace();
let root = workspace.path();
let on_disk = write_workspace_file(root, "bin/runme", b"#!/bin/sh\necho hi\n");
let cache = make_cache(root, HashAlgo::Blake3);
let key = sample_key();
let outs = [StoredOutput {
workspace_absolute_path: "/bin/runme",
on_disk_path: &on_disk,
mode: 0o755,
}];
cache
.store(
&key,
&StoreInputs {
outputs: &outs,
stdout: b"",
stderr: b"",
created_at_unix: 0,
},
)
.expect("store");
std::fs::remove_file(&on_disk).expect("remove on_disk");
let manifest = cache.reader().lookup(&key).expect("lookup");
cache.restore(&manifest).expect("restore");
assert_eq!(
std::fs::read(&on_disk).expect("read restored file"),
b"#!/bin/sh\necho hi\n"
);
assert_eq!(read_unix_mode(&on_disk), 0o755);
}
#[test]
fn tmp_entry_directory_atomically_publishes_as_entry_dir() {
let workspace = fresh_workspace();
let root = workspace.path();
let on_disk = write_workspace_file(root, "proj/out", b"x");
let cache = make_cache(root, HashAlgo::Blake3);
let key = sample_key();
let outs = [StoredOutput {
workspace_absolute_path: "/proj/out",
on_disk_path: &on_disk,
mode: 0o644,
}];
cache
.store(
&key,
&StoreInputs {
outputs: &outs,
stdout: b"",
stderr: b"",
created_at_unix: 0,
},
)
.expect("store");
let entry = layout::entry_dir(cache.cache_root(), &key);
assert!(
entry.is_dir(),
"entry dir must be published at the sharded path"
);
let shard = layout::shard_dir(cache.cache_root(), &key);
for read in std::fs::read_dir(&shard).expect("read shard dir") {
let name = read.expect("dir entry").file_name();
let name_str = name.to_string_lossy();
assert!(
!name_str.starts_with(".tmp-"),
"no .tmp-* should remain after a successful store, found: {name_str}"
);
}
}
#[test]
fn restore_leaves_no_staging_directory_after_success() {
let workspace = fresh_workspace();
let root = workspace.path();
let on_disk = write_workspace_file(root, "proj/out", b"x");
let cache = make_cache(root, HashAlgo::Blake3);
let key = sample_key();
let outs = [StoredOutput {
workspace_absolute_path: "/proj/out",
on_disk_path: &on_disk,
mode: 0o644,
}];
cache
.store(
&key,
&StoreInputs {
outputs: &outs,
stdout: b"",
stderr: b"",
created_at_unix: 0,
},
)
.expect("store");
let manifest = cache.reader().lookup(&key).expect("lookup");
cache.restore(&manifest).expect("restore");
for read in std::fs::read_dir(cache.cache_root()).expect("read cache_root") {
let name = read.expect("dir entry").file_name();
let name_str = name.to_string_lossy();
assert!(
!name_str.starts_with(".restore-"),
"no .restore-* should remain after a successful restore, found: {name_str}"
);
}
}
#[test]
fn second_store_of_same_key_overwrites_and_remains_a_hit() {
let workspace = fresh_workspace();
let root = workspace.path();
let on_disk = write_workspace_file(root, "proj/out", b"v1");
let cache = make_cache(root, HashAlgo::Blake3);
let key = sample_key();
let outs = [StoredOutput {
workspace_absolute_path: "/proj/out",
on_disk_path: &on_disk,
mode: 0o644,
}];
cache
.store(
&key,
&StoreInputs {
outputs: &outs,
stdout: b"first",
stderr: b"first-err",
created_at_unix: 1,
},
)
.expect("first store succeeds");
std::fs::write(&on_disk, b"v2-longer").expect("rewrite output for second run");
cache
.store(
&key,
&StoreInputs {
outputs: &outs,
stdout: b"second",
stderr: b"second-err",
created_at_unix: 2,
},
)
.expect("second store with same key must also succeed");
let manifest = cache
.reader()
.lookup(&key)
.expect("entry hits after second store");
assert_eq!(manifest.stdout_len, len_u64(b"second"));
assert_eq!(manifest.created_at_unix, 2);
}
#[test]
fn clear_empties_a_populated_cache_on_real_fs() {
let workspace = fresh_workspace();
let root = workspace.path();
let on_disk = write_workspace_file(root, "proj/out", b"x");
let sibling = root.join("unrelated.txt");
std::fs::write(&sibling, b"keep me").expect("write sibling");
let cache = make_cache(root, HashAlgo::Blake3);
let key = sample_key();
let outs = [StoredOutput {
workspace_absolute_path: "/proj/out",
on_disk_path: &on_disk,
mode: 0o644,
}];
cache
.store(
&key,
&StoreInputs {
outputs: &outs,
stdout: b"",
stderr: b"",
created_at_unix: 0,
},
)
.expect("store");
assert!(
cache.reader().lookup(&key).is_some(),
"precondition: entry present"
);
cache.clear().expect("clear");
assert!(
cache.reader().lookup(&key).is_none(),
"lookup must miss after clear"
);
assert_eq!(
std::fs::read(&sibling).expect("read sibling"),
b"keep me",
"files outside cache_root must be untouched"
);
}
#[test]
fn clean_soft_prunes_chapter_revision_mismatch_entry() {
let workspace = fresh_workspace();
let root = workspace.path();
let cache = make_cache(root, HashAlgo::Blake3);
let key = key_with_first_byte(0xAB);
let entry_dir = layout::entry_dir(cache.cache_root(), &key);
std::fs::create_dir_all(&entry_dir).expect("mkdir entry_dir");
let manifest = Manifest {
chapter_revision: CHAPTER_REVISION.saturating_add(1),
hash_function: HashFunctionLabel::Blake3,
key,
outputs: vec![],
stdout_len: 0,
stderr_len: 0,
stdout_hash: [0u8; 32],
stderr_hash: [0u8; 32],
exit_status: 0,
created_at_unix: 0,
};
std::fs::write(
layout::manifest_path(cache.cache_root(), &key),
manifest.to_json_bytes(),
)
.expect("write manifest");
let opts = haz_cache::CleanOptions {
soft: true,
..Default::default()
};
let outcome = cache.clean(&opts).expect("clean --soft");
assert!(outcome.failures.is_empty(), "no failures on a clean run");
let report = outcome.report;
assert_eq!(report.evicted_by_soft, 1);
assert!(
!entry_dir.exists(),
"entry dir must be gone after clean --soft prunes it"
);
}
#[test]
fn store_then_lookup_round_trip_under_sha256() {
let workspace = fresh_workspace();
let root = workspace.path();
let on_disk = write_workspace_file(root, "proj/out", b"sha-bytes");
let cache = make_cache(root, HashAlgo::Sha256);
let key = sample_key();
let outs = [StoredOutput {
workspace_absolute_path: "/proj/out",
on_disk_path: &on_disk,
mode: 0o600,
}];
cache
.store(
&key,
&StoreInputs {
outputs: &outs,
stdout: b"sha-out",
stderr: b"sha-err",
created_at_unix: 0,
},
)
.expect("store under sha256");
let manifest = cache.reader().lookup(&key).expect("lookup");
assert_eq!(manifest.hash_function, HashFunctionLabel::Sha256);
assert_eq!(manifest.outputs.len(), 1);
assert_eq!(manifest.stdout_len, len_u64(b"sha-out"));
}