crates/mimir-ingest/tests/ingest/checkout.rs
//! The incremental checkout: same tree as a full checkout, untouched files
//! keep their mtimes, and cargo then rebuilds only what the commit touched.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, SystemTime};
use super::common::Scratch;
use mimir_ingest::checkout::checkout_changed;
fn clone_at(src: &Path, dest: &Path, sha: &str) {
nornir_git::gitio::clone_or_fetch(&src.to_string_lossy(), dest, None).unwrap();
nornir_git::gitio::detach_head_and_checkout(dest, sha).unwrap();
}
/// Every non-.git entry: relative path → (kind/mode, content or link target).
fn snapshot(root: &Path) -> BTreeMap<PathBuf, (String, Vec<u8>)> {
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<PathBuf, (String, Vec<u8>)>) {
for e in std::fs::read_dir(dir).unwrap().flatten() {
let p = e.path();
let rel = p.strip_prefix(root).unwrap().to_path_buf();
if rel.starts_with(".git") {
continue;
}
let md = std::fs::symlink_metadata(&p).unwrap();
if md.file_type().is_symlink() {
out.insert(rel, ("link".into(), std::fs::read_link(&p).unwrap().into_os_string().into_encoded_bytes()));
} else if md.is_dir() {
out.insert(rel.clone(), ("dir".into(), vec![]));
walk(root, &p, out);
} else {
use std::os::unix::fs::PermissionsExt;
let exec = md.permissions().mode() & 0o111 != 0;
out.insert(rel, (if exec { "exec" } else { "file" }.into(), std::fs::read(&p).unwrap()));
}
}
}
let mut out = BTreeMap::new();
walk(root, root, &mut out);
out
}
#[test]
fn incremental_checkout_equals_a_full_one_and_keeps_untouched_mtimes() {
let tmp = tempfile::tempdir().unwrap();
let src = Scratch::new(tmp.path().join("src"));
src.write("a/x.rs", "fn x() {}\n")
.write("b/y.rs", "fn y() {}\n")
.write("old/deep/gone.rs", "// removed later\n")
.write("t", "a file that becomes a directory\n")
.write("run.sh", "#!/bin/sh\n");
let c1 = src.commit("c1");
src.write("a/x.rs", "fn x() { changed() }\n").write("new/deep/z.rs", "fn z() {}\n");
std::fs::remove_dir_all(src.root.join("old")).unwrap();
std::fs::remove_file(src.root.join("t")).unwrap();
src.write("t/inner.rs", "fn inner() {}\n");
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(src.root.join("run.sh"), std::fs::Permissions::from_mode(0o755)).unwrap();
}
std::os::unix::fs::symlink("a/x.rs", src.root.join("link.rs")).unwrap();
let c2 = src.commit("c2");
let inc = tmp.path().join("inc");
clone_at(&src.root, &inc, &c1);
let old = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
std::fs::File::options().write(true).open(inc.join("b/y.rs")).unwrap().set_modified(old).unwrap();
let stats = checkout_changed(&inc, &c2).unwrap();
// written: a/x.rs, new/deep/z.rs, t/inner.rs, run.sh (mode), link.rs;
// removed: old/deep/gone.rs, the file `t`, run.sh (re-created executable)
assert_eq!((stats.written, stats.deleted, stats.full), (5, 3, false));
assert_eq!(std::fs::metadata(inc.join("b/y.rs")).unwrap().modified().unwrap(), old, "an untouched file was rewritten");
assert!(!inc.join("old").exists(), "emptied directories are pruned");
let full = tmp.path().join("full");
clone_at(&src.root, &full, &c2);
assert_eq!(snapshot(&inc), snapshot(&full));
assert_eq!(nornir_git::gitio::head_sha(&inc).unwrap(), c2);
// And back again.
checkout_changed(&inc, &c1).unwrap();
let back = tmp.path().join("back");
clone_at(&src.root, &back, &c1);
assert_eq!(snapshot(&inc), snapshot(&back));
}
/// Units cargo rebuilt (`fresh: false`) in one `cargo check`.
fn rebuilt(ws: &Path, target: &Path) -> Vec<String> {
let o = Command::new("cargo")
.args(["check", "--workspace", "--message-format=json", "--offline"])
.current_dir(ws)
.env("CARGO_TARGET_DIR", target)
.env("RUSTC_WRAPPER", "")
.env_remove("RUSTC_WORKSPACE_WRAPPER")
.output()
.unwrap();
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
let mut out: Vec<String> = String::from_utf8_lossy(&o.stdout)
.lines()
.filter(|l| l.contains("\"reason\":\"compiler-artifact\"") && l.contains("\"fresh\":false"))
.filter_map(|l| l.split("\"name\":\"").nth(1).and_then(|r| r.split('"').next()).map(str::to_string))
.collect();
out.sort();
out
}
#[test]
fn after_an_incremental_checkout_cargo_rebuilds_only_the_touched_crate_and_its_dependents() {
let tmp = tempfile::tempdir().unwrap();
let src = Scratch::new(tmp.path().join("src"));
src.write("Cargo.toml", "[workspace]\nmembers = [\"a\", \"b\", \"c\", \"d\"]\nresolver = \"3\"\n");
for k in ["a", "b", "c"] {
src.write(&format!("{k}/Cargo.toml"), &format!("[package]\nname = \"{k}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"))
.write(&format!("{k}/src/lib.rs"), &format!("pub fn {k}() -> u32 {{ 1 }}\n"));
}
src.write("d/Cargo.toml", "[package]\nname = \"d\"\nversion = \"0.1.0\"\nedition = \"2024\"\n[dependencies]\na = { path = \"../a\" }\n")
.write("d/src/lib.rs", "pub fn d() -> u32 { a::a() }\n");
let c1 = src.commit("c1");
src.write("a/src/lib.rs", "pub fn a() -> u32 { 2 }\n");
let c2 = src.commit("c2 touches a");
let wt = tmp.path().join("clone");
clone_at(&src.root, &wt, &c1);
let target = tmp.path().join("target");
assert_eq!(rebuilt(&wt, &target), vec!["a", "b", "c", "d"], "cold");
assert!(rebuilt(&wt, &target).is_empty(), "warm, nothing changed");
checkout_changed(&wt, &c2).unwrap();
assert_eq!(rebuilt(&wt, &target), vec!["a", "d"], "only a and its dependent d");
// Control: the full-tree writer on the SAME content dirties everything.
nornir_git::gitio::detach_head_and_checkout(&wt, &c2).unwrap();
assert_eq!(rebuilt(&wt, &target), vec!["a", "b", "c", "d"], "the full writer rewrites every mtime");
}