#![cfg(feature = "daemon")]
use std::path::Path;
use running_process::daemon::services::{ServiceDef, ServiceRegistry, ServiceStatus};
use running_process::daemon::services_snapshot::{
resurrect_from_snapshot, save_snapshot, SNAPSHOT_FILE_NAME, SNAPSHOT_VERSION,
};
use tempfile::TempDir;
fn registry() -> (ServiceRegistry, TempDir) {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("svc.sqlite3");
let logs = tmp.path().join("services");
let reg = ServiceRegistry::open(&db, logs).unwrap();
(reg, tmp)
}
fn long_lived_cmd() -> Vec<String> {
#[cfg(windows)]
{
vec![
"cmd".into(),
"/C".into(),
"ping -n 300 127.0.0.1 > NUL".into(),
]
}
#[cfg(not(windows))]
{
vec!["sleep".into(), "300".into()]
}
}
fn def(name: &str, cmd: Vec<String>) -> ServiceDef {
ServiceDef {
name: name.to_string(),
cmd,
cwd: String::new(),
env: Vec::new(),
autorestart: false,
max_restarts: 0,
restart_delay_ms: 0,
kill_timeout_ms: 500,
min_uptime_ms: 0,
}
}
fn read_snapshot(path: &Path) -> serde_json::Value {
let bytes = std::fs::read(path).expect("snapshot file should exist");
serde_json::from_slice(&bytes).expect("snapshot should be valid JSON")
}
#[test]
fn save_writes_snapshot_to_local_scope() {
let (reg, tmp) = registry();
let _ = reg.start(def("alpha", long_lived_cmd())).unwrap();
let _ = reg.start(def("beta", long_lived_cmd())).unwrap();
let (path, count) = save_snapshot(®).expect("save_snapshot should succeed");
assert_eq!(count, 2, "save should report exactly 2 services");
assert_eq!(
path.file_name().unwrap().to_string_lossy(),
SNAPSHOT_FILE_NAME,
"snapshot must use the canonical filename"
);
let expected_parent = tmp.path();
assert_eq!(
path.parent().unwrap(),
expected_parent,
"snapshot must be co-located with the sqlite db"
);
assert!(path.is_file(), "{} must exist on disk", path.display());
let envelope = read_snapshot(&path);
assert_eq!(
envelope["version"].as_u64().unwrap_or(0),
SNAPSHOT_VERSION as u64,
"version field must be the supported snapshot version"
);
let services = envelope["services"].as_array().expect("services array");
assert_eq!(services.len(), 2, "snapshot should carry both services");
let mut names: Vec<&str> = services
.iter()
.map(|s| s["name"].as_str().unwrap_or(""))
.collect();
names.sort();
assert_eq!(names, vec!["alpha", "beta"]);
let _ = reg.stop("all");
}
#[test]
fn resurrect_restores_definitions_from_snapshot() {
let (reg, _tmp) = registry();
let _ = reg.start(def("alpha", long_lived_cmd())).unwrap();
let _ = reg.start(def("beta", long_lived_cmd())).unwrap();
save_snapshot(®).expect("save should succeed");
let _ = reg.delete("all").unwrap();
assert_eq!(reg.list().unwrap().len(), 0, "table should be empty");
let (restored, _restarted) = resurrect_from_snapshot(®).expect("resurrect should succeed");
assert_eq!(restored, 2, "exactly 2 definitions should be restored");
let all = reg.list().unwrap();
let mut names: Vec<&str> = all.iter().map(|r| r.def.name.as_str()).collect();
names.sort();
assert_eq!(names, vec!["alpha", "beta"]);
let _ = reg.stop("all");
}
#[test]
fn resurrect_restarts_only_previously_online_services() {
let (reg, _tmp) = registry();
let _ = reg.start(def("a-online", long_lived_cmd())).unwrap();
reg.register_def(def("b-stopped", long_lived_cmd()))
.unwrap();
save_snapshot(®).expect("save should succeed");
let _ = reg.delete("all").unwrap();
let (restored, restarted) = resurrect_from_snapshot(®).expect("resurrect should succeed");
assert_eq!(restored, 2, "both definitions should be restored");
assert_eq!(
restarted, 1,
"only the previously-online service should be restarted"
);
let a = reg.describe("a-online").unwrap();
let b = reg.describe("b-stopped").unwrap();
assert_eq!(
a.status,
ServiceStatus::Online,
"a-online should resume in online state"
);
assert_eq!(
b.status,
ServiceStatus::Stopped,
"b-stopped should stay stopped"
);
let _ = reg.stop("all");
}
#[test]
fn resurrect_with_no_snapshot_returns_not_found() {
let (reg, _tmp) = registry();
let err = resurrect_from_snapshot(®).expect_err("should fail with no snapshot");
let msg = err.to_string();
let snapshot_path = reg.snapshot_path().to_string_lossy().into_owned();
assert!(
msg.contains("no snapshot"),
"error message should explain the cause; got {msg:?}"
);
assert!(
msg.contains(&snapshot_path) || msg.contains(SNAPSHOT_FILE_NAME),
"error should name the missing path; got {msg:?}"
);
}
#[test]
fn resurrect_is_idempotent() {
let (reg, _tmp) = registry();
reg.register_def(def("svc-a", long_lived_cmd())).unwrap();
reg.register_def(def("svc-b", long_lived_cmd())).unwrap();
save_snapshot(®).expect("save should succeed");
let _ = resurrect_from_snapshot(®).expect("first resurrect should succeed");
let after_first = reg.list().unwrap();
assert_eq!(after_first.len(), 2);
let _ = resurrect_from_snapshot(®).expect("second resurrect should succeed");
let after_second = reg.list().unwrap();
assert_eq!(
after_second.len(),
2,
"second resurrect should not duplicate rows"
);
let mut names: Vec<&str> = after_second.iter().map(|r| r.def.name.as_str()).collect();
names.sort();
assert_eq!(names, vec!["svc-a", "svc-b"]);
}
#[test]
fn resurrect_rejects_unknown_snapshot_version() {
let (reg, _tmp) = registry();
let path = reg.snapshot_path().to_path_buf();
let payload = serde_json::json!({
"version": 999_999_u32,
"saved_at_ms": 0u64,
"services": []
});
std::fs::write(&path, payload.to_string()).unwrap();
let err = resurrect_from_snapshot(®).expect_err("unknown version must not resurrect");
let msg = err.to_string();
assert!(
msg.contains("999999") || msg.contains("not supported"),
"error should explain version mismatch; got {msg:?}"
);
}