use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use std::process::Command;
use haz_cache::CacheWriter;
use haz_cache::key::CacheKey;
use haz_cache::key::prefix::CHAPTER_REVISION;
use haz_cache::layout;
use haz_cache::manifest::{HashFunctionLabel, Manifest};
use haz_cache::store::{StoreInputs, StoredOutput};
use haz_domain::settings::cache::HashAlgo;
use haz_vfs::StdFilesystem;
use tempfile::TempDir;
const HAZ_BIN: &str = env!("CARGO_BIN_EXE_haz");
const HASH_ALGO: HashAlgo = HashAlgo::Blake3;
fn fixture_tempdir(name: &str) -> TempDir {
let src = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/cache_info")
.join(name);
let dst = TempDir::new().expect("tempdir creation failed");
copy_dir_recursive(&src, dst.path());
dst
}
fn copy_dir_recursive(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_dir_recursive(&src_path, &dst_path);
} else {
fs::copy(&src_path, &dst_path).unwrap();
}
}
}
fn haz_cache_info(workspace_root: &Path) -> (String, String, Option<i32>) {
let output = Command::new(HAZ_BIN)
.args(["cache", "info"])
.current_dir(workspace_root)
.output()
.expect("failed to spawn haz");
(
String::from_utf8(output.stdout).expect("haz stdout was not utf-8"),
String::from_utf8(output.stderr).expect("haz stderr was not utf-8"),
output.status.code(),
)
}
fn build_cache(workspace_root: &Path) -> CacheWriter<StdFilesystem> {
CacheWriter::new(StdFilesystem::new(), workspace_root, HASH_ALGO)
}
fn key_with_first_byte(first: u8) -> CacheKey {
let mut bytes = [0u8; 32];
bytes[0] = first;
CacheKey::from_bytes(bytes)
}
fn store_valid_entry(cache: &CacheWriter<StdFilesystem>, key: &CacheKey, rel: &str, bytes: &[u8]) {
let workspace_root = cache.workspace_root().to_path_buf();
let target = workspace_root.join(rel);
let anchored = format!("/{rel}");
fs::create_dir_all(target.parent().unwrap()).unwrap();
fs::write(&target, bytes).unwrap();
let outs = [StoredOutput {
workspace_absolute_path: &anchored,
on_disk_path: &target,
mode: 0o644,
}];
cache
.store(
key,
&StoreInputs {
outputs: &outs,
stdout: b"",
stderr: b"",
created_at_unix: 0,
},
)
.expect("store should succeed for a fresh key");
}
fn write_schema_stale_entry(cache: &CacheWriter<StdFilesystem>, key: &CacheKey) {
let entry_dir = layout::entry_dir(cache.cache_root(), key);
fs::create_dir_all(&entry_dir).unwrap();
let manifest = Manifest {
chapter_revision: CHAPTER_REVISION.saturating_add(1),
hash_function: HashFunctionLabel::Blake3,
key: *key,
outputs: vec![],
stdout_len: 0,
stderr_len: 0,
stdout_hash: [0u8; 32],
stderr_hash: [0u8; 32],
exit_status: 0,
created_at_unix: 0,
};
fs::write(
layout::manifest_path(cache.cache_root(), key),
manifest.to_json_bytes(),
)
.unwrap();
}
fn write_corrupt_entry_without_manifest(cache: &CacheWriter<StdFilesystem>, key: &CacheKey) {
let entry_dir = layout::entry_dir(cache.cache_root(), key);
fs::create_dir_all(&entry_dir).unwrap();
fs::write(entry_dir.join("stdout"), b"").unwrap();
}
fn write_orphan_tmp_dir(cache: &CacheWriter<StdFilesystem>, key: &CacheKey, random: &str) {
let tmp = layout::tmp_entry_dir(cache.cache_root(), key, random);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("partial.bin"), b"scratch").unwrap();
}
fn snapshot_dir(root: &Path) -> BTreeMap<String, Vec<u8>> {
let mut out = BTreeMap::new();
snapshot_into(root, root, &mut out);
out
}
fn snapshot_into(base: &Path, dir: &Path, out: &mut BTreeMap<String, Vec<u8>>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(ft) = entry.file_type() else { continue };
if ft.is_file() {
let rel = path
.strip_prefix(base)
.map_or_else(|_| path.display().to_string(), |p| p.display().to_string());
let bytes = fs::read(&path).unwrap_or_default();
out.insert(rel, bytes);
} else if ft.is_dir() {
snapshot_into(base, &path, out);
}
}
}
#[test]
fn aux_019_fresh_workspace_reports_zeros_and_exits_zero() {
let tmp = fixture_tempdir("fresh_workspace");
let (stdout, stderr, code) = haz_cache_info(tmp.path());
assert_eq!(code, Some(0), "expected exit 0; stderr: {stderr}");
assert!(
stdout.contains("well-formed entries: 0"),
"missing well-formed=0: {stdout}",
);
assert!(
stdout.contains("corrupt entries: 0"),
"missing corrupt=0: {stdout}",
);
assert!(
stdout.contains("orphan tmp directories: 0"),
"missing orphan-tmp=0: {stdout}",
);
assert!(
stdout.contains("orphan restore directories: 0"),
"missing orphan-restore=0: {stdout}",
);
assert!(
stdout.contains("total bytes: 0"),
"missing total-bytes=0: {stdout}",
);
assert!(
!stdout.contains("by schema:"),
"schema section should be absent: {stdout}",
);
}
fn populate_mixed_cache(workspace_root: &Path) {
let cache = build_cache(workspace_root);
let key_a = key_with_first_byte(0xAA);
store_valid_entry(&cache, &key_a, "lib/out_a", b"hello");
let key_b = key_with_first_byte(0xBB);
store_valid_entry(&cache, &key_b, "lib/out_b", b"world!");
let key_c = key_with_first_byte(0xCC);
write_corrupt_entry_without_manifest(&cache, &key_c);
let key_d = key_with_first_byte(0xDD);
write_schema_stale_entry(&cache, &key_d);
let key_e = key_with_first_byte(0xEE);
write_orphan_tmp_dir(&cache, &key_e, "tmpsuffix");
}
#[test]
fn aux_019_populated_cache_reports_all_categories() {
let tmp = fixture_tempdir("populated_with_mixed_entries");
populate_mixed_cache(tmp.path());
let (stdout, stderr, code) = haz_cache_info(tmp.path());
assert_eq!(code, Some(0), "expected exit 0; stderr: {stderr}");
assert!(
stdout.contains("well-formed entries: 2"),
"expected 2 well-formed: {stdout}",
);
assert!(
stdout.contains("corrupt entries: 2"),
"expected 2 corrupt (no-manifest + schema-mismatched): {stdout}",
);
assert!(
stdout.contains("orphan tmp directories: 1"),
"expected 1 orphan tmp: {stdout}",
);
assert!(
stdout.contains("orphan restore directories: 0"),
"expected 0 orphan restore: {stdout}",
);
assert!(
stdout.contains("by schema:"),
"missing schema section header: {stdout}",
);
assert!(
stdout.contains(&format!("(revision={CHAPTER_REVISION}, hash=blake3): 2")),
"missing schema breakdown for the two valid entries: {stdout}",
);
}
#[test]
fn aux_020_repeated_invocation_does_not_alter_cache_root() {
let tmp = fixture_tempdir("populated_with_mixed_entries");
populate_mixed_cache(tmp.path());
let cache_root = tmp.path().join(".haz/cache");
let before = snapshot_dir(&cache_root);
let (_, _, code1) = haz_cache_info(tmp.path());
assert_eq!(code1, Some(0));
let (_, _, code2) = haz_cache_info(tmp.path());
assert_eq!(code2, Some(0));
let after = snapshot_dir(&cache_root);
assert_eq!(
before, after,
"cache root state must be byte-identical before and after two info invocations",
);
}