tee: add short-read regression test

Upstream commit 9f50c8b42 ("tee: fix input with sleep") already
contains the functional fix for short reads from a paused writer.
Keep the regression coverage so we do not reintroduce the bug.

The test writes one small chunk, waits long enough for a buggy tee to
exit, then writes a second chunk and asserts both stdout and the output
file receive both writes.
This commit is contained in:
Kevin Burke
2026-04-13 07:37:50 +02:00
committed by Sylvestre Ledru
parent b08e880c1b
commit b17025b663
+58
View File
@@ -193,6 +193,64 @@ fn test_tee_output_not_buffered() {
handle.join().unwrap();
}
#[test]
fn test_tee_continues_after_short_read() {
// Regression test: `tee` must keep reading until EOF even when the
// first `read(2)` returns fewer bytes than its internal buffer. This
// happens in any pipeline where the upstream writer pauses between
// writes (e.g. a slow producer, a `sleep` in a shell pipeline, or a
// service emitting log lines in bursts). Treating a short read as
// end-of-file caused tee to exit prematurely and downstream producers
// to die with SIGPIPE on their next write.
//
// Run in a separate thread so that the test fails via timeout rather
// than hanging if a regression reintroduces the bug in a form where
// tee blocks on read instead of exiting early.
let handle = std::thread::spawn(move || {
let (at, mut ucmd) = at_and_ucmd!();
let file_out = "tee_short_read_out";
let mut child = ucmd
.arg(file_out)
.set_stdin(Stdio::piped())
.set_stdout(Stdio::piped())
.run_no_wait();
// First chunk — deliberately much smaller than tee's internal
// buffer so that `read(2)` returns a short count.
child.write_in(b"first\n");
assert_eq!(&child.stdout_exact_bytes(6), b"first\n");
// Give a buggy implementation time to exit before we try to
// write again.
child.delay(50);
// Second chunk. A correctly-implemented tee is still reading
// from stdin; a buggy one has already exited and this write
// will either fail with EPIPE or never reach the output file.
child.write_in(b"second\n");
assert_eq!(&child.stdout_exact_bytes(7), b"second\n");
// `wait` closes stdin for us before waiting on the child.
child.wait().unwrap().success();
assert_eq!(at.read(file_out), "first\nsecond\n");
});
for _ in 0..500 {
std::thread::sleep(Duration::from_millis(10));
if handle.is_finished() {
break;
}
}
assert!(
handle.is_finished(),
"tee did not complete within the timeout"
);
handle.join().unwrap();
}
#[cfg(all(target_os = "linux", not(wasi_runner)))]
mod linux_only {
use uutests::util::{AtPath, CmdResult, UCommand};