wsb 0.291.8591

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
use super::*;
use crate::state::stamp_config::DeclaredHook;

fn git(dir: &Path, args: &[&str]) {
    let output = crate::git_backend::git_command()
        .arg("-C")
        .arg(dir)
        .args(["-c", "user.name=Test", "-c", "user.email=test@example.com"])
        .args(args)
        .output()
        .expect("git runs");
    assert!(output.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&output.stderr));
}

fn hook(run: &[&str]) -> Hook {
    Hook::Declared(DeclaredHook {
        run: run.iter().map(|word| word.to_string()).collect(),
        inputs: Some(HookInputs {
            repositories: vec![PathBuf::from("lib")],
            files: vec![PathBuf::from("deps.lock")],
            programs: Vec::new(),
        }),
        produces: vec![PathBuf::from(".build/dist")],
    })
}

fn reason(decision: Decision) -> String {
    match decision {
        Decision::Run { reason } => reason,
        other => panic!("expected a run, got {other:?}"),
    }
}

/// TEST20227: a prepare step that declares its inputs runs when one of them
/// changed, when what it produces is gone, when its command changed and when
/// nothing was recorded, and is skipped otherwise; committing the working
/// changes it was prepared from is no change. One that declares no inputs
/// always runs.
///
/// Run on every render, lungo's prepare-local took 77–80 s each time with
/// nothing changed, and 313 s in one checkpoint (26 Oct 2026).
#[test]
fn test20227_prepare_runs_only_when_what_it_builds_from_changed() {
    let scratch = tempfile::tempdir().unwrap();
    let root = scratch.path().join("tree");
    let lib = root.join("lib");
    std::fs::create_dir_all(&lib).unwrap();
    git(&lib, &["init", "--quiet", "-b", "main"]);
    std::fs::write(lib.join("lib.rs"), "fn one() {}\n").unwrap();
    git(&lib, &["add", "-A"]);
    git(&lib, &["commit", "--quiet", "-m", "one"]);
    std::fs::write(root.join("deps.lock"), "a = 1\n").unwrap();
    std::fs::create_dir_all(root.join(".build/dist")).unwrap();
    let state = scratch.path().join("state");
    let prepare = hook(&["make", "local-deps"]);

    let always = Hook::Command(vec!["make".to_string()]);
    assert_eq!(decide(&always, &root, &state).unwrap(), (Decision::Always, None));

    let (first, fingerprint) = decide(&prepare, &root, &state).unwrap();
    assert!(reason(first).contains("not prepared here"));
    record(&state, fingerprint.unwrap()).unwrap();
    let ran_with = |root: &Path| {
        let (decision, fingerprint) = decide(&prepare, root, &state).unwrap();
        (decision, fingerprint.unwrap())
    };
    assert!(matches!(ran_with(&root).0, Decision::Unchanged { .. }));

    // A working change in the repository, then the same change committed.
    std::fs::write(lib.join("lib.rs"), "fn two() {}\n").unwrap();
    let (changed, now) = ran_with(&root);
    assert_eq!(reason(changed), "repository lib changed");
    record(&state, now).unwrap();
    git(&lib, &["commit", "--quiet", "-am", "two"]);
    assert!(matches!(ran_with(&root).0, Decision::Unchanged { .. }), "a commit of what was prepared from");
    std::fs::write(lib.join("new.rs"), "\n").unwrap();
    assert_eq!(reason(ran_with(&root).0), "repository lib changed", "an untracked file");
    std::fs::remove_file(lib.join("new.rs")).unwrap();

    std::fs::write(root.join("deps.lock"), "a = 2\n").unwrap();
    let (changed, now) = ran_with(&root);
    assert_eq!(reason(changed), "file deps.lock changed");
    record(&state, now).unwrap();

    std::fs::remove_dir_all(root.join(".build/dist")).unwrap();
    assert_eq!(reason(ran_with(&root).0), ".build/dist is not there");
    std::fs::create_dir_all(root.join(".build/dist")).unwrap();

    let other = hook(&["make", "other-deps"]);
    assert_eq!(reason(decide(&other, &root, &state).unwrap().0), "command changed");

    forget(&state).unwrap();
    assert!(reason(ran_with(&root).0).contains("not prepared here"));
}