use super::*;
fn spawn() -> Spawn {
Spawn {
listen: crate::daemon::LOOPBACK,
http_port: crate::daemon::Port::Exact(3123),
parent_pid: None,
debug: false,
peer_port: 7717,
}
}
fn record_for(workspace: &str) -> Record {
let root = PathBuf::from(workspace);
Record {
executable: PathBuf::from("/home/op/.cargo/bin/wsb-daemon"),
arguments: crate::daemon::daemon_arguments(&root, &spawn())
.expect("an exact port resolves without probing"),
workspace_root: root,
log: PathBuf::from("/home/op/.wsb/daemon.log"),
path: "/home/op/.cargo/bin:/usr/bin".to_string(),
machine_dir: PathBuf::from("/home/op/ws/prj/.state/hosts/h0123456789ab"),
prj: Some(PathBuf::from("/home/op/ws/prj")),
autostart: false,
}
}
#[test]
fn test11208_the_unit_starts_the_daemon_with_the_arguments_the_daemon_takes() {
let record = record_for("/home/op/ws/machinefabric");
let unit = render(Supervisor::Systemd, &record);
let exec = unit
.lines()
.find_map(|line| line.strip_prefix("ExecStart="))
.expect("the unit must say what to start");
let expected = crate::daemon::daemon_arguments(
&PathBuf::from("/home/op/ws/machinefabric"),
&spawn(),
)
.expect("an exact port resolves without probing");
assert_eq!(
exec,
format!("/home/op/.cargo/bin/wsb-daemon {}", expected.join(" ")),
"the unit's ExecStart is not the daemon's own argument vector"
);
let plist = render(Supervisor::Launchd, &record);
for argument in &expected {
assert!(
plist.contains(&format!("<string>{argument}</string>")),
"the plist does not pass {argument} to the daemon"
);
}
}
#[test]
fn test11209_a_path_with_a_space_is_quoted_for_the_supervisor() {
let record = record_for("/home/op/My Work/machinefabric");
let unit = render(Supervisor::Systemd, &record);
let exec = unit
.lines()
.find_map(|line| line.strip_prefix("ExecStart="))
.expect("the unit must say what to start");
assert!(
exec.contains("\"/home/op/My Work/machinefabric\""),
"the workspace path is not quoted: {exec}"
);
assert!(
exec.contains(" --workspace "),
"flags that need no quoting were quoted anyway: {exec}"
);
let plist = render(Supervisor::Launchd, &record);
assert!(
plist.contains("<string>/home/op/My Work/machinefabric</string>"),
"the plist mangled a path that needs nothing done to it"
);
}
#[test]
fn test11210_autostart_is_the_only_difference_between_enabled_and_disabled() {
let mut off = record_for("/home/op/ws/machinefabric");
off.autostart = false;
let mut on = off.clone();
on.autostart = true;
let plist_off = render(Supervisor::Launchd, &off);
let plist_on = render(Supervisor::Launchd, &on);
assert!(
plist_off.contains("<key>RunAtLoad</key>\n <false/>"),
"a disabled agent still says it starts at load"
);
assert!(
plist_on.contains("<key>RunAtLoad</key>\n <true/>"),
"an enabled agent does not say it starts at load"
);
assert_eq!(
plist_off.replace("<key>RunAtLoad</key>\n <false/>", "<key>RunAtLoad</key>\n <TOGGLE/>"),
plist_on.replace("<key>RunAtLoad</key>\n <true/>", "<key>RunAtLoad</key>\n <TOGGLE/>"),
"enabling changed something other than whether it starts at login"
);
assert_eq!(
render(Supervisor::Systemd, &off),
render(Supervisor::Systemd, &on),
"the systemd unit encodes autostart, which belongs to the manager"
);
}
#[test]
fn test11211_the_plist_escapes_what_xml_cannot_carry() {
let mut record = record_for("/home/op/R&D/machinefabric");
record.path = "/opt/A&B/bin:/usr/bin".to_string();
let plist = render(Supervisor::Launchd, &record);
assert!(
plist.contains("<string>/home/op/R&D/machinefabric</string>"),
"an ampersand in the workspace path reached the plist raw"
);
assert!(
plist.contains("<string>/opt/A&B/bin:/usr/bin</string>"),
"an ampersand in PATH reached the plist raw"
);
assert!(
!plist.contains("R&D"),
"the plist is not well-formed XML"
);
}
#[test]
fn test11212_a_definition_that_is_not_the_render_reads_as_drift() {
let record = record_for("/home/op/ws/machinefabric");
let current = render(Supervisor::Systemd, &record);
assert!(
!drifted(Supervisor::Systemd, &record, Some(¤t)),
"an untouched definition reads as drifted"
);
let moved = current.replace("/home/op/.cargo/bin", "/usr/local/bin");
assert!(
drifted(Supervisor::Systemd, &record, Some(&moved)),
"a unit naming an executable that is not the recorded one reads as current"
);
assert!(
!drifted(Supervisor::Systemd, &record, None),
"a missing definition was reported as drift rather than as absent"
);
}
#[test]
fn test11213_a_service_writes_to_the_log_the_daemon_command_reads() {
let record = record_for("/home/op/ws/machinefabric");
let log = record.log.display().to_string();
let unit = render(Supervisor::Systemd, &record);
assert!(
unit.contains(&format!("StandardOutput=append:{log}")),
"the unit does not append the daemon's output to its log"
);
assert!(
unit.contains(&format!("StandardError=append:{log}")),
"the unit does not append the daemon's errors to its log"
);
let plist = render(Supervisor::Launchd, &record);
assert!(
plist.contains("<key>StandardOutPath</key>"),
"the agent does not say where its output goes"
);
assert_eq!(
plist.matches(&format!("<string>{log}</string>")).count(),
2,
"the agent does not send both streams to the daemon's log"
);
}
#[test]
fn test11214_the_definition_carries_the_path_the_daemon_needs() {
let record = record_for("/home/op/ws/machinefabric");
assert!(
render(Supervisor::Systemd, &record)
.contains(&format!("Environment=PATH={}", record.path)),
"the unit does not give the daemon a PATH"
);
let plist = render(Supervisor::Launchd, &record);
assert!(
plist.contains("<key>PATH</key>") && plist.contains(&format!("<string>{}</string>", record.path)),
"the agent does not give the daemon a PATH"
);
}
#[test]
fn test11215_the_supervisor_restarts_a_failure_and_not_a_deliberate_stop() {
let record = record_for("/home/op/ws/machinefabric");
let unit = render(Supervisor::Systemd, &record);
assert!(
unit.contains("Restart=on-failure"),
"the unit does not restart the daemon when it fails"
);
assert!(
!unit.contains("Restart=always"),
"the unit would restart a daemon that was deliberately stopped"
);
let plist = render(Supervisor::Launchd, &record);
assert!(
plist.contains("<key>SuccessfulExit</key>\n <false/>"),
"the agent would restart a daemon that exited cleanly"
);
}
#[test]
fn test20053_the_service_keeps_its_state_where_the_install_found_it() {
let record = record_for("/home/op/ws/prj/machinefabric/main-worktree");
let unit = render(Supervisor::Systemd, &record);
assert!(
unit.contains("Environment=WSB_MACHINE_DIR=/home/op/ws/prj/.state/hosts/h0123456789ab\n"),
"the unit does not say where the daemon's state is: {unit}"
);
assert!(unit.contains("Environment=PRJ=/home/op/ws/prj\n"), "the unit does not carry PRJ: {unit}");
let plist = render(Supervisor::Launchd, &record);
assert!(
plist.contains("<key>WSB_MACHINE_DIR</key>\n <string>/home/op/ws/prj/.state/hosts/h0123456789ab</string>"),
"the agent does not say where the daemon's state is: {plist}"
);
assert!(
plist.contains("<key>PRJ</key>\n <string>/home/op/ws/prj</string>"),
"the agent does not carry PRJ: {plist}"
);
let without = Record { prj: None, ..record };
assert!(!render(Supervisor::Systemd, &without).contains("PRJ="));
assert!(!render(Supervisor::Launchd, &without).contains("<key>PRJ</key>"));
}
#[test]
fn test20054_a_reinstall_keeps_autostart_over_an_older_record() {
let dir = tempfile::tempdir().expect("a directory");
std::fs::write(
record_path(dir.path()),
r#"{"executable":"/x/wsb-daemon","arguments":[],"workspace_root":"/w","log":"/l","path":"/usr/bin","autostart":true}"#,
)
.expect("an older record");
assert!(record(dir.path()).is_err(), "an older record read as this wsb's");
assert!(installed_autostart(dir.path()).expect("its switch reads"), "a reinstall would turn autostart off");
assert!(!installed_autostart(&dir.path().join("none")).expect("no record"), "no service is not an enabled one");
}
struct StandIn {
calls: Vec<String>,
loaded: bool,
lingers: u32,
leaving: Option<u32>,
}
impl StandIn {
fn running(lingers: u32) -> Self {
Self { calls: Vec::new(), loaded: true, lingers, leaving: None }
}
fn verbs(&self) -> Vec<&str> {
self.calls.iter().map(String::as_str).collect()
}
}
impl Control for StandIn {
fn systemctl(&mut self, arguments: &[&str]) -> Result<String, String> {
self.calls.push(format!("systemctl {}", arguments.join(" ")));
Ok(String::new())
}
fn launchctl(&mut self, arguments: &[String]) -> Result<String, String> {
let verb = arguments[0].as_str();
self.calls.push(verb.to_string());
match verb {
"bootout" if self.loaded => {
self.leaving = Some(self.lingers);
Ok(String::new())
}
"bootout" => Err("Boot-out failed: 3: No such process".to_string()),
"bootstrap" if self.loaded => Err("Bootstrap failed: 5: Input/output error".to_string()),
"bootstrap" => {
self.loaded = true;
Ok(String::new())
}
"kickstart" if self.loaded => Ok(String::new()),
other => Err(format!("the stand-in was asked to {other} with the job in no state for it")),
}
}
fn launchd_loaded(&mut self, _target: &str) -> Result<bool, String> {
self.calls.push("print".to_string());
match self.leaving {
Some(0) => {
self.leaving = None;
self.loaded = false;
}
Some(left) => self.leaving = Some(left - 1),
None => {}
}
Ok(self.loaded)
}
}
#[test]
fn test20056_an_install_over_a_running_launchd_job_waits_for_it_to_go() {
let dir = tempfile::tempdir().expect("a directory");
let definition = dir.path().join(format!("{LABEL}.plist"));
let record = Record { autostart: true, ..record_for("/home/op/ws/prj/machinefabric/main-worktree") };
let mut launchd = StandIn::running(3);
apply_with(&mut launchd, Supervisor::Launchd, &record, &definition, Duration::from_secs(5))
.expect("the new definition is loaded once the old job has gone");
assert_eq!(
launchd.verbs(),
["bootout", "print", "print", "print", "print", "bootstrap", "kickstart"],
"the job was not bootstrapped once, after launchd stopped listing the old one"
);
assert_eq!(
std::fs::read_to_string(&definition).expect("the plist"),
render(Supervisor::Launchd, &record)
);
}
#[test]
fn test20057_a_launchd_job_that_does_not_go_is_named() {
let dir = tempfile::tempdir().expect("a directory");
let definition = dir.path().join(format!("{LABEL}.plist"));
let record = Record { autostart: true, ..record_for("/home/op/ws/prj/machinefabric/main-worktree") };
let mut launchd = StandIn::running(u32::MAX);
let refused = apply_with(&mut launchd, Supervisor::Launchd, &record, &definition, Duration::from_millis(300))
.expect_err("a job launchd keeps holding cannot be replaced");
assert!(refused.contains(LABEL), "the refusal does not name the job: {refused}");
assert!(refused.contains("launchctl print"), "the refusal does not say how to look: {refused}");
assert!(
!launchd.verbs().contains(&"bootstrap"),
"a bootstrap was sent over a job launchd still held: {:?}",
launchd.verbs()
);
}
#[test]
fn test20058_a_running_systemd_unit_is_restarted_onto_a_changed_definition() {
let dir = tempfile::tempdir().expect("a directory");
let definition = dir.path().join(UNIT);
let enabled = Record { autostart: true, ..record_for("/home/op/ws/prj/machinefabric/main-worktree") };
std::fs::write(&definition, render(Supervisor::Systemd, &record_for("/home/op/ws/elsewhere")))
.expect("an older unit");
let mut systemd = StandIn::running(0);
apply_with(&mut systemd, Supervisor::Systemd, &enabled, &definition, Duration::from_secs(5))
.expect("a changed definition is applied");
assert_eq!(
systemd.verbs(),
[
"systemctl daemon-reload",
&format!("systemctl try-restart {UNIT}"),
&format!("systemctl enable --now {UNIT}"),
],
"a running unit was left on the definition it was started from"
);
let mut systemd = StandIn::running(0);
apply_with(&mut systemd, Supervisor::Systemd, &enabled, &definition, Duration::from_secs(5))
.expect("the same definition is applied");
assert_eq!(
systemd.verbs(),
["systemctl daemon-reload", &format!("systemctl enable --now {UNIT}")],
"an unchanged definition restarted the daemon"
);
let mut systemd = StandIn::running(0);
let disabled = Record { autostart: false, ..enabled };
apply_with(&mut systemd, Supervisor::Systemd, &disabled, &definition, Duration::from_secs(5))
.expect("disabling is applied");
assert_eq!(
systemd.verbs(),
["systemctl daemon-reload", &format!("systemctl disable {UNIT}")],
"disabling ended the running daemon"
);
}