#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use std::path::Path;
use std::process::Command;
use std::time::Duration;
use super::module_process::test_seam;
use super::*;
use crate::types::DiscoverySpec;
fn shell_module(id: &str, script: String) -> ModuleConfig {
ModuleConfig {
id: id.to_string(),
command: "sh".to_string(),
args: vec!["-c".to_string(), script],
restart_policy: RestartPolicy::Never,
}
}
fn long_lived(id: &str, pid_file: &Path) -> ModuleConfig {
shell_module(
id,
format!(
r#"echo $$ >> '{}'; printf '%s\n' '{{"event_id":"evt-{id}","source":"module","timestamp_ms":1,"event":{{"kind":"module","module":"{id}","event_type":"ready","payload":{{}}}}}}'; exec sleep 30"#,
pid_file.display()
),
)
}
fn config(module: ModuleConfig) -> MobKitConfig {
MobKitConfig {
discovery: DiscoverySpec {
namespace: "module-process".to_string(),
modules: vec![module.id.clone()],
},
modules: vec![module],
pre_spawn: vec![],
}
}
fn pids(pid_file: &Path) -> Vec<u32> {
std::fs::read_to_string(pid_file)
.unwrap()
.lines()
.map(|line| line.trim().parse().unwrap())
.collect()
}
fn running_or_unreaped(pid: u32) -> bool {
Command::new("sh")
.args(["-c", &format!("kill -0 {pid} 2>/dev/null")])
.status()
.unwrap()
.success()
}
#[test]
fn an_aborted_respawn_reaps_its_replacement_when_terminating_it_fails() {
let temp = tempfile::tempdir().unwrap();
let pid_file = temp.path().join("pids");
let mut runtime = start_mobkit_runtime_with_options(
config(long_lived("respawn", &pid_file)),
vec![],
Duration::from_secs(5),
RuntimeOptions::default(),
)
.expect("runtime starts");
let error = {
let _failing = test_seam::fail_terminations();
runtime
.spawn_member("respawn", Duration::from_secs(5))
.expect_err("the existing child cannot be terminated")
};
assert!(format!("{error:?}").contains("failed to terminate replacement child"));
let started = pids(&pid_file);
assert_eq!(started.len(), 2, "original and replacement both started");
assert!(
!running_or_unreaped(started[1]),
"replacement {} leaked by the aborted respawn",
started[1]
);
assert!(
running_or_unreaped(started[0]),
"existing child stays owned"
);
assert_eq!(runtime.shutdown().orphan_processes, 0);
assert!(!running_or_unreaped(started[0]));
}
#[test]
fn shutdown_reaps_a_child_whose_termination_fails() {
let temp = tempfile::tempdir().unwrap();
let pid_file = temp.path().join("pids");
let mut runtime = start_mobkit_runtime_with_options(
config(long_lived("live", &pid_file)),
vec![],
Duration::from_secs(5),
RuntimeOptions::default(),
)
.expect("runtime starts");
let pid = pids(&pid_file)[0];
let report = {
let _failing = test_seam::fail_terminations();
runtime.shutdown()
};
assert_eq!(
report.orphan_processes, 1,
"the failed termination is reported"
);
assert!(!running_or_unreaped(pid), "module {pid} leaked by shutdown");
}