Handle broken pipe errors on stdout gracefully (#932)

Replace `println!` calls on the results paths with `writeln!` on
`io::stdout()` and propagate the `io::Error` to `main`, where
`ErrorKind::BrokenPipe` is turned into a quiet `exit(0)`. This matches
the conventional behavior of command-line tools when their output pipe
is closed early (e.g. `hyperfine ... | head -n 0`), and fixes panics in
all run-output paths: the per-benchmark header and results, the speed
comparison summary, and `-`-style exports to stdout. The warnings and
"Note" outputs to stderr are converted the same way.


---------

Co-authored-by: Mathjk <Mathjk@users.noreply.github.com>
Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
authored and GitHub committed 2026-10-02 11:18:56 +02:00
1 parent 4d073e08ae
commit c0b3834277
7 files changed
+118 -30

No files matched your search

+42 -1
View File
@@ -1,5 +1,5 @@
mod common;
use common::hyperfine;
use common::{hyperfine, hyperfine_raw_command};
use predicates::prelude::*;
@@ -33,6 +33,47 @@ fn one_run_is_supported() {
.success();
}
/// Regression test: hyperfine must not panic when writing to a closed
/// stdout pipe (e.g. `hyperfine ... | head -n 1` or a downstream process
/// that terminates early). A `BrokenPipe` error should result in a quiet
/// exit with code 0 instead of a `println!` panic.
#[test]
fn exits_quietly_when_stdout_is_closed() {
use std::process::Stdio;
for args in [
vec!["--runs=1", "echo dummy benchmark"],
vec!["--runs=1", "--export-json=-", "echo dummy benchmark"],
vec!["--runs=1", "echo dummy benchmark", "echo second benchmark"],
] {
// A pipe whose read end has been closed before hyperfine is even
// started: every write to it will fail with `BrokenPipe`.
let (reader, writer) = std::io::pipe().expect("failed to create pipe");
drop(reader);
let output = hyperfine_raw_command()
.args(&args)
.stdout(writer)
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn hyperfine")
.wait_with_output()
.expect("failed to wait for hyperfine");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("panicked"),
"hyperfine panicked while writing to a closed pipe:\n{}",
stderr
);
assert!(
output.status.success(),
"hyperfine did not exit cleanly; stderr:\n{}",
stderr
);
}
}
#[test]
fn can_run_commands_without_a_shell() {
hyperfine()