use super::*;
use crate::cli_outbound::{Binary, Cli};
use crate::test_support::spawn_guard;
use std::cell::Cell;
use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::MutexGuard;
use tempfile::tempdir;
enum Mode {
AllOk,
Fail(Binary, &'static str),
Absent,
}
struct Fake {
mode: Mode,
calls: Cell<Vec<Vec<String>>>,
}
impl Fake {
fn new(mode: Mode) -> Self {
Self {
mode,
calls: Cell::new(Vec::new()),
}
}
fn verbs(&self) -> Vec<String> {
let calls = self.calls.take();
let verbs = calls.iter().filter_map(|a| a.first().cloned()).collect();
self.calls.set(calls);
verbs
}
}
impl ToolProbe for Fake {
fn status(&self, binary: Binary, args: &[&str]) -> io::Result<i32> {
let mut calls = self.calls.take();
calls.push(args.iter().map(|s| (*s).to_owned()).collect());
self.calls.set(calls);
match &self.mode {
Mode::AllOk => Ok(0),
Mode::Absent => Err(io::Error::other("boom")),
Mode::Fail(b, v) => Ok(i32::from(binary == *b && args.first() == Some(v)) * 2),
}
}
}
fn write_exe(dir: &Path, name: &str, body: &str) -> (PathBuf, MutexGuard<'static, ()>) {
let guard = spawn_guard();
let path = dir.join(name);
std::fs::write(&path, body).unwrap();
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&path, perms).unwrap();
(path, guard)
}
#[test]
fn remediation_and_tool_name_cover_every_tool() {
for b in [Binary::Bl, Binary::Lernie, Binary::Bz] {
assert!(remediation(b).contains("cargo install --locked"));
}
assert_eq!(remediation(Binary::Bl), BL_FIX);
assert_eq!(remediation(Binary::Lernie), LERNIE_FIX);
assert_eq!(remediation(Binary::Bz), BZ_FIX);
assert_eq!(tool_name(Binary::Bl), "bl");
assert_eq!(tool_name(Binary::Lernie), "lernie");
assert_eq!(tool_name(Binary::Bz), "bz");
}
#[test]
fn permits_only_ok_refuses_mismatch_and_missing() {
assert!(ToolState::Ok.permits());
assert!(
!ToolState::Mismatch {
verb: "prime".to_owned(),
remediation: "fix".to_owned(),
}
.permits()
);
assert!(
!ToolState::Missing {
remediation: "fix".to_owned(),
}
.permits()
);
}
#[test]
fn remediation_accessor_and_describe_read_each_state() {
assert_eq!(ToolState::Ok.remediation(), None);
assert_eq!(ToolState::Ok.describe(), "ok");
let mismatch = ToolState::Mismatch {
verb: "prime".to_owned(),
remediation: "reinstall lernie".to_owned(),
};
assert_eq!(mismatch.remediation(), Some("reinstall lernie"));
assert_eq!(
mismatch.describe(),
"missing verb `prime` — reinstall lernie"
);
let missing = ToolState::Missing {
remediation: "install bl".to_owned(),
};
assert_eq!(missing.remediation(), Some("install bl"));
assert_eq!(missing.describe(), "not found — install bl");
}
#[test]
fn classify_probes_the_normative_lernie_and_bl_verbs_by_help() {
let lernie = Fake::new(Mode::AllOk);
assert_eq!(classify(&lernie, Binary::Lernie), ToolState::Ok);
assert_eq!(
lernie.verbs(),
[
"prime", "new", "prompt", "message", "stop", "scan", "config"
]
);
assert!(
lernie
.calls
.take()
.iter()
.all(|a| a.get(1).map(String::as_str) == Some("--help"))
);
let bl = Fake::new(Mode::AllOk);
assert_eq!(classify(&bl, Binary::Bl), ToolState::Ok);
assert_eq!(
bl.verbs(),
[
"create", "claim", "unclaim", "close", "update", "list", "show", "conf"
]
);
}
#[test]
fn classify_probes_bz_by_version() {
let bz = Fake::new(Mode::AllOk);
assert_eq!(classify(&bz, Binary::Bz), ToolState::Ok);
assert_eq!(bz.verbs(), ["--version"]);
}
#[test]
fn classify_names_the_missing_verb_with_remediation() {
let f = Fake::new(Mode::Fail(Binary::Lernie, "scan"));
assert_eq!(
classify(&f, Binary::Lernie),
ToolState::Mismatch {
verb: "scan".to_owned(),
remediation: LERNIE_FIX.to_owned(),
}
);
let bz = Fake::new(Mode::Fail(Binary::Bz, "--version"));
assert_eq!(
classify(&bz, Binary::Bz),
ToolState::Mismatch {
verb: "--version".to_owned(),
remediation: BZ_FIX.to_owned(),
}
);
}
#[test]
fn classify_reports_an_unspawnable_binary_missing() {
let f = Fake::new(Mode::Absent);
assert_eq!(
classify(&f, Binary::Bl),
ToolState::Missing {
remediation: BL_FIX.to_owned(),
}
);
}
#[test]
fn probe_classifies_the_whole_toolchain_and_gates_per_tool() {
let all_ok = probe(&Fake::new(Mode::AllOk));
assert!(all_ok.permits(Binary::Bl));
assert!(all_ok.permits(Binary::Lernie));
assert!(all_ok.permits(Binary::Bz));
assert!(all_ok.require(Binary::Lernie).is_ok());
let stale = probe(&Fake::new(Mode::Fail(Binary::Lernie, "prime")));
assert!(!stale.permits(Binary::Lernie));
assert!(stale.permits(Binary::Bl));
let refusal = stale.require(Binary::Lernie).unwrap_err();
assert_eq!(refusal.tool, "lernie");
assert_eq!(
refusal.state,
ToolState::Mismatch {
verb: "prime".to_owned(),
remediation: LERNIE_FIX.to_owned(),
}
);
let msg = refusal.to_string();
assert!(msg.contains("lernie"));
assert!(msg.contains("missing verb `prime`"));
assert!(msg.contains("cargo install --locked lernie"));
}
#[test]
fn rows_lists_the_three_tools_in_order() {
let state = probe(&Fake::new(Mode::AllOk));
let rows = state.rows();
assert_eq!(rows.map(|(name, _)| name), ["bl", "lernie", "bz"]);
assert_eq!(rows[0].1, &ToolState::Ok);
}
#[test]
fn cli_probe_returns_the_exit_code_and_errors_on_a_missing_binary() {
let dir = tempdir().unwrap();
let (bl, _g) = write_exe(dir.path(), "bl", "#!/bin/sh\nexit 0\n");
let bz = dir.path().join("bz");
std::fs::write(&bz, "#!/bin/sh\nexit 2\n").unwrap();
let mut perms = std::fs::metadata(&bz).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&bz, perms).unwrap();
let probe = CliProbe::new(
Cli::new(bl),
Cli::new(dir.path().join("lernie-absent")),
Cli::new(bz),
);
assert_eq!(probe.status(Binary::Bl, &["list", "--help"]).unwrap(), 0);
assert_eq!(probe.status(Binary::Bz, &["--version"]).unwrap(), 2);
assert!(probe.status(Binary::Lernie, &["prime", "--help"]).is_err());
}