use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use super::{Outcome, is_fresh, prune, read, slug, write as write_record};
use crate::clock::Clock;
const RAN_AT: u64 = 1_700_000_000;
fn write(dir: &Path, command: &str, digest: &str, ok: bool) {
write_record(
dir,
command,
Clock::pinned(RAN_AT),
&Outcome {
digest,
ok,
..Outcome::default()
},
);
}
#[test]
fn fresh_only_for_matching_successful_record() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
assert!(!is_fresh(base, "sh", "digest-a"), "no record yet");
write(base, "sh", "digest-a", true);
assert!(
is_fresh(base, "sh", "digest-a"),
"matching ok record is fresh"
);
assert!(!is_fresh(base, "sh", "digest-b"), "different digest misses");
write(base, "sh", "digest-a", false);
assert!(
!is_fresh(base, "sh", "digest-a"),
"failed record is never fresh"
);
}
#[test]
fn read_exposes_record_fields_for_macros() {
let dir = tempfile::tempdir().expect("tempdir");
write(dir.path(), "cargo test", "d1", true);
let cached = read(dir.path(), "cargo test").expect("record");
assert_eq!(
cached.fields.get("command").map(String::as_str),
Some("cargo test"),
"string field exposed"
);
assert_eq!(
cached.fields.get("status").map(String::as_str),
Some("ok"),
"enum field exposed in its serialized spelling"
);
assert_eq!(
cached.fields.get("input_digest").map(String::as_str),
Some("d1")
);
assert!(
cached.fields.contains_key("ran_at"),
"numeric field exposed for macros"
);
}
#[test]
fn declared_outputs_are_stored_with_the_record() {
let dir = tempfile::tempdir().expect("tempdir");
write_record(
dir.path(),
"just cover",
Clock::pinned(RAN_AT),
&Outcome {
digest: "d1",
ok: true,
outputs: &[PathBuf::from("target/coverage/lcov.info")],
..Outcome::default()
},
);
let cached = read(dir.path(), "just cover").expect("record");
assert_eq!(
cached.outputs,
vec!["target/coverage/lcov.info".to_owned()],
"the record remembers what the run promised to produce"
);
write(dir.path(), "cargo test", "d2", true);
let bare = read(dir.path(), "cargo test").expect("record");
assert!(
bare.outputs.is_empty(),
"a rule declaring no outputs records none"
);
}
#[test]
fn probe_digests_are_stored_with_the_record() {
let dir = tempfile::tempdir().expect("tempdir");
let probes: BTreeMap<String, String> = [("fmt-recipe".to_owned(), "abc".to_owned())]
.into_iter()
.collect();
write_record(
dir.path(),
"just fmt-check",
Clock::pinned(RAN_AT),
&Outcome {
digest: "d1",
ok: true,
probes: probes.clone(),
..Outcome::default()
},
);
let cached = read(dir.path(), "just fmt-check").expect("record");
assert_eq!(
cached.probes, probes,
"the record remembers what each probe printed, so a later stale verdict can name the one that moved"
);
write(dir.path(), "cargo test", "d2", true);
let bare = read(dir.path(), "cargo test").expect("record");
assert!(
bare.probes.is_empty(),
"a rule naming no probes records none"
);
}
#[test]
fn distinct_commands_get_distinct_records() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
write(base, "cargo test", "d1", true);
write(base, "cargo build", "d2", true);
assert!(is_fresh(base, "cargo test", "d1"));
assert!(is_fresh(base, "cargo build", "d2"));
}
#[test]
fn read_surfaces_the_clock_it_was_stamped_from() {
let dir = tempfile::tempdir().expect("tempdir");
write(dir.path(), "sh", "d", true);
let cached = read(dir.path(), "sh").expect("record");
assert_eq!(
cached.ran_at, RAN_AT,
"ran_at is the clock the writer was handed, not one read here"
);
}
#[test]
fn slug_is_readable_distinct_capped_and_never_empty() {
assert!(slug("cargo test").starts_with("cargo-test-"));
assert_ne!(slug("cargo test"), slug("cargo build"));
assert!(
slug("+++").starts_with("cmd-"),
"all-symbol name gets a stem"
);
let long = "x".repeat(500);
let stem = slug(&long);
assert_eq!(stem.len(), super::SLUG_MAX + 1 + 16, "stem is capped");
}
#[test]
fn write_is_atomic_and_leaves_no_temp_file() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
write(base, "cargo test", "digest-a", true);
let temps: Vec<_> = std::fs::read_dir(base)
.expect("cache dir")
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "tmp"))
.collect();
assert!(temps.is_empty(), "rename leaves no .tmp behind");
assert!(
is_fresh(base, "cargo test", "digest-a"),
"record is readable"
);
}
#[test]
fn prune_drops_only_orphan_records() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
write(base, "cargo test", "d1", true);
write(base, "cargo build", "d2", true);
let live: BTreeSet<String> = ["cargo test".to_owned()].into_iter().collect();
let pruned = prune(base, &live).expect("prune");
assert_eq!(pruned, vec!["cargo build".to_owned()], "orphan removed");
assert!(is_fresh(base, "cargo test", "d1"), "live record kept");
assert!(read(base, "cargo build").is_none(), "orphan record gone");
}
#[test]
fn prune_on_missing_dir_is_empty() {
let dir = tempfile::tempdir().expect("tempdir");
let pruned = prune(&dir.path().join("absent"), &BTreeSet::new()).expect("prune");
assert!(pruned.is_empty(), "missing cache dir prunes nothing");
}
fn write_at(dir: &Path, command: &str, digest: &str, ran_at: u64) {
write_record(
dir,
command,
Clock::pinned(ran_at),
&Outcome {
digest,
ok: true,
..Outcome::default()
},
);
}
fn live(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|name| (*name).to_owned()).collect()
}
#[test]
fn import_copies_live_records_and_keeps_newer_local_ones() {
let from = tempfile::tempdir().expect("tempdir");
let to = tempfile::tempdir().expect("tempdir");
write_at(from.path(), "new", "theirs", 20);
write_at(from.path(), "kept", "theirs", 10);
write_at(from.path(), "dead", "theirs", 10);
write_at(to.path(), "new", "ours", 10);
write_at(to.path(), "kept", "ours", 20);
let names = live(&["new", "kept"]);
let imported = super::import(from.path(), to.path(), &names).expect("import");
assert_eq!(imported, ["new"], "only the newer live record moves");
assert!(
is_fresh(to.path(), "new", "theirs"),
"newer record replaced"
);
assert!(is_fresh(to.path(), "kept", "ours"), "newer local survives");
assert!(read(to.path(), "dead").is_none(), "unknown rule skipped");
}
#[test]
fn import_skips_an_identical_record_and_a_missing_source() {
let from = tempfile::tempdir().expect("tempdir");
let to = tempfile::tempdir().expect("tempdir");
write_at(from.path(), "sh", "same", 10);
write_at(to.path(), "sh", "same", 10);
let names = live(&["sh"]);
let none = super::import(from.path(), to.path(), &names).expect("import");
assert!(none.is_empty(), "an identical record needs no import");
let gone = from.path().join("absent");
let none = super::import(&gone, to.path(), &names).expect("import");
assert!(none.is_empty(), "a missing source directory is empty");
}
#[test]
fn import_leaves_unparseable_files_alone() {
let from = tempfile::tempdir().expect("tempdir");
let to = tempfile::tempdir().expect("tempdir");
std::fs::write(from.path().join("junk.yaml"), "not: [a record").expect("junk");
std::fs::write(from.path().join("note.txt"), "hi").expect("note");
let imported = super::import(from.path(), to.path(), &live(&["sh"])).expect("import");
assert!(imported.is_empty(), "nothing parseable, nothing imported");
}