cli/test: Add broken pipe (SIGPIPE) tests
Add tests that verify the `rad` binary does not panic when its stdout is a broken pipe (e.g., `rad config | head -1`). Rust (since 1.62) ignores SIGPIPE by default (see [Rust #62569]), causing `println!` to panic with "failed printing to stdout: Broken pipe" instead of exiting silently. This is the standard Unix behaviour expected of CLI tools. These tests currently fail on `rad config` and `rad self`, and are set to `ignore`, to be enabled when fixed. [Rust #62569]: https://github.com/rust-lang/rust/issues/62569
This commit is contained in:
parent
7b07e57b9c
commit
8f4b90db7b
|
|
@ -27,6 +27,8 @@ mod commands {
|
|||
mod patch;
|
||||
mod policy;
|
||||
mod remote;
|
||||
#[cfg(unix)]
|
||||
mod sigpipe;
|
||||
mod sync;
|
||||
mod utility;
|
||||
mod watch;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
//! Test that `rad` exits cleanly when its standard output is closed earlier
|
||||
//! before all output is written, i.e., in the case of a ["broken pipe"].
|
||||
//!
|
||||
//! On Unix-like systems, when writing to a pipe whose read end was closed,
|
||||
//! the writing process receives `SIGPIPE`. Often times, this signal
|
||||
//! is not handled by the process, i.e., no signal handler is installed.
|
||||
//! The default behaviour in this case is to terminate the process with an
|
||||
//! exit code of 141 (128 + 13). Such lack of a signal handler is also
|
||||
//! indidcated by `SIG_DFL`.
|
||||
//!
|
||||
//! Rust (since 1.62) ignores `SIGPIPE` by default, see [issue #6529].
|
||||
//! Ignorance in this case means that the signal handler is set to
|
||||
//! `SIG_IGN`. Instead, writes to broken pipes return an
|
||||
//! [`std::io::Error`] of kind [`std::io::ErrorKind::BrokenPipe`].
|
||||
//!
|
||||
//! The [`println!`] macro panics on errors when writing, thus also in case
|
||||
//! [`std::io::ErrorKind::BrokenPipe`] is returned.
|
||||
//!
|
||||
//! We would like to allow users to pipe output from `rad` into other commands,
|
||||
//! which may also stop reading early, such as `head -1` or `less`.
|
||||
//!
|
||||
//! Tests in this module check that the `rad` binary handles broken pipes gracefully:
|
||||
//! It should succeed or fail with an exit code 141,
|
||||
//! and must not panic (or fail with exit code 101, common for panics).
|
||||
//!
|
||||
//! For the definitions of `SIGPIPE`, `SIG_DFL`, and `SIG_IGN`, refer to
|
||||
//! [`signal.h` in POSIX.1-2024].
|
||||
//!
|
||||
//! ["broken pipe"]: https://en.wikipedia.org/wiki/Broken_pipe
|
||||
//! [issue #62569]: https://github.com/rust-lang/rust/issues/62569
|
||||
//! [`signal.h` in POSIX.1-2024]: https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/signal.h.html
|
||||
|
||||
/// A panicking process exits with code 101. A process killed by SIGPIPE
|
||||
/// shows an exit code of 141 (128 + 13). A clean exit is 0.
|
||||
/// Any of these except 101 is acceptable.
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use radicle::profile;
|
||||
|
||||
use crate::util::environment::Environment;
|
||||
|
||||
#[ignore = "test fails"]
|
||||
#[test]
|
||||
fn config() {
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
|
||||
let rad = env!("CARGO_BIN_EXE_rad");
|
||||
|
||||
// Spawn `rad config` with stdout piped so we control it.
|
||||
let mut child = Command::new(rad)
|
||||
.arg("config")
|
||||
.env("RAD_HOME", profile.home.path())
|
||||
.env(profile::env::RAD_PASSPHRASE, "radicle")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to spawn rad");
|
||||
|
||||
let mut stdout = child.stdout.take().unwrap();
|
||||
|
||||
// Read just one byte, then drop stdout to close the pipe.
|
||||
// This simulates `head -1` closing the read end early.
|
||||
let mut buf = [0u8; 1];
|
||||
let _ = stdout.read(&mut buf);
|
||||
drop(stdout);
|
||||
|
||||
let output = child.wait_with_output().expect("failed to wait on rad");
|
||||
|
||||
// Capture stderr for diagnostics.
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// Exit code 101 is Rust's panic exit code — this must not happen.
|
||||
let code = output.status.code();
|
||||
|
||||
assert!(
|
||||
code != Some(101),
|
||||
"rad panicked on broken pipe (exit code 101).\nstderr:\n{stderr}"
|
||||
);
|
||||
|
||||
// Additionally, stderr should not contain panic messages.
|
||||
assert!(
|
||||
!stderr.contains("panicked at"),
|
||||
"rad panicked on broken pipe.\nstderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `rad --help` exercises [`println!`] directly (via clap's help rendering),
|
||||
/// and not just [`radicle_term::Element::print`].
|
||||
#[test]
|
||||
fn help() {
|
||||
let rad = env!("CARGO_BIN_EXE_rad");
|
||||
|
||||
let mut child = Command::new(rad)
|
||||
.arg("--help")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to spawn rad");
|
||||
|
||||
let mut stdout = child.stdout.take().unwrap();
|
||||
|
||||
// Read a single byte and close.
|
||||
let mut buf = [0u8; 1];
|
||||
let _ = stdout.read(&mut buf);
|
||||
drop(stdout);
|
||||
|
||||
let output = child.wait_with_output().expect("failed to wait on rad");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let code = output.status.code();
|
||||
|
||||
assert!(
|
||||
code != Some(101),
|
||||
"rad panicked on broken pipe (exit code 101).\nstderr:\n{stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("panicked at"),
|
||||
"rad panicked on broken pipe.\nstderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `rad self` uses `Element::print()` for table output.
|
||||
#[ignore = "test fails"]
|
||||
#[test]
|
||||
fn rad_self() {
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
|
||||
let rad = env!("CARGO_BIN_EXE_rad");
|
||||
|
||||
let mut child = Command::new(rad)
|
||||
.arg("self")
|
||||
.env("RAD_HOME", profile.home.path())
|
||||
.env(profile::env::RAD_PASSPHRASE, "radicle")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to spawn rad");
|
||||
|
||||
let mut stdout = child.stdout.take().unwrap();
|
||||
let mut buf = [0u8; 1];
|
||||
let _ = stdout.read(&mut buf);
|
||||
drop(stdout);
|
||||
|
||||
let output = child.wait_with_output().expect("failed to wait on rad");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let code = output.status.code();
|
||||
|
||||
assert!(
|
||||
code != Some(101),
|
||||
"rad panicked on broken pipe (exit code 101).\nstderr:\n{stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("panicked at"),
|
||||
"rad panicked on broken pipe.\nstderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue