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

nordisk/gunnar

xtask/tests/both_ways_runs.rs

//! `cargo xtask crates-io-install` and `lone-clone` (`xtask/src/both_ways.rs`): the step
//! runner's verdicts, driven through stand-in `cargo` programs. An install that leaves a
//! binary that answers passes and says what it said; a failed step is reported by the
//! compiler's own lines only; an install that leaves nothing fails; and a lone clone
//! whose committed lock names a crate by path is refused before anything is built.

use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;

use xtask::both_ways::{crates_io_install, lone_clone, lone_home, Verdict};

/// `crates_io_install` works in ONE fixed directory per process (`work_dir`), so two tests
/// that run it at once delete each other's files: they take turns.
static ONE_AT_A_TIME: std::sync::Mutex<()> = std::sync::Mutex::new(());

struct Scratch(PathBuf);

impl Drop for Scratch {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

/// `install … --root R` leaves `R/bin/gunnar`, which answers `gunnar 9.9.9`.
const GOOD: &str = r#"#!/bin/sh
case "$1" in
metadata) echo '{"packages":[{"name":"gunnar-core","version":"1.0.0"}]}' ;;
build) echo "   Compiling gunnar-core v1.0.0" >&2 ;;
install)
  while [ $# -gt 0 ]; do [ "$1" = --root ] && root=$2; shift; done
  mkdir -p "$root/bin"
  printf '#!/bin/sh\necho "gunnar 9.9.9"\n' > "$root/bin/gunnar"
  chmod +x "$root/bin/gunnar" ;;
esac
"#;

/// Every step fails the way cargo fails: progress, a warning, then the errors.
const RED: &str = r#"#!/bin/sh
cat >&2 <<'ERR'
   Compiling gunnar v1.6.0
warning: unused import
error[E0425]: cannot find value `x` in this scope
  --> src/lib.rs:1:1
error: could not compile `gunnar` (lib) due to 1 previous error
ERR
exit 101
"#;

/// Every step "succeeds" and installs nothing.
const HOLLOW: &str = "#!/bin/sh\nexit 0\n";

fn stand_in(dir: &Path, name: &str, body: &str) -> String {
    let p = dir.join(name);
    std::fs::write(&p, body).unwrap();
    std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
    p.display().to_string()
}

fn base(name: &str) -> (PathBuf, Scratch) {
    let b = std::env::temp_dir().join(format!("gunnar-both-ways-{name}-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&b);
    std::fs::create_dir_all(&b).unwrap();
    (b.clone(), Scratch(b))
}

#[test]
fn crates_io_install_reports_what_the_installed_binary_said() {
    let _turn = ONE_AT_A_TIME.lock().unwrap_or_else(|p| p.into_inner());
    let (b, _s) = base("cio");
    match crates_io_install(&b, &stand_in(&b, "cargo-good", GOOD), false) {
        Verdict::Passed(said) => {
            assert_eq!(
                said,
                "cargo install gunnar --features client -> gunnar (gunnar 9.9.9); \
                 cargo install gunnar --features client --locked -> gunnar (gunnar 9.9.9)"
            );
        }
        Verdict::Failed(why) => panic!("{why}"),
    }
}

#[test]
fn a_failed_step_is_reported_by_the_compilers_lines_only() {
    let _turn = ONE_AT_A_TIME.lock().unwrap_or_else(|p| p.into_inner());
    let (b, _s) = base("red");
    match crates_io_install(&b, &stand_in(&b, "cargo-red", RED), false) {
        Verdict::Failed(why) => {
            assert!(
                why.starts_with("`cargo install gunnar --features client` failed:\n"),
                "{why}"
            );
            assert!(
                why.contains("error[E0425]") && why.contains("  --> src/lib.rs:1:1"),
                "{why}"
            );
            assert!(
                !why.contains("Compiling") && !why.contains("warning"),
                "{why}"
            );
        }
        Verdict::Passed(said) => panic!("a red step passed: {said}"),
    }
    match crates_io_install(&b, &stand_in(&b, "cargo-hollow", HOLLOW), false) {
        Verdict::Failed(why) => assert!(why.contains("left no working binary"), "{why}"),
        Verdict::Passed(said) => panic!("an install that left nothing passed: {said}"),
    }
}

fn git(dir: &Path, args: &[&str]) {
    let ok = Command::new("git")
        .current_dir(dir)
        .args(["-c", "user.name=t", "-c", "user.email=t@mock.invalid"])
        .args(args)
        .output()
        .unwrap()
        .status
        .success();
    assert!(ok, "git {args:?}");
}

/// A one-member repository whose committed lock is `lock`.
fn repo(at: &Path, lock: &str) {
    std::fs::create_dir_all(at).unwrap();
    std::fs::write(
        at.join("Cargo.toml"),
        "[workspace]\nmembers = [\"crates/gunnar-core\"]\n",
    )
    .unwrap();
    std::fs::write(at.join("Cargo.lock"), lock).unwrap();
    git(at, &["init", "-q"]);
    git(at, &["add", "-A"]);
    git(at, &["commit", "-q", "-m", "a lone repository"]);
}

const CLEAN_LOCK: &str = "version = 4\n\n[[package]]\nname = \"gunnar-core\"\nversion = \"1.0.0\"\n\n\
[[package]]\nname = \"serde\"\nversion = \"1.0.200\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n";

#[test]
fn a_lone_clone_builds_from_the_commit_alone_and_refuses_a_path_lock() {
    let _turn = ONE_AT_A_TIME.lock().unwrap_or_else(|p| p.into_inner());
    let (b, _s) = base("lone");
    let cargo = stand_in(&b, "cargo-good", GOOD);

    let clean = b.join("clean");
    repo(&clean, CLEAN_LOCK);
    let work = b.join("work-clean");
    let (v, server) = lone_clone(&clean, &cargo, false, &work);
    match v {
        Verdict::Passed(said) => assert!(
            said.contains("--path crates/gunnar --features client -> gunnar (gunnar 9.9.9)"),
            "{said}"
        ),
        Verdict::Failed(why) => panic!("{why}"),
    }
    let server = server.expect("the installed server");
    assert!(server.is_file(), "{}", server.display());
    assert!(
        lone_home(&work).is_dir(),
        "the plain lone clone builds with an empty CARGO_HOME"
    );
    assert!(
        work.join("lone/Cargo.lock").is_file(),
        "the clone is of the commit"
    );

    let pathy = b.join("pathy");
    repo(
        &pathy,
        &format!("{CLEAN_LOCK}\n[[package]]\nname = \"edda-admit\"\nversion = \"0.1.0\"\n"),
    );
    let (v, server) = lone_clone(&pathy, &cargo, false, &b.join("work-pathy"));
    match v {
        Verdict::Failed(why) => assert!(
            why.contains("names 1 crate(s) by path") && why.contains("(edda-admit 0.1.0)"),
            "{why}"
        ),
        Verdict::Passed(said) => panic!("a path lock passed: {said}"),
    }
    assert_eq!(server, None);

    let (v, _) = lone_clone(&b.join("not-a-repo"), &cargo, false, &b.join("work-none"));
    assert!(
        matches!(v, Verdict::Failed(ref why) if why.starts_with("git clone")),
        "{v:?}"
    );
}