Files
coreutils/tests/by-util/test_timeout.rs
Chris Dryden 67046f5df7 timeout: add all signal handlers, pass on signals to child, add ignored signal handling (#10254)
There was quite a bunch of different features missing in timeout, to start off, now that we have a mechanism to read the SIGPIPE handlers before they are overwritten by the rust runtime, it means that we can not propagate this signal down to the child processes if the signal is set to ignore.

This also includes all of the latest changes since 9.9 where the specific signal sent to timeout will be propagated instead of just defaulting to a TERM signal.
2026-01-26 20:41:39 +01:00

289 lines
7.1 KiB
Rust

use std::time::Duration;
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore dont
use rstest::rstest;
use uucore::display::Quotable;
use uutests::util::TestScenario;
use uutests::{new_ucmd, util_name};
#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails_with_code(125);
}
#[test]
fn test_subcommand_return_code() {
new_ucmd!().arg("1").arg("true").succeeds();
new_ucmd!().arg("1").arg("false").fails_with_code(1);
}
#[rstest]
#[case::alphabetic("xyz")]
#[case::single_quote("'1")]
fn test_invalid_time_interval(#[case] input: &str) {
new_ucmd!()
.args(&[input, "sleep", "0"])
.fails_with_code(125)
.usage_error(format!("invalid time interval {}", input.quote()));
}
#[test]
fn test_invalid_kill_after() {
new_ucmd!()
.args(&["-k", "xyz", "1", "sleep", "0"])
.fails_with_code(125)
.usage_error("invalid time interval 'xyz'");
}
#[test]
fn test_command_with_args() {
new_ucmd!()
.args(&["1700", "echo", "-n", "abcd"])
.succeeds()
.stdout_only("abcd");
}
#[test]
fn test_verbose() {
for verbose_flag in ["-v", "--verbose"] {
new_ucmd!()
.args(&[verbose_flag, ".1", "sleep", "1"])
.fails()
.stderr_only("timeout: sending signal TERM to command 'sleep'\n");
new_ucmd!()
.args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "1"])
.fails()
.stderr_only("timeout: sending signal 0 to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'\n");
}
}
#[test]
fn test_zero_timeout() {
new_ucmd!()
.args(&["-v", "0", "sleep", ".1"])
.succeeds()
.no_output();
new_ucmd!()
.args(&["-v", "0", "-s0", "-k0", "sleep", ".1"])
.succeeds()
.no_output();
}
#[test]
fn test_command_empty_args() {
new_ucmd!()
.args(&["", ""])
.fails()
.stderr_contains("timeout: invalid time interval ''");
}
#[test]
fn test_foreground() {
for arg in ["-f", "--foreground"] {
new_ucmd!()
.args(&[arg, ".1", "sleep", "10"])
.fails_with_code(124)
.no_output();
}
}
#[test]
fn test_preserve_status() {
for arg in ["-p", "--preserve-status"] {
new_ucmd!()
.args(&[arg, ".1", "sleep", "10"])
// 128 + SIGTERM = 128 + 15
.fails_with_code(128 + 15)
.no_output();
}
}
#[test]
fn test_preserve_status_even_when_send_signal() {
// When sending CONT signal, process doesn't get killed or stopped.
// So, expected result is success and code 0.
for cont_spelling in ["CONT", "cOnT", "SIGcont"] {
new_ucmd!()
.args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "1"])
.succeeds()
.no_output();
}
}
#[test]
fn test_dont_overflow() {
new_ucmd!()
.args(&["9223372036854775808d", "sleep", "0"])
.succeeds()
.no_output();
new_ucmd!()
.args(&["-k", "9223372036854775808d", "10", "sleep", "0"])
.succeeds()
.no_output();
}
#[test]
fn test_dont_underflow() {
new_ucmd!()
.args(&[".0000000001", "sleep", "1"])
.fails_with_code(124)
.no_output();
new_ucmd!()
.args(&["1e-100", "sleep", "1"])
.fails_with_code(124)
.no_output();
// Unlike GNU coreutils, we underflow to 1ns for very short timeouts.
// https://debbugs.gnu.org/cgi/bugreport.cgi?bug=77535
new_ucmd!()
.args(&["1e-18172487393827593258", "sleep", "1"])
.fails_with_code(124)
.no_output();
}
#[test]
fn test_negative_interval() {
new_ucmd!()
.args(&["--", "-1", "sleep", "0"])
.fails()
.usage_error("invalid time interval '-1'");
}
#[test]
fn test_invalid_signal() {
new_ucmd!()
.args(&["-s", "invalid", "1", "sleep", "0"])
.fails()
.usage_error("'invalid': invalid signal");
}
#[test]
fn test_invalid_multi_byte_characters() {
new_ucmd!()
.args(&["10€", "sleep", "0"])
.fails()
.usage_error("invalid time interval '10€'");
}
/// Test that the long form of the `--kill-after` argument is recognized.
#[test]
fn test_kill_after_long() {
new_ucmd!()
.args(&["--kill-after=1", "1", "sleep", "0"])
.succeeds()
.no_output();
}
#[test]
fn test_kill_subprocess() {
new_ucmd!()
.args(&[
// Make sure the CI can spawn the subprocess.
"1",
"sh",
"-c",
"trap 'echo inside_trap' TERM; sleep 5",
])
.fails_with_code(124)
.stdout_contains("inside_trap");
}
#[test]
fn test_hex_timeout_ending_with_d() {
new_ucmd!()
.args(&["0x0.1d", "sleep", "10"])
.timeout(Duration::from_secs(1))
.fails_with_code(124)
.no_output();
}
#[test]
fn test_terminate_child_on_receiving_terminate() {
let mut timeout_cmd = new_ucmd!()
.args(&[
"10",
"sh",
"-c",
"trap 'echo child received TERM' TERM; sleep 5",
])
.run_no_wait();
timeout_cmd.delay(100);
timeout_cmd.kill_with_custom_signal(nix::sys::signal::Signal::SIGTERM);
timeout_cmd
.make_assertion()
.is_not_alive()
.with_current_output()
.code_is(143)
.stdout_contains("child received TERM");
}
#[test]
fn test_command_not_found() {
// Test exit code 127 when command doesn't exist
new_ucmd!()
.args(&["1", "/this/command/definitely/does/not/exist"])
.fails_with_code(127);
}
#[test]
fn test_command_cannot_invoke() {
// Test exit code 126 when command exists but cannot be invoked
// Try to execute a directory (should give permission denied or similar)
new_ucmd!().args(&["1", "/"]).fails_with_code(126);
}
#[test]
#[cfg(unix)]
fn test_sigchld_ignored_by_parent() {
let ts = TestScenario::new(util_name!());
let bin_path = ts.bin_path.to_string_lossy();
ts.ucmd()
.args(&[
"10",
"sh",
"-c",
&format!("trap '' CHLD; exec {bin_path} timeout 1 true"),
])
.succeeds();
}
#[test]
#[cfg(unix)]
fn test_with_background_child() {
new_ucmd!()
.args(&[".5", "sh", "-c", "sleep .1 & sleep 2"])
.fails_with_code(124)
.no_stdout();
}
#[test]
#[cfg(unix)]
fn test_forward_sigint_to_child() {
let mut cmd = new_ucmd!()
.args(&[
"10",
"sh",
"-c",
"trap 'echo got_int; exit 42' INT; sleep 5",
])
.run_no_wait();
cmd.delay(100);
cmd.kill_with_custom_signal(nix::sys::signal::Signal::SIGINT);
cmd.make_assertion()
.is_not_alive()
.with_current_output()
.stdout_contains("got_int");
}
#[test]
fn test_foreground_signal0_kill_after() {
new_ucmd!()
.args(&["--foreground", "-s0", "-k.1", ".1", "sleep", "10"])
.fails_with_code(137);
}