use std::collections::BTreeSet;
use std::path::Path;
use crate::error::Result;
use crate::manifest::Manifest;
use crate::{cache, parametric};
pub fn prune(cwd: &Path) -> Result<String> {
let located = Manifest::locate(cwd)?;
let manifest = &located.manifest;
let base = located.root.as_path();
let cache_dir = base.join(&manifest.cache_dir);
let live = live_identities(manifest, base)?;
let pruned = cache::prune(&cache_dir, &live)?;
Ok(render(&pruned))
}
pub(crate) fn live_identities(manifest: &Manifest, base: &Path) -> Result<BTreeSet<String>> {
let mut live = BTreeSet::new();
for rule in &manifest.commands {
for hit in parametric::expand_rule(manifest, base, rule)? {
live.insert(hit.exp.identity);
}
}
Ok(live)
}
fn render(pruned: &[String]) -> String {
if pruned.is_empty() {
return "no orphan records to prune\n".to_owned();
}
let mut out = format!("pruned {} orphan record(s):\n", pruned.len());
for name in pruned {
out.push_str(&format!(" {name}\n"));
}
out
}
#[cfg(test)]
mod tests {
use super::prune;
use crate::cache;
fn manifest(root: &std::path::Path, body: &str) {
let dir = root.join(".mmz");
std::fs::create_dir_all(&dir).expect("mkdir .mmz");
std::fs::write(dir.join("config.yaml"), body).expect("write manifest");
}
fn record(cache_dir: &std::path::Path, command: &str) {
cache::write(
cache_dir,
command,
crate::clock::Clock::pinned(1_700_000_000),
&cache::Outcome {
digest: "d",
ok: true,
..cache::Outcome::default()
},
);
}
#[test]
fn prunes_records_for_rules_not_in_the_manifest() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
manifest(base, "cache_dir: .cache\ncommands:\n - name: cargo test\n");
let cache_dir = base.join(".cache");
record(&cache_dir, "cargo test");
record(&cache_dir, "cargo bench");
let first = prune(base).expect("prune");
assert!(first.contains("cargo bench"), "orphan reported: {first}");
assert!(first.contains("pruned 1"), "count reported: {first}");
assert!(
cache::read(&cache_dir, "cargo test").is_some(),
"live record kept"
);
assert!(
cache::read(&cache_dir, "cargo bench").is_none(),
"orphan removed"
);
let again = prune(base).expect("prune");
assert!(
again.contains("no orphan"),
"nothing left to prune: {again}"
);
}
#[test]
fn parametric_expansions_are_live_until_their_file_is_gone() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
std::fs::create_dir(base.join("src")).expect("mkdir");
std::fs::write(base.join("src/a.rs"), b"a").expect("a");
std::fs::write(base.join("src/b.rs"), b"b").expect("b");
manifest(
base,
"cache_dir: .cache\nscopes:\n targets: [\"src/**/*.rs\"]\ncommands:\n - name: \"run {targets}\"\n",
);
let cache_dir = base.join(".cache");
record(&cache_dir, "run src/a.rs");
record(&cache_dir, "run src/b.rs");
std::fs::remove_file(base.join("src/b.rs")).expect("rm b");
let pruned = prune(base).expect("prune");
assert!(pruned.contains("run src/b.rs"), "orphan pruned: {pruned}");
assert!(
cache::read(&cache_dir, "run src/a.rs").is_some(),
"live expansion kept"
);
assert!(
cache::read(&cache_dir, "run src/b.rs").is_none(),
"orphan expansion gone"
);
}
#[test]
fn missing_manifest_is_an_error() {
let dir = tempfile::tempdir().expect("tempdir");
assert!(prune(dir.path()).is_err(), "no manifest is fatal");
}
}