#![cfg(unix)]
use assert_cmd::Command;
use serde_json::Value;
use std::process::{Child, Command as StdCommand};
use std::time::{Duration, Instant};
struct TestChild(Child);
impl TestChild {
fn exiting_after(secs: &str) -> Self {
TestChild(StdCommand::new("sleep").arg(secs).spawn().unwrap())
}
fn ignoring_sigterm() -> Self {
let child = StdCommand::new("sh")
.args(["-c", "trap '' TERM; exec sleep 300"])
.spawn()
.unwrap();
std::thread::sleep(Duration::from_millis(300));
TestChild(child)
}
fn pid(&self) -> String {
self.0.id().to_string()
}
}
impl Drop for TestChild {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn default_sigint(command: &mut StdCommand) -> &mut StdCommand {
use std::os::unix::process::CommandExt;
unsafe {
command.pre_exec(|| {
libc::signal(libc::SIGINT, libc::SIG_DFL);
Ok(())
});
}
command
}
fn run_json_timed(args: &[&str], limit_secs: u64) -> (Value, Option<i32>, f64) {
let started = Instant::now();
let output = Command::cargo_bin("proc")
.unwrap()
.args(args)
.timeout(Duration::from_secs(limit_secs))
.output()
.unwrap();
let took = started.elapsed().as_secs_f64();
let stdout = String::from_utf8(output.stdout).unwrap();
let json = serde_json::from_str(&stdout).unwrap_or(Value::Null);
(json, output.status.code(), took)
}
#[test]
fn wait_timeout_is_not_rounded_up_to_the_interval() {
let child = TestChild::exiting_after("300");
let (json, code, took) =
run_json_timed(&["wait", &child.pid(), "--timeout", "1", "--json"], 20);
assert_eq!(code, Some(1), "{json}");
assert_eq!(json["timed_out"], true);
assert!(took < 4.0, "took {took:.1}s for --timeout 1");
}
#[test]
fn wait_returns_soon_after_the_process_exits() {
let child = TestChild::exiting_after("1");
let (json, code, took) = run_json_timed(&["wait", &child.pid(), "--json"], 20);
assert_eq!(code, Some(0), "{json}");
assert_eq!(json["success"], true);
assert!(
took < 4.0,
"took {took:.1}s to notice a 1-second process exit"
);
}
#[test]
fn stop_waits_for_all_processes_together_and_never_hangs() {
let first = TestChild::ignoring_sigterm();
let second = TestChild::ignoring_sigterm();
let targets = format!("{},{}", first.pid(), second.pid());
let (json, code, took) =
run_json_timed(&["stop", &targets, "--yes", "--timeout", "3", "--json"], 30);
assert_eq!(
code,
Some(0),
"stop hung or failed after {took:.1}s: {json}"
);
assert_eq!(json["succeeded_count"], 2, "{json}");
assert!(
took < 5.5,
"took {took:.1}s for two processes with --timeout 3"
);
}
#[test]
fn unstick_sees_a_spinning_process_as_stuck() {
let spinning = TestChild(
default_sigint(StdCommand::new("sh").args(["-c", "while :; do :; done"]))
.spawn()
.unwrap(),
);
std::thread::sleep(Duration::from_millis(300));
let (json, code, took) = run_json_timed(&["unstick", &spinning.pid(), "--yes", "--json"], 30);
assert_eq!(code, Some(0), "after {took:.1}s: {json}");
assert_eq!(json["not_stuck"], 0, "{json}");
assert_eq!(json["terminated"], 1, "{json}");
}
#[test]
fn list_measures_cpu_once_for_all_processes() {
let (json, code, took) = run_json_timed(&["list", "--json"], 60);
assert_eq!(code, Some(0), "{json}");
let count = json["count"].as_u64().unwrap();
assert!(took < 2.0, "list of {count} processes took {took:.1}s");
}
#[test]
fn the_missing_yes_error_lists_processes_with_measured_cpu() {
let spinning = TestChild(
StdCommand::new("sh")
.args(["-c", "while :; do :; done"])
.spawn()
.unwrap(),
);
std::thread::sleep(Duration::from_millis(300));
let (json, code, took) = run_json_timed(&["kill", &spinning.pid(), "--json"], 30);
assert_eq!(code, Some(4), "after {took:.1}s: {json}");
assert_eq!(json["needs_confirmation"], true, "{json}");
let cpu = json["processes"][0]["cpu_percent"].as_f64().unwrap();
assert!(cpu > 20.0, "cpu_percent {cpu} was not measured: {json}");
}