use super::*;
use crate::daemon::service::{systemd_state, launchd_state, Phase};
use std::cell::RefCell;
use std::rc::Rc;
const VERSION: &str = "1.2.3";
struct World {
calls: Vec<String>,
unit: (&'static str, &'static str),
unit_pid: Option<u32>,
restarts: u32,
last_exit: Option<i32>,
serving: Option<Serving>,
unit_version: String,
crashes: bool,
next_pid: u32,
}
impl World {
fn new() -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
calls: Vec::new(),
unit: ("inactive", "dead"),
unit_pid: None,
restarts: 0,
last_exit: None,
serving: None,
unit_version: VERSION.to_string(),
crashes: false,
next_pid: 500,
}))
}
fn start_unit(&mut self) {
if self.unit.0 == "active" {
return;
}
if self.crashes {
self.unit = ("activating", "auto-restart");
self.unit_pid = None;
self.restarts += 3;
self.last_exit = Some(1);
return;
}
if self.serving.is_some() {
self.unit = ("failed", "failed");
self.unit_pid = None;
self.last_exit = Some(crate::daemon::ANOTHER_SERVES_EXIT);
return;
}
self.next_pid += 1;
self.unit = ("active", "running");
self.unit_pid = Some(self.next_pid);
self.last_exit = None;
self.serving = Some(Serving { pid: self.next_pid, version: self.unit_version.clone(), port: Some(3000) });
}
fn stop_unit(&mut self) {
if let Some(pid) = self.unit_pid.take() {
if self.serving.as_ref().is_some_and(|daemon| daemon.pid == pid) {
self.serving = None;
}
}
self.unit = ("inactive", "dead");
}
}
struct Systemd(Rc<RefCell<World>>);
impl Control for Systemd {
fn systemctl(&mut self, arguments: &[&str]) -> Result<String, String> {
let mut world = self.0.borrow_mut();
let verb = arguments[0];
if verb != "show" {
world.calls.push(format!("systemctl {verb}"));
}
match verb {
"show" => Ok(format!(
"ActiveState={}\nSubState={}\nMainPID={}\nNRestarts={}\nExecMainStatus={}\nResult={}\n",
world.unit.0,
world.unit.1,
world.unit_pid.unwrap_or(0),
world.restarts,
world.last_exit.unwrap_or(0),
if world.last_exit.is_some() { "exit-code" } else { "success" }
)),
"start" => {
world.start_unit();
Ok(String::new())
}
"stop" => {
world.stop_unit();
Ok(String::new())
}
_ => Ok(String::new()),
}
}
fn launchctl(&mut self, _arguments: &[String]) -> Result<String, String> {
Err("this host has systemd".to_string())
}
fn launchd_loaded(&mut self, _target: &str) -> Result<bool, String> {
Err("this host has systemd".to_string())
}
}
struct OnTheWorkshop(Rc<RefCell<World>>);
impl Daemons for OnTheWorkshop {
fn serving(&mut self) -> Result<Option<Serving>, String> {
Ok(self.0.borrow().serving.clone())
}
fn stop(&mut self, daemon: &Serving) -> Result<(), String> {
let mut world = self.0.borrow_mut();
world.calls.push(format!("stop {}", daemon.pid));
if world.unit_pid == Some(daemon.pid) {
world.unit_pid = None;
world.unit = ("inactive", "dead");
}
world.serving = None;
Ok(())
}
}
fn bring_up(world: &Rc<RefCell<World>>, restart: bool) -> Result<Brought, String> {
bring_up_with(
&mut Systemd(world.clone()),
&mut OnTheWorkshop(world.clone()),
Supervisor::Systemd,
Path::new("/nowhere/wsb-daemon.service"),
VERSION,
restart,
|control| control.systemctl(&["daemon-reload"]).map(|_| ()),
Duration::from_secs(5),
Duration::from_millis(1),
)
}
#[test]
fn test20215_a_stray_daemon_gives_way_to_the_service() {
let world = World::new();
world.borrow_mut().serving = Some(Serving { pid: 281935, version: VERSION.to_string(), port: Some(3000) });
let brought = bring_up(&world, false).expect("the service's daemon comes up");
let world = world.borrow();
assert_eq!(Some(brought.serving.pid), world.unit_pid, "what serves is not the service's daemon");
assert_eq!(world.calls, ["stop 281935", "systemctl daemon-reload", "systemctl reset-failed", "systemctl start"]);
assert!(brought.replaced[0].contains("281935") && brought.replaced[0].contains("not keeping"), "{:?}", brought.replaced);
}
#[test]
fn test20216_the_services_daemon_is_kept_replaced_or_restarted() {
let world = World::new();
world.borrow_mut().start_unit();
let first = world.borrow().unit_pid;
let kept = bring_up(&world, false).expect("a current daemon is kept");
assert_eq!(Some(kept.serving.pid), first);
assert!(kept.replaced.is_empty(), "{:?}", kept.replaced);
let restarted = bring_up(&world, true).expect("a restart starts it again");
assert_ne!(Some(restarted.serving.pid), first, "a restart left the same daemon serving");
assert!(restarted.replaced.is_empty(), "a restart is not a replacement: {:?}", restarted.replaced);
world.borrow_mut().serving.as_mut().unwrap().version = "1.2.2".to_string();
let replaced = bring_up(&world, false).expect("an older daemon is replaced");
assert_eq!(replaced.serving.version, VERSION);
assert!(replaced.replaced[0].contains("1.2.2"), "{:?}", replaced.replaced);
world.borrow_mut().unit_version = "0.9.0".to_string();
let refused = bring_up(&world, true).expect_err("the service starts another build");
assert!(refused.contains("0.9.0") && refused.contains("wsb daemon install"), "{refused}");
}
#[test]
fn test20217_a_failing_service_is_never_reported_as_up() {
let world = World::new();
world.borrow_mut().crashes = true;
let refused = bring_up(&world, false).expect_err("a crash loop is not a daemon");
assert!(refused.starts_with("the service is failing") && refused.contains("status 1"), "{refused}");
assert!(!refused.contains("has not answered"), "the failure was waited out, not seen: {refused}");
}
#[test]
fn test20218_a_crash_loop_reads_as_failing() {
let looping = systemd_state(
"ActiveState=activating\nSubState=auto-restart\nMainPID=0\nNRestarts=127\nExecMainStatus=1\nResult=exit-code\n",
);
assert_eq!(looping.phase, Phase::Restarting);
assert!(looping.failing() && looping.keeping().is_none());
assert_eq!(looping.failure(), "failing: started again 127 time(s); its daemon last exited with status 1");
let running = systemd_state("ActiveState=active\nSubState=running\nMainPID=4242\nNRestarts=0\nExecMainStatus=0\nResult=success\n");
assert_eq!((running.phase, running.keeping(), running.failing()), (Phase::Running, Some(4242), false));
let starting = systemd_state("ActiveState=activating\nSubState=start\nMainPID=4243\nNRestarts=0\nExecMainStatus=0\nResult=success\n");
assert_eq!((starting.phase, starting.keeping()), (Phase::Starting, None));
let refused = systemd_state("ActiveState=failed\nSubState=failed\nMainPID=0\nNRestarts=0\nExecMainStatus=78\nResult=exit-code\n");
assert!(refused.failing());
assert!(refused.failure().contains("another daemon was serving"), "{}", refused.failure());
let stopped = systemd_state("ActiveState=inactive\nSubState=dead\nMainPID=0\nNRestarts=0\nExecMainStatus=0\nResult=success\n");
assert_eq!((stopped.phase, stopped.failing()), (Phase::Stopped, false));
let job = "gui/501/com.machinefabric.wsb-daemon = {\n\tactive count = 1\n\tstate = running\n\truns = 1\n\tpid = 812\n\tlast exit code = (never exited)\n}\n";
let up = launchd_state(job);
assert_eq!((up.phase, up.keeping()), (Phase::Running, Some(812)));
let throttled = launchd_state("gui/501/x = {\n\tstate = not running\n\truns = 9\n\tlast exit code = 1\n}\n");
assert_eq!((throttled.phase, throttled.restarts, throttled.last_exit), (Phase::Restarting, Some(8), Some(1)));
let idle = launchd_state("gui/501/x = {\n\tstate = not running\n\truns = 1\n\tlast exit code = 0\n}\n");
assert_eq!(idle.phase, Phase::Stopped);
}
#[test]
fn test20219_a_refusal_is_never_restarted_into_a_loop() {
let record = service::Record {
executable: PathBuf::from("/opt/wsb/wsb-daemon"),
arguments: vec!["--worktree".to_string(), "/w".to_string()],
worktree_root: PathBuf::from("/w"),
log: PathBuf::from("/m/daemon.log"),
path: "/usr/bin".to_string(),
host_dir: PathBuf::from("/m"),
prj: None,
autostart: true,
};
let unit = service::render(Supervisor::Systemd, &record);
assert!(unit.contains(&format!("RestartPreventExitStatus={}\n", crate::daemon::ANOTHER_SERVES_EXIT)), "{unit}");
assert!(unit.contains(&format!("Environment={}=systemd\n", crate::daemon::SUPERVISOR_ENV)), "{unit}");
let plist = service::render(Supervisor::Launchd, &record);
assert!(
plist.contains(&format!("<key>{}</key>\n <string>launchd</string>", crate::daemon::SUPERVISOR_ENV)),
"{plist}"
);
}
#[test]
fn test20239_a_service_with_an_older_record_is_still_stopped() {
let dir = tempfile::tempdir().unwrap();
let older = serde_json::json!({ "worktree_root": "/w", crate::migrate::OLD_HOST_DIR_FIELD: "/h" });
std::fs::write(service::record_path(dir.path()), older.to_string()).unwrap();
let world = World::new();
world.borrow_mut().start_unit();
let kept = world.borrow().unit_pid.expect("the unit's daemon");
let stopped = stop_in(dir.path(), &mut Systemd(world.clone()), &mut OnTheWorkshop(world.clone()), || Ok(Supervisor::Systemd))
.expect("the service stops");
assert!(world.borrow().calls.iter().any(|call| call == "systemctl stop"), "{:?}", world.borrow().calls);
assert_eq!(world.borrow().unit, ("inactive", "dead"));
assert!(stopped.iter().any(|line| line.contains(&kept.to_string())), "{stopped:?}");
let read = installed(dir.path()).map(|_| ()).expect_err("an older record is not read as this wsb's");
assert!(read.contains("wsb migrate"), "{read}");
}