use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use marver::daemon::{self, Config, Startup};
use tempfile::TempDir;
struct Daemon(u32);
impl Drop for Daemon {
fn drop(&mut self) {
let _ = Command::new("kill").arg(self.0.to_string()).status();
}
}
fn config(dir: &TempDir) -> Config {
let mut config = Config::new(dir.path(), dir.path());
config.marver_bin = PathBuf::from(env!("CARGO_BIN_EXE_marver"));
config
}
fn process_group(pid: u32) -> String {
let out = Command::new("ps")
.args(["-o", "pgid=", "-p", &pid.to_string()])
.output()
.expect("ps");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn a_daemon_is_started_on_demand_and_then_found_rather_than_duplicated() {
let dir = TempDir::new().unwrap();
let config = config(&dir);
assert!(!daemon::is_running(&config), "nothing should be listening");
let Startup::Started { pid } = daemon::ensure_running(&config).expect("start") else {
panic!("the first call should have started one");
};
let _guard = Daemon(pid);
assert!(
daemon::is_running(&config),
"ensure_running must not return until the socket answers"
);
assert_eq!(
daemon::ensure_running(&config).expect("second call"),
Startup::AlreadyRunning,
"a second caller must find the first daemon, not start another"
);
}
#[test]
fn an_auto_started_daemon_is_in_its_own_process_group() {
let dir = TempDir::new().unwrap();
let config = config(&dir);
let Startup::Started { pid } = daemon::ensure_running(&config).expect("start") else {
panic!("expected a fresh daemon");
};
let _guard = Daemon(pid);
let ours = process_group(std::process::id());
let theirs = process_group(pid);
assert!(!theirs.is_empty(), "the daemon should still be running");
assert_ne!(
ours, theirs,
"the daemon shares our process group, so ctrl-c would kill it"
);
}
#[test]
fn a_second_daemon_refuses_instead_of_stealing_the_socket() {
let dir = TempDir::new().unwrap();
let config = config(&dir);
let Startup::Started { pid } = daemon::ensure_running(&config).expect("start") else {
panic!("expected a fresh daemon");
};
let _guard = Daemon(pid);
let out = Command::new(env!("CARGO_BIN_EXE_marver"))
.args(["daemon", "--data-dir"])
.arg(dir.path())
.arg("--scan-root")
.arg(dir.path())
.output()
.expect("run marver daemon");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "a second daemon must exit non-zero");
assert!(
stderr.contains("already running"),
"it should say a daemon is running, not leak an errno: {stderr:?}"
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("cap"),
"a refused start should announce no configuration: {stdout:?}"
);
assert!(
daemon::is_running(&config),
"the original daemon must still hold its socket"
);
}
#[test]
fn status_reports_what_is_running_and_exits_accordingly() {
let dir = TempDir::new().unwrap();
let config = config(&dir);
let status = |dir: &TempDir| {
Command::new(env!("CARGO_BIN_EXE_marver"))
.args(["status", "--data-dir"])
.arg(dir.path())
.output()
.expect("run marver status")
};
let before = status(&dir);
let text = String::from_utf8_lossy(&before.stdout).to_string();
assert!(text.contains("not running"), "{text:?}");
assert!(
!before.status.success(),
"a shell should be able to ask, so this exits non-zero"
);
assert!(
!config.db.exists(),
"reporting on the system must not create part of it"
);
let Startup::Started { pid } = daemon::ensure_running(&config).expect("start") else {
panic!("expected a fresh daemon");
};
let _guard = Daemon(pid);
let after = status(&dir);
let text = String::from_utf8_lossy(&after.stdout).to_string();
assert!(after.status.success(), "{text:?}");
assert!(
text.contains("running") && !text.contains("not running"),
"{text:?}"
);
}
#[test]
fn a_liveness_probe_leaves_no_trace_in_the_log() {
let dir = TempDir::new().unwrap();
let config = config(&dir);
let Startup::Started { pid } = daemon::ensure_running(&config).expect("start") else {
panic!("expected a fresh daemon");
};
let _guard = Daemon(pid);
for _ in 0..5 {
assert!(daemon::is_running(&config));
}
std::thread::sleep(Duration::from_millis(300));
let log = std::fs::read_to_string(&config.log).unwrap_or_default();
assert!(!log.contains("ignoring hook"), "{log:?}");
}