use regex::Regex;
use std::process::{Child, Command, Stdio};
const BIN: &str = env!("CARGO_BIN_EXE_pkill");
struct SleepChild(Child);
impl Drop for SleepChild {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn spawn_sleep() -> SleepChild {
let c = Command::new("sleep")
.arg("30") .stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn sleep");
SleepChild(c)
}
#[track_caller]
fn run(args: &[&str]) -> std::process::Output {
Command::new(BIN).args(args).output().expect("spawn pkill")
}
#[track_caller]
fn assert_matches(haystack: &str, pattern: &str) {
let re = Regex::new(pattern).expect("valid regex");
assert!(
re.is_match(haystack),
"pattern did not match\n--- pattern ---\n{pattern}\n--- output ---\n{haystack}"
);
}
#[test]
#[ignore = "intentional: error wording differs (procps says 'no matching criteria specified')"]
fn no_arguments() {
let out = run(&[]);
assert!(!out.status.success());
}
#[test]
fn find_both_test_pids() {
let c1 = spawn_sleep();
let c2 = spawn_sleep();
let pid1 = c1.0.id() as i32;
let pid2 = c2.0.id() as i32;
let out = run(&["-0", "-e", "-p", &format!("{pid1},{pid2}")]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let count = stdout.matches("killed (pid").count();
assert_eq!(
count, 2,
"expected two 'killed (pid …)' lines, got:\n{stdout}"
);
}
#[test]
fn signal_option_order() {
let c1 = spawn_sleep();
let c2 = spawn_sleep();
let pid1 = c1.0.id() as i32;
let pid2 = c2.0.id() as i32;
let out = run(&["-e", "-p", &format!("{pid1},{pid2}"), "-0"]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let count = stdout.matches("killed (pid").count();
assert_eq!(
count, 2,
"expected two 'killed (pid …)' lines, got:\n{stdout}"
);
}
#[test]
#[ignore = "intentional: clap's invalid-flag error wording differs from procps's"]
fn trailing_garbage_on_int_signal() {
let c = spawn_sleep();
let pid = c.0.id() as i32;
let out = run(&["-0garbage", "-p", &pid.to_string()]);
assert!(!out.status.success());
}
#[test]
fn sigusr1_invocation_succeeds() {
let c = spawn_sleep();
let pid = c.0.id() as i32;
let out = run(&["-USR1", "-e", "-p", &pid.to_string()]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_matches(&stdout, &format!(r"killed \(pid {pid}\)"));
}
#[test]
fn sigusr2_invocation_succeeds() {
let c = spawn_sleep();
let pid = c.0.id() as i32;
let out = run(&["-USR2", "-e", "-p", &pid.to_string()]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_matches(&stdout, &format!(r"killed \(pid {pid}\)"));
}
#[test]
fn pidfile_filters_to_listed_pids() {
let c = spawn_sleep();
let pid = c.0.id();
let dir = tempfile::tempdir().expect("tmp");
let path = dir.path().join("pkill.pid");
std::fs::write(&path, format!("{pid}\n")).expect("write pidfile");
let out = run(&["-0", "-e", "-F", path.to_str().unwrap()]);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains(&format!("(pid {pid})")),
"expected `(pid {pid})` in {stdout:?}",
);
}
#[test]
fn pidfile_conflict_with_p() {
let out = run(&["-p", "1", "-F", "/dev/null"]);
assert!(!out.status.success());
}
#[test]
fn sigqueue_value() {
let c = spawn_sleep();
let pid = c.0.id();
let out = run(&["-USR1", "-e", "-q", "42", "-p", &pid.to_string()]);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains(&format!("(pid {pid})")),
"expected `(pid {pid})` in {stdout:?}",
);
}
#[test]
fn sigqueue_rejects_unparseable_value() {
let out = run(&["-USR1", "-q", "not-an-int", "sleep"]);
assert!(!out.status.success());
}