Skip to content

Commit

Permalink
test(sleep): add test for signal handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Ecordonnier committed Oct 21, 2024
1 parent 29eb15a commit c16a512
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 3 deletions.
38 changes: 37 additions & 1 deletion tests/by-util/test_sleep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
// file that was distributed with this source code.
use rstest::rstest;

// spell-checker:ignore dont
// spell-checker:ignore dont SIGBUS SIGSEGV sigsegv sigbus
use crate::common::util::TestScenario;

#[cfg(unix)]
use nix::sys::signal::Signal::{SIGBUS, SIGSEGV};
use std::time::{Duration, Instant};

#[test]
Expand Down Expand Up @@ -135,6 +137,40 @@ fn test_sleep_wrong_time() {
new_ucmd!().args(&["0.1s", "abc"]).fails();
}

#[test]
#[cfg(unix)]
fn test_sleep_stops_after_sigsegv() {
let mut child = new_ucmd!()
.arg("100")
.timeout(Duration::from_secs(10))
.run_no_wait();

child
.delay(100)
.kill_with_custom_signal(SIGSEGV)
.make_assertion()
.with_current_output()
.signal_is(SIGSEGV as i32) // make sure it was us who terminated the process
.no_output();
}

#[test]
#[cfg(unix)]
fn test_sleep_stops_after_sigbus() {
let mut child = new_ucmd!()
.arg("100")
.timeout(Duration::from_secs(10))
.run_no_wait();

child
.delay(100)
.kill_with_custom_signal(SIGBUS)
.make_assertion()
.with_current_output()
.signal_is(SIGBUS as i32) // make sure it was us who terminated the process
.no_output();
}

#[test]
fn test_sleep_when_single_input_exceeds_max_duration_then_no_error() {
let mut child = new_ucmd!()
Expand Down
75 changes: 73 additions & 2 deletions tests/common/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// file that was distributed with this source code.

//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized openpty
//spell-checker: ignore (linux) winsize xpixel ypixel setrlimit FSIZE
//spell-checker: ignore (linux) winsize xpixel ypixel setrlimit FSIZE SIGBUS SIGSEGV sigbus

#![allow(dead_code)]
#![allow(
Expand All @@ -17,6 +17,8 @@
use libc::mode_t;
#[cfg(unix)]
use nix::pty::OpenptyResult;
#[cfg(unix)]
use nix::sys;
use pretty_assertions::assert_eq;
#[cfg(unix)]
use rlimit::setrlimit;
Expand Down Expand Up @@ -2095,7 +2097,7 @@ impl UChild {
self.delay(millis).make_assertion()
}

/// Try to kill the child process and wait for it's termination.
/// Try to kill the child process and wait for its termination.
///
/// This method blocks until the child process is killed, but returns an error if `self.timeout`
/// or the default of 60s was reached. If no such error happened, the process resources are
Expand Down Expand Up @@ -2155,6 +2157,75 @@ impl UChild {
self
}

/// Try to kill the child process and wait for its termination.
///
/// This method blocks until the child process is killed, but returns an error if `self.timeout`
/// or the default of 60s was reached. If no such error happened, the process resources are
/// released, so there is usually no need to call `wait` or alike on unix systems although it's
/// still possible to do so.
///
/// # Platform specific behavior
///
/// On unix systems the child process resources will be released like a call to [`Child::wait`]
/// or alike would do.
///
/// # Error
///
/// If [`Child::kill`] returned an error or if the child process could not be terminated within
/// `self.timeout` or the default of 60s.
#[cfg(unix)]
pub fn try_kill_with_custom_signal(
&mut self,
signal_name: sys::signal::Signal,
) -> io::Result<()> {
let start = Instant::now();
sys::signal::kill(
nix::unistd::Pid::from_raw(self.raw.id().try_into().unwrap()),
signal_name,
)
.unwrap();

let timeout = self.timeout.unwrap_or(Duration::from_secs(60));
// As a side effect, we're cleaning up the killed child process with the implicit call to
// `Child::try_wait` in `self.is_alive`, which reaps the process id on unix systems. We
// always fail with error on timeout if `self.timeout` is set to zero.
while self.is_alive() || timeout == Duration::ZERO {
if start.elapsed() < timeout {
self.delay(10);
} else {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("kill: Timeout of '{}s' reached", timeout.as_secs_f64()),
));
}
hint::spin_loop();
}

Ok(())
}

/// Terminate the child process using custom signal parameter and wait for the termination.
///
/// Ignores any errors happening during [`Child::kill`] (i.e. child process already exited) but
/// still panics on timeout.
///
/// # Panics
/// If the child process could not be terminated within `self.timeout` or the default of 60s.
#[cfg(unix)]
pub fn kill_with_custom_signal(&mut self, signal_name: sys::signal::Signal) -> &mut Self {
self.try_kill_with_custom_signal(signal_name)
.or_else(|error| {
// We still throw the error on timeout in the `try_kill` function
if error.kind() == io::ErrorKind::Other {
Err(error)
} else {
Ok(())
}
})
.unwrap();
self
}

/// Wait for the child process to terminate and return a [`CmdResult`].
///
/// See [`UChild::wait_with_output`] for details on timeouts etc. This method can also be run if
Expand Down

0 comments on commit c16a512

Please sign in to comment.