mirror of
https://github.com/uutils/coreutils.git
synced 2026-05-06 07:26:38 -04:00
776 lines
23 KiB
Rust
776 lines
23 KiB
Rust
// This file is part of the uutils coreutils package.
|
|
//
|
|
// For the full copyright and license information, please view the LICENSE
|
|
// file that was distributed with this source code.
|
|
#![allow(clippy::borrow_as_ptr)]
|
|
|
|
use uutests::{at_and_ucmd, new_ucmd};
|
|
|
|
use regex::Regex;
|
|
use std::process::Stdio;
|
|
use std::time::Duration;
|
|
|
|
// tests for basic tee functionality.
|
|
// inspired by:
|
|
// https://github.com/coreutils/coreutils/tests/misc/tee.sh
|
|
|
|
// spell-checker:ignore nopipe
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_error_stdin_directory() {
|
|
new_ucmd!()
|
|
.set_stdin(std::fs::File::open(".").unwrap())
|
|
.fails_with_code(1)
|
|
.stderr_is("tee: read error: Is a directory\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_arg() {
|
|
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_short_help_is_long_help() {
|
|
// I can't believe that this test is necessary.
|
|
let help_short = new_ucmd!()
|
|
.arg("-h")
|
|
.succeeds()
|
|
.no_stderr()
|
|
.stdout_str()
|
|
.to_owned();
|
|
new_ucmd!()
|
|
.arg("--help")
|
|
.succeeds()
|
|
.no_stderr()
|
|
.stdout_is(help_short);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tee_processing_multiple_operands() {
|
|
// POSIX says: "Processing of at least 13 file operands shall be supported."
|
|
|
|
let content = "tee_sample_content";
|
|
for n in [1, 2, 12, 13] {
|
|
let files = (1..=n).map(|x| x.to_string()).collect::<Vec<_>>();
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
ucmd.args(&files)
|
|
.pipe_in(content)
|
|
.succeeds()
|
|
.stdout_is(content);
|
|
|
|
for file in &files {
|
|
assert!(at.file_exists(file));
|
|
assert_eq!(at.read(file), content);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_tee_treat_minus_as_filename() {
|
|
// Ensure tee treats '-' as the name of a file, as mandated by POSIX.
|
|
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let content = "tee_sample_content";
|
|
let file = "-";
|
|
|
|
ucmd.arg("-").pipe_in(content).succeeds().stdout_is(content);
|
|
|
|
assert!(at.file_exists(file));
|
|
assert_eq!(at.read(file), content);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tee_append() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let content = "tee_sample_content";
|
|
let file = "tee_out";
|
|
|
|
at.touch(file);
|
|
at.write(file, content);
|
|
assert_eq!(at.read(file), content);
|
|
|
|
ucmd.arg("-a")
|
|
.arg(file)
|
|
.pipe_in(content)
|
|
.succeeds()
|
|
.stdout_is(content);
|
|
assert!(at.file_exists(file));
|
|
assert_eq!(at.read(file), content.repeat(2));
|
|
}
|
|
|
|
#[test]
|
|
fn test_tee_multiple_append_flags() {
|
|
// Test for bug: https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2134578
|
|
// The command should accept multiple -a flags for different files
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let content = "don't fail me now rust";
|
|
let file1 = "log1";
|
|
let file2 = "log2";
|
|
|
|
// Pre-populate files with some content to verify append behavior
|
|
at.write(file1, "existing1\n");
|
|
at.write(file2, "existing2\n");
|
|
|
|
ucmd.args(&["-a", file1, "-a", file2])
|
|
.pipe_in(content)
|
|
.succeeds()
|
|
.stdout_is(content);
|
|
|
|
assert!(at.file_exists(file1));
|
|
assert!(at.file_exists(file2));
|
|
assert_eq!(at.read(file1), format!("existing1\n{content}"));
|
|
assert_eq!(at.read(file2), format!("existing2\n{content}"));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")]
|
|
fn test_readonly() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let content_tee = "hello";
|
|
let content_file = "world";
|
|
let file_out = "tee_file_out";
|
|
let writable_file = "tee_file_out2";
|
|
at.write(file_out, content_file);
|
|
at.set_readonly(file_out);
|
|
ucmd.arg(file_out)
|
|
.arg(writable_file)
|
|
.pipe_in(content_tee)
|
|
.ignore_stdin_write_error()
|
|
.fails()
|
|
.stdout_is(content_tee)
|
|
// Windows says "Access is denied" for some reason.
|
|
.stderr_matches(&Regex::new("(Permission|Access is) denied").unwrap());
|
|
assert_eq!(at.read(file_out), content_file);
|
|
assert_eq!(at.read(writable_file), content_tee);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg_attr(wasi_runner, ignore = "WASI: no pipe/signal support")]
|
|
fn test_tee_output_not_buffered() {
|
|
// POSIX says: The tee utility shall not buffer output
|
|
|
|
// If the output is buffered, the test will hang, so we run it in
|
|
// a separate thread to stop execution by timeout.
|
|
let handle = std::thread::spawn(move || {
|
|
let content = "a";
|
|
let file_out = "tee_file_out";
|
|
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let mut child = ucmd
|
|
.arg(file_out)
|
|
.set_stdin(Stdio::piped())
|
|
.set_stdout(Stdio::piped())
|
|
.run_no_wait();
|
|
|
|
// We write to the input pipe, but do not close it. If the output is
|
|
// buffered, reading from output pipe will hang indefinitely, as we
|
|
// will never write anything else to it.
|
|
child.write_in(content.as_bytes());
|
|
|
|
let out = String::from_utf8(child.stdout_exact_bytes(1)).unwrap();
|
|
assert_eq!(&out, content);
|
|
|
|
// Writing to a file may take a couple hundreds nanoseconds
|
|
child.delay(1);
|
|
assert_eq!(at.read(file_out), content);
|
|
});
|
|
|
|
// Give some time for the `tee` to create an output file. Some platforms
|
|
// take a lot of time to spin up the process and create the output file
|
|
for _ in 0..100 {
|
|
std::thread::sleep(Duration::from_millis(1));
|
|
if handle.is_finished() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
handle.is_finished(),
|
|
"Nothing was received through output pipe"
|
|
);
|
|
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};
|
|
|
|
use std::fmt::Write;
|
|
use std::fs::File;
|
|
use std::process::Stdio;
|
|
use std::time::Duration;
|
|
use uutests::at_and_ucmd;
|
|
use uutests::new_ucmd;
|
|
|
|
fn make_broken_pipe() -> File {
|
|
let (read, write) = rustix::pipe::pipe().expect("Failed to create pipe");
|
|
// Drop the read end of the pipe
|
|
drop(read);
|
|
// Make the write end of the pipe into a Rust File
|
|
write.into()
|
|
}
|
|
|
|
fn make_hanging_read() -> File {
|
|
let (read, write) = rustix::pipe::pipe().expect("Failed to create pipe");
|
|
// PURPOSELY leak the write end of the pipe, so the read end hangs.
|
|
std::mem::forget(write);
|
|
// Return the read end of the pipe
|
|
read.into()
|
|
}
|
|
|
|
fn run_tee(proc: &mut UCommand) -> (String, CmdResult) {
|
|
let content = (1..=100_000).fold(String::new(), |mut output, x| {
|
|
let _ = writeln!(output, "{x}");
|
|
output
|
|
});
|
|
|
|
let result = proc
|
|
.ignore_stdin_write_error()
|
|
.set_stdin(Stdio::piped())
|
|
.run_no_wait()
|
|
.pipe_in_and_wait(content.as_bytes());
|
|
|
|
(content, result)
|
|
}
|
|
|
|
fn expect_success(result: &CmdResult) {
|
|
assert!(
|
|
result.succeeded(),
|
|
"Command was expected to succeed.\nstdout = {}\n stderr = {}",
|
|
std::str::from_utf8(result.stdout()).unwrap(),
|
|
std::str::from_utf8(result.stderr()).unwrap(),
|
|
);
|
|
assert!(
|
|
result.stderr_str().is_empty(),
|
|
"Unexpected data on stderr.\n stderr = {}",
|
|
std::str::from_utf8(result.stderr()).unwrap(),
|
|
);
|
|
}
|
|
|
|
fn expect_failure(result: &CmdResult, message: &str) {
|
|
assert!(
|
|
!result.succeeded(),
|
|
"Command was expected to fail.\nstdout = {}\n stderr = {}",
|
|
std::str::from_utf8(result.stdout()).unwrap(),
|
|
std::str::from_utf8(result.stderr()).unwrap(),
|
|
);
|
|
assert!(
|
|
result.stderr_str().contains(message),
|
|
"Expected to see error message fragment {message} in stderr, but did not.\n stderr = {}",
|
|
std::str::from_utf8(result.stderr()).unwrap(),
|
|
);
|
|
}
|
|
|
|
fn expect_silent_failure(result: &CmdResult) {
|
|
assert!(
|
|
!result.succeeded(),
|
|
"Command was expected to fail.\nstdout = {}\n stderr = {}",
|
|
std::str::from_utf8(result.stdout()).unwrap(),
|
|
std::str::from_utf8(result.stderr()).unwrap(),
|
|
);
|
|
assert!(
|
|
result.stderr_str().is_empty(),
|
|
"Unexpected data on stderr.\n stderr = {}",
|
|
std::str::from_utf8(result.stderr()).unwrap(),
|
|
);
|
|
}
|
|
|
|
fn expect_correct(name: &str, at: &AtPath, contents: &str) {
|
|
assert!(at.file_exists(name));
|
|
let compare = at.read(name);
|
|
assert_eq!(compare, contents);
|
|
}
|
|
|
|
fn expect_short(name: &str, at: &AtPath, contents: &str) {
|
|
assert!(at.file_exists(name));
|
|
let compare = at.read(name);
|
|
assert!(
|
|
compare.len() < contents.len(),
|
|
"Too many bytes ({}) written to {name} (should be a short count from {})",
|
|
compare.len(),
|
|
contents.len()
|
|
);
|
|
assert!(
|
|
contents.starts_with(&compare),
|
|
"Expected truncated output to be a prefix of the correct output, but it isn't.\n Correct: {contents}\n Compare: {compare}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_default() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd.arg(file_out_a).set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_silent_failure(&output);
|
|
expect_short(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_warn_nopipe_1() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("-p")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_success(&output);
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_warn_nopipe_2() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_success(&output);
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_warn_nopipe_3() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=warn-nopipe")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_success(&output);
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_warn_nopipe_3_shortcut() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=warn-")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_success(&output);
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_warn() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=warn")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "Broken pipe");
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_exit() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=exit")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "Broken pipe");
|
|
expect_short(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_exit_nopipe() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=exit-nopipe")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_success(&output);
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_error_exit_nopipe_shortcut() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=exit-nop")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_success(&output);
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_space_error_default() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd.arg(file_out_a).arg("/dev/full");
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "No space left");
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_space_error_warn_nopipe_1() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("-p")
|
|
.arg(file_out_a)
|
|
.arg("/dev/full")
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "No space left");
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_space_error_warn_nopipe_2() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error")
|
|
.arg(file_out_a)
|
|
.arg("/dev/full")
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "No space left");
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_space_error_warn_nopipe_3() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=warn-nopipe")
|
|
.arg(file_out_a)
|
|
.arg("/dev/full")
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "No space left");
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_space_error_warn() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=warn")
|
|
.arg(file_out_a)
|
|
.arg("/dev/full")
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "No space left");
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_space_error_exit() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=exit")
|
|
.arg(file_out_a)
|
|
.arg("/dev/full")
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "No space left");
|
|
expect_short(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_space_error_exit_nopipe() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("--output-error=exit-nopipe")
|
|
.arg(file_out_a)
|
|
.arg("/dev/full")
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_failure(&output, "No space left");
|
|
expect_short(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_mode_broken_pipe_only() {
|
|
new_ucmd!()
|
|
.timeout(Duration::from_secs(1))
|
|
.arg("-p")
|
|
.set_stdin(make_hanging_read())
|
|
.set_stdout(make_broken_pipe())
|
|
.succeeds();
|
|
}
|
|
|
|
#[test]
|
|
fn test_pipe_mode_broken_pipe_file() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let file_out_a = "tee_file_out_a";
|
|
|
|
let proc = ucmd
|
|
.arg("-p")
|
|
.arg(file_out_a)
|
|
.set_stdout(make_broken_pipe());
|
|
|
|
let (content, output) = run_tee(proc);
|
|
|
|
expect_success(&output);
|
|
expect_correct(file_out_a, &at, content.as_str());
|
|
}
|
|
|
|
#[test]
|
|
fn test_tee_no_more_writeable_1() {
|
|
// equals to 'tee /dev/full out2 <multi_read' call
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let content = (1..=10).fold(String::new(), |mut output, x| {
|
|
writeln!(output, "{x}").unwrap();
|
|
output
|
|
});
|
|
let file_out = "tee_file_out";
|
|
|
|
ucmd.arg("/dev/full")
|
|
.arg(file_out)
|
|
.pipe_in(&content[..])
|
|
.fails()
|
|
.stdout_contains(&content)
|
|
.stderr_contains("No space left on device");
|
|
|
|
assert_eq!(at.read(file_out), content);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tee_no_more_writeable_2() {
|
|
use std::fs::File;
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let content = (1..=10).fold(String::new(), |mut output, x| {
|
|
let _ = writeln!(output, "{x}");
|
|
output
|
|
});
|
|
let file_out_a = "tee_file_out_a";
|
|
let file_out_b = "tee_file_out_b";
|
|
let dev_full = File::options().append(true).open("/dev/full").unwrap();
|
|
|
|
let result = ucmd
|
|
.arg(file_out_a)
|
|
.arg(file_out_b)
|
|
.set_stdout(dev_full)
|
|
.pipe_in(content.as_bytes())
|
|
.fails();
|
|
|
|
assert_eq!(at.read(file_out_a), content);
|
|
assert_eq!(at.read(file_out_b), content);
|
|
assert!(result.stderr_str().contains("No space left on device"));
|
|
}
|
|
}
|
|
|
|
// Additional cross-platform tee tests to cover GNU compatibility around --output-error
|
|
#[test]
|
|
fn test_output_error_flag_without_value_defaults_warn_nopipe() {
|
|
// When --output-error is present without an explicit value, it should default to warn-nopipe
|
|
// We can't easily simulate a broken pipe across all platforms here, but we can ensure
|
|
// the flag is accepted without error and basic tee functionality still works.
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let file_out = "tee_output_error_default.txt";
|
|
let content = "abc";
|
|
|
|
let result = ucmd
|
|
.arg("--output-error")
|
|
.arg(file_out)
|
|
.pipe_in(content)
|
|
.succeeds();
|
|
|
|
result.stdout_is(content);
|
|
assert!(at.file_exists(file_out));
|
|
assert_eq!(at.read(file_out), content);
|
|
}
|
|
// Unix-only: presence-only --output-error should not crash on broken pipe.
|
|
// Current implementation may exit zero; we only assert the process exits to avoid flakiness.
|
|
// TODO: When semantics are aligned with GNU warn-nopipe, strengthen assertions here.
|
|
#[cfg(all(unix, not(target_os = "freebsd")))]
|
|
#[test]
|
|
fn test_output_error_presence_only_broken_pipe_unix() {
|
|
let (read, write) = rustix::pipe::pipe().expect("Failed to create pipe");
|
|
// Close the read end to simulate a broken pipe on stdout
|
|
drop(read);
|
|
let write: std::fs::File = write.into();
|
|
let content = (0..10_000).map(|_| "x").collect::<String>();
|
|
let result = new_ucmd!()
|
|
.arg("--output-error") // presence-only flag
|
|
.set_stdout(write)
|
|
.pipe_in(content.as_bytes())
|
|
.run();
|
|
|
|
// Assert that a status was produced (i.e., process exited) and no crash occurred.
|
|
assert!(result.try_exit_status().is_some(), "process did not exit");
|
|
}
|
|
|
|
// Skip on FreeBSD due to repeated CI hangs in FreeBSD VM (see PR #8684)
|
|
#[cfg(all(unix, not(target_os = "freebsd")))]
|
|
#[test]
|
|
fn test_broken_pipe_early_termination_stdout_only() {
|
|
let (read, write) = rustix::pipe::pipe().expect("Failed to create pipe");
|
|
// Create a broken stdout
|
|
drop(read);
|
|
let write: std::fs::File = write.into();
|
|
let content = (0..10_000).map(|_| "x").collect::<String>();
|
|
let mut proc = new_ucmd!();
|
|
let result = proc
|
|
.set_stdout(write)
|
|
.ignore_stdin_write_error()
|
|
.pipe_in(content.as_bytes())
|
|
.run();
|
|
// GNU tee exits nonzero on broken pipe unless configured otherwise; implementation
|
|
// details vary by mode, but we should not panic and should return an exit status.
|
|
// Assert that a status was produced (i.e., process exited) and no crash occurred.
|
|
assert!(result.try_exit_status().is_some(), "process did not exit");
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_failure_reports_error_and_nonzero_exit() {
|
|
// Simulate a file open failure which should be reported via show_error and cause a failure
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
// Create a directory and try to use it as an output file (open will fail)
|
|
at.mkdir("out_dir");
|
|
|
|
let result = ucmd.arg("out_dir").pipe_in("data").fails();
|
|
|
|
assert!(!result.stderr_str().is_empty());
|
|
}
|