use super::*;
use crate::commands::guided_liveness::DaemonLockUnverified;
use std::cell::RefCell;
use std::collections::VecDeque;
const DEAD_PID: u32 = 4_194_303;
fn print_body(state: &str) -> String {
format!(
"gui/501/com.trusty.mpm = {{\n\tactive count = 0\n\tpath = /x.plist\n\
\tstate = {state}\n\n\tendpoints = {{\n\t\tstate = active\n\t}}\n}}\n"
)
}
struct FakeLaunchctl {
prints: RefCell<VecDeque<Option<String>>>,
kickstart_ok: bool,
calls: RefCell<Vec<String>>,
}
impl FakeLaunchctl {
fn new(prints: Vec<Option<&str>>, kickstart_ok: bool) -> Self {
Self {
prints: RefCell::new(prints.into_iter().map(|p| p.map(print_body)).collect()),
kickstart_ok,
calls: RefCell::new(Vec::new()),
}
}
fn run(&self, args: &[String]) -> Option<LaunchctlReply> {
self.calls.borrow_mut().push(args.join(" "));
let reply = |success: bool, stdout: String| Some(LaunchctlReply { success, stdout });
match args.first().map(String::as_str) {
Some("print") => match self.prints.borrow_mut().pop_front().flatten() {
Some(body) => reply(true, body),
None => reply(false, String::new()),
},
Some("kickstart") => reply(self.kickstart_ok, String::new()),
_ => reply(true, String::new()),
}
}
fn calls(&self) -> Vec<String> {
self.calls.borrow().clone()
}
}
fn target() -> LaunchdTarget {
LaunchdTarget {
domain: "gui/501".to_string(),
label: "com.trusty.mpm".to_string(),
plist: std::path::PathBuf::from("/fake/com.trusty.mpm.plist"),
}
}
fn write_lock(path: &std::path::Path, pid: u32) {
std::fs::write(
path,
format!(
"product = \"trusty-mpm\"\npid = {pid}\naddr = \"http://127.0.0.1:47011\"\n\
started_at = \"\"\n"
),
)
.expect("write lock");
}
fn prepare(
fake: &FakeLaunchctl,
launchd: bool,
lock: &std::path::Path,
identity: PidIdentity,
) -> AutostartPlan {
let run = |args: &[String]| fake.run(args);
let identify = move |_: u32| identity;
let t = target();
prepare_autostart(&run, launchd.then_some(&t), lock, &identify)
}
#[test]
fn parse_state_running() {
assert_eq!(
parse_launchd_state(&print_body("running")),
Some(LaunchdJob::Running)
);
}
#[test]
fn parse_state_not_running() {
assert_eq!(
parse_launchd_state(&print_body("not running")),
Some(LaunchdJob::Stopped)
);
assert_eq!(
parse_launchd_state(&print_body("spawn scheduled")),
Some(LaunchdJob::Stopped)
);
}
#[test]
fn parse_state_absent_is_none() {
assert_eq!(parse_launchd_state("gui/501/x = {\n\tpid = 1\n}\n"), None);
}
#[test]
fn prepare_awaits_a_running_launchd_job() {
let tmp = tempfile::tempdir().expect("tempdir");
let fake = FakeLaunchctl::new(vec![Some("running")], true);
let plan = prepare(
&fake,
true,
&tmp.path().join("daemon.lock"),
PidIdentity::Unknown,
);
assert!(matches!(plan, AutostartPlan::AwaitExisting(ref e) if e.contains("running")));
assert_eq!(fake.calls(), vec!["print gui/501/com.trusty.mpm"]);
}
#[test]
fn prepare_kickstarts_a_loaded_but_stopped_job() {
let tmp = tempfile::tempdir().expect("tempdir");
let fake = FakeLaunchctl::new(vec![Some("not running")], true);
let plan = prepare(
&fake,
true,
&tmp.path().join("daemon.lock"),
PidIdentity::Unknown,
);
assert_eq!(plan, AutostartPlan::AwaitLaunchd);
assert_eq!(
fake.calls(),
vec![
"print gui/501/com.trusty.mpm",
"kickstart gui/501/com.trusty.mpm"
]
);
}
#[test]
fn prepare_bootstraps_then_kickstarts_an_unloaded_job() {
let tmp = tempfile::tempdir().expect("tempdir");
let fake = FakeLaunchctl::new(vec![None, Some("not running")], true);
let plan = prepare(
&fake,
true,
&tmp.path().join("daemon.lock"),
PidIdentity::Unknown,
);
assert_eq!(plan, AutostartPlan::AwaitLaunchd);
assert_eq!(
fake.calls(),
vec![
"print gui/501/com.trusty.mpm",
"bootstrap gui/501 /fake/com.trusty.mpm.plist",
"print gui/501/com.trusty.mpm",
"kickstart gui/501/com.trusty.mpm"
]
);
}
#[test]
fn prepare_spawns_when_launchd_cannot_start_the_job() {
let tmp = tempfile::tempdir().expect("tempdir");
let fake = FakeLaunchctl::new(vec![Some("not running")], false);
assert_eq!(
prepare(
&fake,
true,
&tmp.path().join("daemon.lock"),
PidIdentity::Unknown
),
AutostartPlan::Spawn
);
}
#[test]
fn prepare_awaits_a_live_daemon_lock_pid() {
let tmp = tempfile::tempdir().expect("tempdir");
let lock = tmp.path().join("daemon.lock");
let me = std::process::id();
write_lock(&lock, me);
let fake = FakeLaunchctl::new(vec![], true);
let plan = prepare(&fake, false, &lock, PidIdentity::Daemon);
assert!(
matches!(plan, AutostartPlan::AwaitExisting(ref e) if e.contains("tm start")),
"a live daemon lock pid is awaited and names its recovery: {plan:?}"
);
assert!(lock.exists(), "a live daemon's lock must never be deleted");
}
#[test]
fn prepare_never_deletes_an_unverified_lock() {
for identity in [PidIdentity::NotDaemon, PidIdentity::Unknown] {
let tmp = tempfile::tempdir().expect("tempdir");
let lock = tmp.path().join("daemon.lock");
write_lock(&lock, std::process::id());
let fake = FakeLaunchctl::new(vec![Some("not running")], true);
let plan = prepare(&fake, true, &lock, identity);
assert!(
matches!(plan, AutostartPlan::Blocked(ref e) if e.contains("remove ~/.trusty-mpm/daemon.lock")),
"{identity:?}: an unverified live lock pid must block: {plan:?}"
);
assert!(
lock.exists(),
"{identity:?}: the lock must never be deleted"
);
assert!(
!fake.calls().iter().any(|c| c.starts_with("kickstart")),
"{identity:?}: nothing is started while the lock is unverified"
);
let err = blocked_error(&match plan {
AutostartPlan::Blocked(e) => e,
_ => unreachable!(),
});
assert!(err.downcast_ref::<DaemonLockUnverified>().is_some());
}
}
#[test]
fn lock_verdict_follows_the_removal_rule() {
let tmp = tempfile::tempdir().expect("tempdir");
let lock = tmp.path().join("daemon.lock");
let me = std::process::id();
assert_eq!(
lock_verdict(&lock, &|_| PidIdentity::Daemon),
LockVerdict::Absent
);
write_lock(&lock, DEAD_PID);
assert_eq!(
lock_verdict(&lock, &|_| PidIdentity::Daemon),
LockVerdict::Stale(DEAD_PID)
);
write_lock(&lock, me);
assert_eq!(
lock_verdict(&lock, &|_| PidIdentity::Daemon),
LockVerdict::LiveDaemon(me)
);
assert_eq!(
lock_verdict(&lock, &|_| PidIdentity::Unknown),
LockVerdict::Unverified(me, PidIdentity::Unknown)
);
assert!(lock.exists(), "reading a verdict never deletes");
}
#[test]
fn prepare_clears_dead_pid_lock_and_spawns() {
let tmp = tempfile::tempdir().expect("tempdir");
let lock = tmp.path().join("daemon.lock");
write_lock(&lock, DEAD_PID);
let fake = FakeLaunchctl::new(vec![], true);
assert_eq!(
prepare(&fake, false, &lock, PidIdentity::Unknown),
AutostartPlan::Spawn
);
assert!(!lock.exists(), "a dead-pid lock is stale and is removed");
}
#[test]
fn timeout_evidence_relaunched_job_still_running_is_slow() {
let t = target();
let running = FakeLaunchctl::new(vec![Some("running")], true);
let run = |args: &[String]| running.run(args);
let evidence = timeout_evidence(&AutostartPlan::AwaitLaunchd, &run, Some(&t), None);
assert!(evidence.is_some_and(|e| e.contains("running")));
let stopped = FakeLaunchctl::new(vec![Some("not running")], true);
let run = |args: &[String]| stopped.run(args);
assert_eq!(
timeout_evidence(&AutostartPlan::AwaitLaunchd, &run, Some(&t), None),
None,
"a job that stopped again is down"
);
}
#[test]
fn timeout_evidence_spawn_depends_on_the_child() {
let fake = FakeLaunchctl::new(vec![], true);
let run = |args: &[String]| fake.run(args);
assert_eq!(
timeout_evidence(&AutostartPlan::Spawn, &run, None, Some(42)),
Some("spawned pid 42 still starting".to_string())
);
assert_eq!(
timeout_evidence(&AutostartPlan::Spawn, &run, None, None),
None
);
assert!(fake.calls().is_empty(), "a spawn never asks launchd");
}
#[test]
fn spawned_child_still_running_reports_its_pid() {
let mut child = std::process::Command::new("sleep")
.arg("5")
.spawn()
.expect("spawn sleep");
let pid = child.id();
assert_eq!(spawned_still_running(&mut child), Some(pid));
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn spawned_child_that_exited_reports_none() {
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn true");
let _ = child.wait();
assert_eq!(spawned_still_running(&mut child), None);
}
#[test]
fn timeout_error_with_evidence_is_alive_unresponsive() {
let err = autostart_timeout_error(Some("spawned pid 42 still starting".to_string()));
let alive = err
.downcast_ref::<DaemonAliveUnresponsive>()
.expect("evidence must yield DaemonAliveUnresponsive");
assert_eq!(alive.evidence, "spawned pid 42 still starting");
}
#[test]
fn timeout_error_without_evidence_is_plain() {
let err = autostart_timeout_error(None);
assert!(err.downcast_ref::<DaemonAliveUnresponsive>().is_none());
assert!(err.to_string().contains("did not become healthy"));
}