mbx 1.24.1

A build cache for Rust projects
Documentation
use super::*;
use crate::store;

fn project(workspace_root: PathBuf) -> store::ProjectUsage {
    store::ProjectUsage {
        workspace_root,
        identities: 1,
        action_bytes: 2,
        target_bytes: 3,
        live: true,
    }
}

#[test]
fn interactive_removal_only_removes_selected_workspaces() {
    let selected = PathBuf::from("/workspace/selected");
    let skipped = PathBuf::from("/workspace/skipped");
    let mut removed = Vec::new();

    let exit = cache_remove_interactive_with(
        &[project(selected.clone()), project(skipped)],
        |_| Ok(vec![selected.clone()]),
        |_| Ok(true),
        |workspace| {
            removed.push(workspace.to_owned());
            Ok(())
        },
    )
    .unwrap();

    assert_eq!(exit, ExitCode::SUCCESS);
    assert_eq!(removed, [selected]);
}

#[test]
fn interactive_removal_cancellation_removes_nothing() {
    let selected = PathBuf::from("/workspace/selected");

    let exit = cache_remove_interactive_with(
        &[project(selected.clone())],
        |_| Ok(vec![selected]),
        |_| Ok(false),
        |_| panic!("cancelled removal must not run"),
    )
    .unwrap();

    assert_eq!(exit, ExitCode::SUCCESS);
}

#[test]
fn interactive_removal_with_no_selection_removes_nothing() {
    let exit = cache_remove_interactive_with(
        &[project(PathBuf::from("/workspace/selected"))],
        |_| Ok(Vec::new()),
        |_| panic!("empty selection must not be confirmed"),
        |_| panic!("empty selection must not be removed"),
    )
    .unwrap();

    assert_eq!(exit, ExitCode::SUCCESS);
}

#[test]
fn interactive_removal_with_no_recorded_workspaces_removes_nothing() {
    let exit = cache_remove_interactive_with(
        &[],
        |_| panic!("empty workspace list must not be selected"),
        |_| panic!("empty workspace list must not be confirmed"),
        |_| panic!("empty workspace list must not be removed"),
    )
    .unwrap();

    assert_eq!(exit, ExitCode::SUCCESS);
}

#[test]
fn interactive_removal_continues_after_a_workspace_fails() {
    let first = PathBuf::from("/workspace/first");
    let second = PathBuf::from("/workspace/second");
    let mut attempted = Vec::new();

    let exit = cache_remove_selected_with(&[first.clone(), second.clone()], |workspace| {
        attempted.push(workspace.to_owned());
        if workspace == first {
            eyre::bail!("target is busy");
        }
        Ok(())
    });

    assert_eq!(exit, ExitCode::FAILURE);
    assert_eq!(attempted, [first, second]);
}

#[test]
fn cache_remove_requires_exactly_one_mode() {
    let missing = ["mbx", "cache", "remove"].map(std::ffi::OsStr::new);
    assert!(Cli::try_parse_from(&missing).is_err());

    let conflicting =
        ["mbx", "cache", "remove", "--interactive", "/workspace"].map(std::ffi::OsStr::new);
    assert!(Cli::try_parse_from(&conflicting).is_err());

    let interactive = ["mbx", "cache", "remove", "--interactive"].map(std::ffi::OsStr::new);
    let cli = Cli::try_parse_from(&interactive).unwrap();
    let Commands::Cache(CacheArgs {
        command: CacheCommands::Remove(_),
    }) = cli.command
    else {
        panic!("interactive cache removal should parse");
    };
}

#[test]
fn cache_removal_uses_the_workspace_identity_reported_by_cargo() {
    let directory = tempfile::tempdir().unwrap();
    let requested = directory.path().join("workspace-alias");
    std::fs::create_dir_all(&requested).unwrap();
    std::fs::write(
        requested.join("Cargo.toml"),
        "[package]\nname = \"cache-remove-root\"\nversion = \"0.0.0\"\n",
    )
    .unwrap();
    std::fs::create_dir(requested.join("src")).unwrap();
    std::fs::write(requested.join("src/lib.rs"), "").unwrap();

    let reported = cache_workspace_root(std::ffi::OsStr::new("cargo"), &requested);

    let metadata = std::process::Command::new("cargo")
        .args([
            "metadata",
            "--no-deps",
            "--format-version",
            "1",
            "--manifest-path",
        ])
        .arg(requested.join("Cargo.toml"))
        .output()
        .unwrap();
    assert!(metadata.status.success());
    assert_eq!(
        reported,
        parse_cargo_roots(&metadata.stdout).unwrap().workspace_root
    );
}

#[test]
fn cache_removal_preserves_the_requested_spelling_when_cargo_cannot_resolve_it() {
    let requested = PathBuf::from("/workspace/that/does/not/exist");

    assert_eq!(
        cache_workspace_root(std::ffi::OsStr::new("definitely-not-cargo"), &requested),
        requested
    );
}