#![cfg(unix)]
use super::*;
use crate::test_support::{ActionSink, SharedSink};
use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
const QUICK: Cadence = Cadence {
grace: Duration::from_millis(60),
interval: Duration::from_millis(60),
};
fn terms(limit: Duration, sink: &SharedSink) -> WatchTerms {
return WatchTerms {
limit,
cadence: QUICK,
notices: Box::new(sink.clone()),
};
}
#[test]
fn the_timeout_sentence_names_the_wait_the_bound_and_the_next_step() {
let limit: Duration = ChildClass::Object.default_limit();
let subject: String = format!("git add -- a.md, {}", ChildClass::Object.label());
let text: String = watched_timeout_text(subject.as_str(), limit);
assert!(text.starts_with(format!("{subject} did not finish within ").as_str()), "{text}");
assert!(text.contains(format!("within {} seconds and was stopped", limit.as_secs()).as_str()), "{text}");
assert!(text.contains(format!("{CHILD_WAIT_VARIABLE}=").as_str()), "the raise hint spells the variable: {text}");
assert!(text.contains("run the command again"), "{text}");
assert!(text.contains("`ps`"), "{text}");
}
#[test]
fn the_reader_subject_names_the_command_and_its_class() {
let subject: String = reader_subject();
assert!(subject.starts_with("git cat-file --batch"), "{subject}");
assert!(subject.ends_with(ChildClass::Object.label()), "{subject}");
}
#[test]
fn the_terms_of_a_class_carry_its_bound_and_a_grace_inside_it() {
for class in [ChildClass::Plumbing, ChildClass::Object] {
let terms: WatchTerms = WatchTerms::for_class(class);
assert_eq!(terms.limit, class.limit(), "{class:?}");
assert!(terms.cadence.grace <= class.notice_grace(), "{class:?}");
assert!(terms.cadence.grace <= terms.cadence.interval, "{class:?}");
assert!(terms.cadence.grace < terms.limit, "{class:?}");
}
}
#[test]
fn a_short_bound_pulls_the_first_notice_inside_it() {
for class in [ChildClass::Plumbing, ChildClass::Object] {
let interval: Duration = class.notice_grace() * 2;
let short: Duration = class.notice_grace() / 2;
assert!(grace_for(class, interval, short) < short, "{class:?}");
assert_eq!(grace_for(class, interval, class.default_limit()), class.notice_grace(), "{class:?}");
}
}
struct LineSignal {
shared: SharedSink,
lines: std::sync::mpsc::Sender<()>,
}
impl Write for LineSignal {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
Write::write(&mut self.shared, bytes)?;
for _line in bytes.iter().filter(|byte| { return **byte == b'\n'; }) {
self.lines.send(()).ok();
}
return Ok(bytes.len());
}
fn flush(&mut self) -> std::io::Result<()> {
return Ok(());
}
}
#[test]
fn a_wait_past_the_bound_is_announced_then_the_child_is_stopped() {
let sink: SharedSink = SharedSink::default();
let mut child: Child = Command::new("sleep").arg("30").spawn().expect("sleeper");
let watch: ChildWatch = ChildWatch::start("the sleeper", child.id(), terms(Duration::from_millis(450), &sink));
watch.begin();
let status = child.wait().expect("the stopped child is collected");
watch.end();
assert!(watch.expired());
assert_eq!(status.signal(), Some(9), "{status:?}");
let text: String = sink.text();
let lines: Vec<&str> = text.lines().collect();
assert!(!lines.is_empty(), "{text}");
for line in &lines {
assert!(line.starts_with("cli-git: still waiting for the sleeper (process "), "{text}");
assert!(line.contains("(CLI_GIT_CHILD_WAIT_SECONDS changes this)"), "{text}");
}
assert_eq!(text.matches("Hint:").count(), 1, "{text}");
}
#[test]
fn a_wait_that_ends_inside_the_grace_prints_nothing() {
let sink: SharedSink = SharedSink::default();
let mut child: Child = Command::new("sleep").arg("30").spawn().expect("sleeper");
let watch: ChildWatch = ChildWatch::start(
"the sleeper",
child.id(),
WatchTerms {
limit: Duration::from_secs(7200),
cadence: Cadence { grace: Duration::from_secs(3600), interval: Duration::from_secs(3600) },
notices: Box::new(sink.clone()),
},
);
std::thread::sleep(Duration::from_millis(50));
watch.begin();
std::thread::sleep(Duration::from_millis(50));
watch.end();
assert!(!watch.expired());
assert_eq!(sink.text(), "");
drop(watch);
child.kill().expect("kill");
child.wait().expect("collect");
}
#[test]
fn a_wait_that_has_ended_is_announced_no_more() {
let shared: SharedSink = SharedSink::default();
let (sender, receiver) = std::sync::mpsc::channel::<()>();
let mut child: Child = Command::new("sleep").arg("30").spawn().expect("sleeper");
let watch: ChildWatch = ChildWatch::start(
"the sleeper",
child.id(),
WatchTerms {
limit: Duration::from_secs(3600),
cadence: QUICK,
notices: Box::new(LineSignal { shared: shared.clone(), lines: sender }),
},
);
watch.begin();
receiver.recv().expect("a notice announces the wait");
watch.end();
std::thread::sleep(QUICK.interval * 3);
let settled: usize = shared.text().lines().count();
std::thread::sleep(QUICK.interval * 6);
let later: String = shared.text();
let expired: bool = watch.expired();
drop(watch);
child.kill().expect("kill");
child.wait().expect("collect");
assert_eq!(later.lines().count(), settled, "a finished wait went on being announced: {later}");
assert!(!expired);
}
#[test]
fn each_wait_is_announced_from_its_own_start() {
let shared: SharedSink = SharedSink::default();
let (sender, receiver) = std::sync::mpsc::channel::<()>();
let mut child: Child = Command::new("sleep").arg("30").spawn().expect("sleeper");
let watch: ChildWatch = ChildWatch::start(
"the sleeper",
child.id(),
WatchTerms {
limit: Duration::from_secs(3600),
cadence: QUICK,
notices: Box::new(LineSignal { shared: shared.clone(), lines: sender }),
},
);
for _wait in 0..2 {
watch.begin();
receiver.recv().expect("a notice announces the wait");
watch.end();
}
let text: String = shared.text();
assert_eq!(text.matches("Hint:").count(), 2, "the hint opens each wait: {text}");
assert!(!watch.expired());
drop(watch);
child.kill().expect("kill");
child.wait().expect("collect");
}
#[test]
fn a_command_that_runs_long_is_announced_and_its_output_is_kept() {
let shared: SharedSink = SharedSink::default();
let root: PathBuf = crate::test_support::fixture("child-watch-long");
let release: PathBuf = root.join("release");
let mut command: Command = Command::new("sh");
command
.arg("-c")
.arg("while [ ! -e \"$1\" ]; do sleep 0.02; done; printf out; printf err >&2")
.arg("sh")
.arg(&release)
.stdin(Stdio::null());
let release_file: PathBuf = release.clone();
let counted: ActionSink = ActionSink::new(&shared, 2, move || {
std::fs::write(&release_file, b"").expect("release the command");
});
let output: Output = output_watched(
&mut command,
"the slow command",
WatchTerms { limit: Duration::from_secs(3600), cadence: QUICK, notices: Box::new(counted) },
)
.expect("finished");
assert_eq!((output.stdout, output.stderr), (b"out".to_vec(), b"err".to_vec()));
assert!(output.status.success());
let text: String = shared.text();
assert!(text.lines().count() >= 2, "{text}");
assert!(text.starts_with("cli-git: still waiting for the slow command (process "), "{text}");
crate::test_support::remove(root.as_path());
}
#[test]
fn a_command_past_the_bound_is_stopped_and_reported() {
let sink: SharedSink = SharedSink::default();
let mut command: Command = Command::new("sleep");
command.arg("30").stdin(Stdio::null());
let failure: std::io::Error = output_watched(&mut command, "git rev-parse HEAD", terms(Duration::from_millis(400), &sink))
.expect_err("stopped at the bound");
assert_eq!(failure.kind(), std::io::ErrorKind::TimedOut);
assert!(failure.to_string().starts_with("git rev-parse HEAD did not finish within 0 seconds and was stopped"), "{failure}");
assert!(sink.text().lines().count() >= 1, "{}", sink.text());
}
fn helper_is_running(pid: i32) -> bool {
return !matches!(crate::process_identity::process_birth_identity(i64::from(pid)), Ok(None));
}
#[test]
fn a_killed_process_that_is_not_yet_reaped_is_not_running() {
let mut child: std::process::Child = Command::new("sleep").arg("30").stdin(Stdio::null()).spawn().expect("a process");
let pid: i32 = i32::try_from(child.id()).expect("a process ID");
assert!(helper_is_running(pid), "a sleeping process runs");
unsafe { libc::kill(pid, libc::SIGKILL) };
let began: Instant = Instant::now();
while std::fs::read_to_string(format!("/proc/{pid}/stat")).is_ok_and(|text| { return !text.contains(") Z "); }) {
assert!(began.elapsed() < Duration::from_secs(10), "the process never became a zombie");
std::thread::sleep(Duration::from_millis(2));
}
assert_eq!(unsafe { libc::kill(pid, 0) }, 0, "the premise: a zombie still answers signal 0");
assert!(!helper_is_running(pid), "a zombie has exited");
child.wait().expect("reaped");
assert!(!helper_is_running(pid), "a reaped process is gone");
}
#[test]
fn a_command_past_the_bound_is_stopped_with_the_processes_it_started() {
let root: PathBuf = crate::test_support::fixture("child-watch-tree");
let helper_file: PathBuf = root.join("helper-pid");
let helper_seconds: u64 = 25;
let sink: SharedSink = SharedSink::default();
let mut command: Command = Command::new("sh");
command
.arg("-c")
.arg(format!("sleep {helper_seconds} & echo $! > '{}'; wait", helper_file.display()))
.stdin(Stdio::null());
let started: Instant = Instant::now();
let failure: std::io::Error = output_watched(&mut command, "a step with a helper", terms(Duration::from_millis(600), &sink))
.expect_err("stopped at the bound");
let waited: Duration = started.elapsed();
assert_eq!(failure.kind(), std::io::ErrorKind::TimedOut);
let helper: i32 = std::fs::read_to_string(&helper_file).expect("the helper's process ID").trim().parse().expect("a process ID");
let helper_alive: bool = helper_is_running(helper);
unsafe { libc::kill(helper, libc::SIGKILL) };
assert!(
waited < Duration::from_secs(helper_seconds / 2),
"the stop took {waited:?}, as long as the helper that kept the output open lived"
);
assert!(!helper_alive, "the helper the step started is still running after the step was stopped");
crate::test_support::remove(root.as_path());
}
#[test]
fn a_command_that_finishes_at_once_prints_nothing() {
let sink: SharedSink = SharedSink::default();
let mut command: Command = Command::new("true");
command.stdin(Stdio::null());
let output: Output = output_watched(
&mut command,
"true",
WatchTerms {
limit: Duration::from_secs(7200),
cadence: Cadence { grace: Duration::from_secs(3600), interval: Duration::from_secs(3600) },
notices: Box::new(sink.clone()),
},
)
.expect("finished");
assert!(output.status.success());
assert_eq!(sink.text(), "");
}