Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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");
}