From b17025b663e2dd3f123370ec1f2072df92c559ff Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 12 Apr 2026 18:08:54 -0700 Subject: [PATCH] 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. --- tests/by-util/test_tee.rs | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index f35c19eb0..10370f7e8 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -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};