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