Skip to content

Commit 6aaf88f

Browse files
committed
tee: add short-read regression test
Upstream commit 9f50c8b ("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.
1 parent b08e880 commit 6aaf88f

1 file changed

Lines changed: 58 additions & 0 deletions

File tree

tests/by-util/test_tee.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,64 @@ fn test_tee_output_not_buffered() {
193193
handle.join().unwrap();
194194
}
195195

196+
#[test]
197+
fn test_tee_continues_after_short_read() {
198+
// Regression test: `tee` must keep reading until EOF even when the
199+
// first `read(2)` returns fewer bytes than its internal buffer. This
200+
// happens in any pipeline where the upstream writer pauses between
201+
// writes (e.g. a slow producer, a `sleep` in a shell pipeline, or a
202+
// service emitting log lines in bursts). Treating a short read as
203+
// end-of-file caused tee to exit prematurely and downstream producers
204+
// to die with SIGPIPE on their next write.
205+
//
206+
// Run in a separate thread so that the test fails via timeout rather
207+
// than hanging if a regression reintroduces the bug in a form where
208+
// tee blocks on read instead of exiting early.
209+
let handle = std::thread::spawn(move || {
210+
let (at, mut ucmd) = at_and_ucmd!();
211+
let file_out = "tee_short_read_out";
212+
213+
let mut child = ucmd
214+
.arg(file_out)
215+
.set_stdin(Stdio::piped())
216+
.set_stdout(Stdio::piped())
217+
.run_no_wait();
218+
219+
// First chunk — deliberately much smaller than tee's internal
220+
// buffer so that `read(2)` returns a short count.
221+
child.write_in(b"first\n");
222+
assert_eq!(&child.stdout_exact_bytes(6), b"first\n");
223+
224+
// Give a buggy implementation time to exit before we try to
225+
// write again.
226+
child.delay(50);
227+
228+
// Second chunk. A correctly-implemented tee is still reading
229+
// from stdin; a buggy one has already exited and this write
230+
// will either fail with EPIPE or never reach the output file.
231+
child.write_in(b"second\n");
232+
assert_eq!(&child.stdout_exact_bytes(7), b"second\n");
233+
234+
// `wait` closes stdin for us before waiting on the child.
235+
child.wait().unwrap().success();
236+
237+
assert_eq!(at.read(file_out), "first\nsecond\n");
238+
});
239+
240+
for _ in 0..500 {
241+
std::thread::sleep(Duration::from_millis(10));
242+
if handle.is_finished() {
243+
break;
244+
}
245+
}
246+
247+
assert!(
248+
handle.is_finished(),
249+
"tee did not complete within the timeout"
250+
);
251+
handle.join().unwrap();
252+
}
253+
196254
#[cfg(all(target_os = "linux", not(wasi_runner)))]
197255
mod linux_only {
198256
use uutests::util::{AtPath, CmdResult, UCommand};

0 commit comments

Comments
 (0)