use std::path::Path;
use procpilot::testing::{MockRunner, nonzero, ok_str, spawn_error};
use procpilot::{Cmd, RunError, Runner};
fn current_branch(runner: &dyn Runner, repo: &Path) -> Result<String, RunError> {
let cmd = Cmd::new("git")
.args(["branch", "--show-current"])
.in_dir(repo);
let out = runner.run(cmd)?;
Ok(out.stdout_lossy().trim().to_string())
}
#[test]
fn mock_returns_canned_stdout() {
let mock = MockRunner::new().expect("git branch --show-current", ok_str("main\n"));
let branch = current_branch(&mock, Path::new("/tmp")).expect("ok");
assert_eq!(branch, "main");
mock.verify().expect("all expectations met");
}
#[test]
fn mock_returns_canned_error() {
let mock = MockRunner::new()
.expect("git fetch", nonzero(128, "fatal: unable to access remote"));
let err = (&mock as &dyn Runner)
.run(Cmd::new("git").arg("fetch"))
.expect_err("mock returned Err");
assert!(err.is_non_zero_exit());
assert_eq!(err.exit_status().and_then(|s| s.code()), Some(128));
assert_eq!(err.stderr(), Some("fatal: unable to access remote"));
assert_eq!(err.command().to_string(), "git fetch");
}
#[test]
fn predicate_matcher_inspects_cwd_via_to_rightmost_command() {
let mock = MockRunner::new().expect_when(
|cmd| {
let std_cmd = cmd.to_rightmost_command();
std_cmd.get_program() == "git"
&& std_cmd.get_current_dir() == Some(Path::new("/special-repo"))
},
ok_str("on the special repo"),
);
let out = mock
.run(Cmd::new("git").arg("status").in_dir("/special-repo"))
.expect("matched by cwd");
assert_eq!(out.stdout_lossy(), "on the special repo");
}
#[test]
fn verify_reports_unmet_expectations() {
let mock = MockRunner::new()
.expect("git status", ok_str(""))
.expect("git log", ok_str(""));
let _ = mock.run(Cmd::new("git").arg("status"));
let report = mock.verify().expect_err("one expectation never matched");
assert!(report.contains("git log"), "got: {report}");
}
#[test]
fn first_match_wins_among_overlapping_expectations() {
let mock = MockRunner::new()
.expect_when(|_| true, ok_str("first"))
.expect_when(|_| true, ok_str("second"));
let a = mock.run(Cmd::new("anything")).unwrap();
let b = mock.run(Cmd::new("anything")).unwrap();
assert_eq!(a.stdout_lossy(), "first");
assert_eq!(b.stdout_lossy(), "second");
mock.verify().unwrap();
}
#[test]
#[should_panic(expected = "no matching expectation")]
fn no_match_panics_by_default() {
let mock = MockRunner::new().expect("git status", ok_str(""));
let _ = mock.run(Cmd::new("git").arg("log"));
}
#[test]
fn no_match_returns_spawn_error_when_configured() {
let mock = MockRunner::new()
.error_on_no_match()
.expect("git status", ok_str(""));
let err = mock
.run(Cmd::new("git").arg("log"))
.expect_err("no match returns Err");
assert!(err.is_spawn_failure());
assert!(err.command().to_string().contains("git log"));
}
#[test]
fn spawn_error_helper_constructs_typed_error() {
let mock = MockRunner::new().expect("missing-binary", spawn_error("not on PATH"));
let err = mock
.run(Cmd::new("missing-binary"))
.expect_err("err");
assert!(err.is_spawn_failure());
}
#[test]
fn panic_on_no_match_does_not_poison_mutex() {
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
let mock = Arc::new(MockRunner::new().expect("matches", ok_str("ok")));
let m = Arc::clone(&mock);
let _ = catch_unwind(AssertUnwindSafe(|| {
let _ = m.run(Cmd::new("does-not-match"));
}));
let result = catch_unwind(AssertUnwindSafe(|| mock.run(Cmd::new("matches"))));
assert!(
result.is_ok(),
"run() after a caught no-match panic should not re-panic due to a poisoned mutex"
);
let out = result.expect("no re-panic").expect("canned ok result");
assert_eq!(out.stdout_lossy(), "ok");
}
#[test]
fn expect_repeated_matches_up_to_n_times() {
use procpilot::testing::ok_str;
let mock = MockRunner::new().expect_repeated("git pull", 3, || ok_str("Already up to date."));
for _ in 0..3 {
let out = mock.run(Cmd::new("git").arg("pull")).expect("ok");
assert_eq!(out.stdout_lossy(), "Already up to date.");
}
let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
mock.run(Cmd::new("git").arg("pull"))
}));
assert!(err.is_err(), "4th call should panic on no-match");
}
#[test]
fn expect_always_matches_unlimited_times() {
use procpilot::testing::ok_str;
let mock = MockRunner::new().expect_always("git config core.hooksPath", || {
ok_str(".husky\n")
});
for _ in 0..10 {
let out = mock
.run(Cmd::new("git").args(["config", "core.hooksPath"]))
.expect("ok");
assert_eq!(out.stdout_lossy(), ".husky\n");
}
mock.verify().expect("always-matching expectation counts as met after first call");
}
#[test]
fn expect_repeated_with_varying_factory_output() {
use procpilot::testing::ok_str;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let counter = Arc::new(AtomicUsize::new(0));
let c = counter.clone();
let mock = MockRunner::new().expect_repeated("git log", 3, move || {
let n = c.fetch_add(1, Ordering::SeqCst);
ok_str(format!("commit {n}"))
});
let a = mock.run(Cmd::new("git").arg("log")).unwrap();
let b = mock.run(Cmd::new("git").arg("log")).unwrap();
let c2 = mock.run(Cmd::new("git").arg("log")).unwrap();
assert_eq!(a.stdout_lossy(), "commit 0");
assert_eq!(b.stdout_lossy(), "commit 1");
assert_eq!(c2.stdout_lossy(), "commit 2");
}
#[test]
fn mock_result_resolve_attaches_given_command() {
use procpilot::CmdDisplay;
use procpilot::testing::{MockResult, nonzero};
let cmd = Cmd::new("pretend").arg("arg");
let display: CmdDisplay = cmd.display();
let result: MockResult = nonzero(2, "something failed");
let err = result
.resolve(&display)
.expect_err("nonzero resolves to Err");
assert!(err.is_non_zero_exit());
assert_eq!(err.command().to_string(), "pretend arg");
}
#[test]
fn mock_result_resolve_covers_every_variant() {
use procpilot::testing::{MockResult, nonzero, ok, ok_str, spawn_error, timeout};
use std::time::Duration;
let cmd = Cmd::new("x").display();
let out = ok(b"bytes".to_vec()).resolve(&cmd).expect("Ok");
assert_eq!(out.stdout, b"bytes");
let out = ok_str("hello").resolve(&cmd).expect("Ok");
assert_eq!(out.stdout_lossy(), "hello");
let err = nonzero(42, "nope").resolve(&cmd).expect_err("Err");
assert!(err.is_non_zero_exit());
assert_eq!(err.exit_status().and_then(|s| s.code()), Some(42));
let err = spawn_error("boom").resolve(&cmd).expect_err("Err");
assert!(err.is_spawn_failure());
let err = timeout(Duration::from_secs(1), "hung").resolve(&cmd).expect_err("Err");
assert!(err.is_timeout());
let _compile_check: fn(MockResult, &procpilot::CmdDisplay) = |r, d| {
let _ = r.resolve(d);
};
}
#[test]
fn pipeline_display_matches_for_mock() {
let mock = MockRunner::new().expect("git log | head -5", ok_str("commit ..."));
let out = mock
.run(Cmd::new("git").arg("log").pipe(Cmd::new("head").arg("-5")))
.expect("ok");
assert_eq!(out.stdout_lossy(), "commit ...");
}