use super::*;
fn ours() -> BTreeSet<u32> {
[42u32, 43, 44].into_iter().collect()
}
#[test]
fn test9018_a_process_never_finds_itself() {
let listed = "\
42 /home/b/.cargo/bin/wsb run --client unifloom --kill-existing
43 bash -c wsb run --client unifloom
9001 /opt/unifloom/unifloom --enable-features=x
9002 /Applications/FabricLoom.app/Contents/MacOS/FabricLoom
";
let found = parse_matches(listed, &ours());
assert_eq!(
found.iter().map(|m| m.pid).collect::<Vec<_>>(),
vec![9001, 9002],
"only the actual clients are matches; the launcher and its shell are not"
);
assert!(
found.iter().all(|m| !m.command.contains("--kill-existing")),
"the process doing the killing must never be among the killed: {found:?}"
);
}
#[test]
fn test9019_only_ourselves_means_nothing_is_running() {
let listed = "42 wsb run --client unifloom\n43 bash -c wsb run --client unifloom\n";
assert!(
parse_matches(listed, &ours()).is_empty(),
"a launcher seeing only itself must report nothing running"
);
}
#[test]
fn test9020_matches_carry_what_they_are_running() {
let found = parse_matches("9001 /opt/unifloom/unifloom --flag\n", &ours());
assert_eq!(found.len(), 1);
assert_eq!(found[0].pid, 9001);
assert_eq!(found[0].command, "/opt/unifloom/unifloom --flag");
assert_eq!(
describe(&found),
"9001 /opt/unifloom/unifloom --flag"
);
}
#[test]
fn test9021_only_pid_lines_are_read() {
let listed = "\n\npgrep: cannot allocate memory\n \n9001 unifloom\nnot-a-pid also-not\n";
let found = parse_matches(listed, &BTreeSet::new());
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].pid, 9001);
}
#[test]
fn test9022_the_lineage_always_holds_this_process() {
let found = lineage();
assert!(
found.contains(&std::process::id()),
"the asking process must always be excluded from its own matches"
);
assert!(!found.contains(&0), "pid 0 is not an ancestor");
assert!(!found.contains(&1), "init is not treated as an ancestor to protect");
}
#[test]
fn test9023_a_real_child_is_found_and_the_asker_is_not() {
let marker = format!("wsb-process-rule-{}", std::process::id());
let child = crate::git_backend::command_without_repo_scope("sh")
.arg("-c")
.arg(format!("sleep 30 # {marker}\n:"))
.spawn();
let Ok(mut child) = child else {
return;
};
std::thread::sleep(std::time::Duration::from_millis(300));
let found = matching(&marker);
let pids: Vec<u32> = found.iter().map(|m| m.pid).collect();
assert!(
pids.contains(&child.id()),
"the child running the marker must be found: {found:?}"
);
assert!(
!pids.contains(&std::process::id()),
"this process must never be among its own matches: {found:?}"
);
signal(&pids, "TERM");
let _ = child.wait();
std::thread::sleep(std::time::Duration::from_millis(300));
assert!(
matching(&marker).is_empty(),
"the child was signalled and should be gone"
);
}
mod stopping {
use super::super::*;
fn sleeper() -> std::process::Child {
#[cfg(unix)]
let mut command = {
let mut command = crate::git_backend::command_without_repo_scope("sleep");
command.arg("120");
command
};
#[cfg(windows)]
let mut command = {
let mut command = crate::git_backend::command_without_repo_scope("powershell");
command.args(["-NoProfile", "-Command", "Start-Sleep -Seconds 120"]);
command
};
command
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("sleep runs")
}
#[test]
fn test11149_a_signal_that_lands_is_reported_as_landing() {
let mut child = sleeper();
let pid = child.id();
assert!(is_alive(pid), "it started");
let results = signal(&[pid], "TERM");
assert_eq!(results.len(), 1);
assert_eq!(results[0].outcome, SignalOutcome::Sent);
assert!(!results[0].outcome.is_failure());
let _ = child.wait();
}
#[test]
fn test11150_a_process_that_is_already_gone_is_not_a_failure() {
let mut child = sleeper();
let pid = child.id();
let _ = signal(&[pid], "KILL");
let _ = child.wait();
let results = signal(&[pid], "TERM");
assert_eq!(results[0].outcome, SignalOutcome::Gone);
assert!(!results[0].outcome.is_failure());
}
#[cfg(unix)]
#[test]
fn test11151_signalling_pid_one_is_refused_rather_than_silently_failing() {
if unsafe { libc::geteuid() } == 0 {
return;
}
let results = signal(&[1], "TERM");
assert_eq!(
results[0].outcome,
SignalOutcome::NotPermitted,
"another user's process is refused, and says so"
);
assert!(results[0].outcome.is_failure());
}
#[test]
fn test11152_an_unknown_signal_is_refused_rather_than_guessed() {
let results = signal(&[std::process::id()], "HUP-ish");
assert!(matches!(results[0].outcome, SignalOutcome::Failed(_)));
assert!(is_alive(std::process::id()));
}
#[test]
fn test11153_being_alive_is_asked_of_the_kernel_not_assumed() {
let mut child = sleeper();
let pid = child.id();
assert!(is_alive(pid));
let _ = signal(&[pid], "KILL");
let _ = child.wait();
assert!(!is_alive(pid), "and it knows when it has gone");
}
}
#[test]
fn test12612_descendants_are_found_through_their_parents_deepest_first() {
let table = [
(10u32, 20u32),
(11, 10),
(12, 10),
(13, 11),
(14, 13),
(20, 1),
(21, 20),
(22, 21),
];
assert_eq!(descendants_in(&table, 10), vec![14, 13, 12, 11]);
assert_eq!(descendants_in(&table, 22), Vec::<u32>::new(), "a leaf started nothing");
let cyclic = [(30u32, 31u32), (31, 30)];
assert_eq!(descendants_in(&cyclic, 30), vec![31]);
}
#[cfg(unix)]
mod passing_a_stop_on {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
fn shell(script: &str) -> std::process::Child {
crate::git_backend::command_without_repo_scope("sh")
.args(["-c", script])
.spawn()
.expect("sh starts")
}
#[test]
fn test12613_a_stop_is_passed_to_the_child_being_waited_on() {
use std::os::unix::process::ExitStatusExt;
let mut child = shell("sleep 60");
let flag = AtomicBool::new(true);
let started = Instant::now();
let status = wait_passing_stop(&mut child, &flag, Duration::from_secs(30)).unwrap();
assert_eq!(status.signal(), Some(libc::SIGTERM), "the child was asked, not killed");
assert!(started.elapsed() < Duration::from_secs(10), "it ended when asked, not at the grace");
}
#[test]
fn test12614_a_child_that_ignores_a_stop_is_ended_after_the_grace() {
use std::os::unix::process::ExitStatusExt;
let mut child = crate::git_backend::command_without_repo_scope("sh")
.args(["-c", "trap '' TERM; echo ready; while :; do sleep 1; done"])
.stdout(std::process::Stdio::piped())
.spawn()
.expect("sh starts");
let mut ready = [0u8; 6];
std::io::Read::read_exact(child.stdout.as_mut().unwrap(), &mut ready).unwrap();
assert_eq!(&ready, b"ready\n");
let flag = AtomicBool::new(true);
let grace = Duration::from_millis(400);
let started = Instant::now();
let status = wait_passing_stop(&mut child, &flag, grace).unwrap();
assert_eq!(status.signal(), Some(libc::SIGKILL), "it was ended, since it would not stop");
assert!(started.elapsed() >= grace, "it was given the grace first");
assert!(started.elapsed() < Duration::from_secs(10));
}
#[test]
fn test12615_a_child_nobody_stopped_ends_with_its_own_status() {
let mut child = shell("exit 7");
let flag = AtomicBool::new(false);
let status = wait_passing_stop(&mut child, &flag, Duration::from_secs(30)).unwrap();
assert_eq!(status.code(), Some(7));
assert!(!flag.load(Ordering::SeqCst));
}
#[test]
fn test12616_a_real_grandchild_is_a_descendant() {
let mut child = crate::git_backend::command_without_repo_scope("sh")
.args(["-c", "sleep 60 & echo $!; wait"])
.stdout(std::process::Stdio::piped())
.spawn()
.expect("sh starts");
let mut line = String::new();
std::io::BufRead::read_line(
&mut std::io::BufReader::new(child.stdout.as_mut().unwrap()),
&mut line,
)
.unwrap();
let grandchild: u32 = line.trim().parse().expect("the shell printed its child's pid");
let found = descendants().unwrap();
let _ = signal(&[grandchild, child.id()], "KILL");
let _ = child.wait();
let child_at = found.iter().position(|pid| *pid == child.id());
let grandchild_at = found.iter().position(|pid| *pid == grandchild);
assert!(child_at.is_some(), "the child is missing from {found:?}");
assert!(grandchild_at.is_some(), "the grandchild is missing from {found:?}");
assert!(grandchild_at < child_at, "deepest first: {found:?}");
}
}