#![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}");
}
struct Proc(Child);
impl Drop for Proc {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn ended_within(child: &mut Proc, limit: Duration) -> Option<Option<i32>> {
let deadline = Instant::now() + limit;
while Instant::now() < deadline {
if let Some(status) = child.0.try_wait().unwrap() {
return Some(status.code());
}
std::thread::sleep(Duration::from_millis(20));
}
None
}
#[test]
fn watch_json_ends_soon_after_its_reader_leaves() {
use std::io::{BufRead, BufReader};
use std::process::Stdio;
let target = TestChild::exiting_after("300");
let mut watch = Proc(
StdCommand::new(env!("CARGO_BIN_EXE_proc"))
.args(["watch", &target.pid(), "--json", "-n", "30"])
.stdout(Stdio::piped())
.spawn()
.unwrap(),
);
let mut reader = BufReader::new(watch.0.stdout.take().unwrap());
let mut first = String::new();
reader.read_line(&mut first).unwrap();
assert!(first.contains("\"action\":\"watch\""), "{first}");
let left = Instant::now();
drop(reader);
let code = ended_within(&mut watch, Duration::from_secs(5))
.unwrap_or_else(|| panic!("still running {:.1}s after its reader left", 5.0));
assert_eq!(code, Some(0), "a reader that left is not an error");
assert!(
left.elapsed() < Duration::from_secs(5),
"took {:?} to notice",
left.elapsed()
);
}
#[test]
fn wait_keeps_waiting_when_its_reader_leaves_and_ends_with_its_own_code() {
use std::process::Stdio;
let target = TestChild::exiting_after("2");
let (reader, writer) = std::io::pipe().unwrap();
drop(reader);
let started = Instant::now();
let mut wait = Proc(
StdCommand::new(env!("CARGO_BIN_EXE_proc"))
.args(["wait", &target.pid()])
.stdout(Stdio::from(writer))
.spawn()
.unwrap(),
);
assert_eq!(
ended_within(&mut wait, Duration::from_secs(20)),
Some(Some(0))
);
assert!(
started.elapsed() >= Duration::from_millis(1500),
"wait stopped after {:?}, before the process had ended",
started.elapsed()
);
let target = TestChild::exiting_after("300");
let (reader, writer) = std::io::pipe().unwrap();
drop(reader);
let mut wait = Proc(
StdCommand::new(env!("CARGO_BIN_EXE_proc"))
.args(["wait", &target.pid(), "--timeout", "1"])
.stdout(Stdio::from(writer))
.spawn()
.unwrap(),
);
assert_eq!(
ended_within(&mut wait, Duration::from_secs(20)),
Some(Some(1))
);
}