mirror of
https://github.com/uutils/coreutils
synced 2024-12-13 14:52:41 +00:00
90a0226844
* Fix a timing related bug with polling (---disable-inotify) where some Events weren't delivered fast enough by `Notify::PollWatcher` to pass all of tests/tail-2/retry.sh and test_tail::{test_retry4, retry7}. * uu_tail now reverts to polling automatically if inotify backend reports too many open files (this mimics the behavior of GNU's tail).
1465 lines
43 KiB
Rust
1465 lines
43 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.
|
|
|
|
// spell-checker:ignore (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile logfile bogusfile siette ocho nueve diez
|
|
// spell-checker:ignore (libs) kqueue
|
|
// spell-checker:ignore (jargon) tailable untailable
|
|
|
|
extern crate tail;
|
|
|
|
use crate::common::util::*;
|
|
use std::char::from_digit;
|
|
use std::io::{Read, Write};
|
|
#[cfg(unix)]
|
|
use std::thread::sleep;
|
|
#[cfg(unix)]
|
|
use std::time::Duration;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub static BACKEND: &str = "inotify";
|
|
#[cfg(all(unix, not(target_os = "linux")))]
|
|
pub static BACKEND: &str = "kqueue";
|
|
|
|
static FOOBAR_TXT: &str = "foobar.txt";
|
|
static FOOBAR_2_TXT: &str = "foobar2.txt";
|
|
static FOOBAR_WITH_NULL_TXT: &str = "foobar_with_null.txt";
|
|
#[cfg(unix)]
|
|
static FOLLOW_NAME_TXT: &str = "follow_name.txt";
|
|
#[cfg(unix)]
|
|
static FOLLOW_NAME_SHORT_EXP: &str = "follow_name_short.expected";
|
|
#[cfg(target_os = "linux")]
|
|
static FOLLOW_NAME_EXP: &str = "follow_name.expected";
|
|
|
|
#[test]
|
|
fn test_stdin_default() {
|
|
new_ucmd!()
|
|
.pipe_in_fixture(FOOBAR_TXT)
|
|
.run()
|
|
.stdout_is_fixture("foobar_stdin_default.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_stdin_explicit() {
|
|
new_ucmd!()
|
|
.pipe_in_fixture(FOOBAR_TXT)
|
|
.arg("-")
|
|
.run()
|
|
.stdout_is_fixture("foobar_stdin_default.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_default() {
|
|
new_ucmd!()
|
|
.arg(FOOBAR_TXT)
|
|
.run()
|
|
.stdout_is_fixture("foobar_single_default.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_n_greater_than_number_of_lines() {
|
|
new_ucmd!()
|
|
.arg("-n")
|
|
.arg("99999999")
|
|
.arg(FOOBAR_TXT)
|
|
.run()
|
|
.stdout_is_fixture(FOOBAR_TXT);
|
|
}
|
|
|
|
#[test]
|
|
fn test_null_default() {
|
|
new_ucmd!()
|
|
.arg("-z")
|
|
.arg(FOOBAR_WITH_NULL_TXT)
|
|
.run()
|
|
.stdout_is_fixture("foobar_with_null_default.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_follow_single() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let mut child = ucmd.arg("-f").arg(FOOBAR_TXT).run_no_wait();
|
|
|
|
let expected = at.read("foobar_single_default.expected");
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
// We write in a temporary copy of foobar.txt
|
|
let expected = "line1\nline2\n";
|
|
at.append(FOOBAR_TXT, expected);
|
|
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
child.kill().unwrap();
|
|
}
|
|
|
|
/// Test for following when bytes are written that are not valid UTF-8.
|
|
#[test]
|
|
fn test_follow_non_utf8_bytes() {
|
|
// Tail the test file and start following it.
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let mut child = ucmd.arg("-f").arg(FOOBAR_TXT).run_no_wait();
|
|
let expected = at.read("foobar_single_default.expected");
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
// Now append some bytes that are not valid UTF-8.
|
|
//
|
|
// The binary integer "10000000" is *not* a valid UTF-8 encoding
|
|
// of a character: https://en.wikipedia.org/wiki/UTF-8#Encoding
|
|
//
|
|
// We also write the newline character because our implementation
|
|
// of `tail` is attempting to read a line of input, so the
|
|
// presence of a newline character will force the `follow()`
|
|
// function to conclude reading input bytes and start writing them
|
|
// to output. The newline character is not fundamental to this
|
|
// test, it is just a requirement of the current implementation.
|
|
let expected = [0b10000000, b'\n'];
|
|
at.append_bytes(FOOBAR_TXT, &expected);
|
|
let actual = read_size_bytes(&mut child, expected.len());
|
|
assert_eq!(actual, expected.to_vec());
|
|
|
|
child.kill().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_follow_multiple() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let mut child = ucmd
|
|
.arg("-f")
|
|
.arg(FOOBAR_TXT)
|
|
.arg(FOOBAR_2_TXT)
|
|
.run_no_wait();
|
|
|
|
let expected = at.read("foobar_follow_multiple.expected");
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
let first_append = "trois\n";
|
|
at.append(FOOBAR_2_TXT, first_append);
|
|
assert_eq!(read_size(&mut child, first_append.len()), first_append);
|
|
|
|
let second_append = "twenty\nthirty\n";
|
|
let expected = at.read("foobar_follow_multiple_appended.expected");
|
|
at.append(FOOBAR_TXT, second_append);
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
child.kill().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_follow_name_multiple() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
let mut child = ucmd
|
|
.arg("--follow=name")
|
|
.arg(FOOBAR_TXT)
|
|
.arg(FOOBAR_2_TXT)
|
|
.run_no_wait();
|
|
|
|
let expected = at.read("foobar_follow_multiple.expected");
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
let first_append = "trois\n";
|
|
at.append(FOOBAR_2_TXT, first_append);
|
|
assert_eq!(read_size(&mut child, first_append.len()), first_append);
|
|
|
|
let second_append = "twenty\nthirty\n";
|
|
let expected = at.read("foobar_follow_multiple_appended.expected");
|
|
at.append(FOOBAR_TXT, second_append);
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
child.kill().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_follow_stdin() {
|
|
new_ucmd!()
|
|
.arg("-f")
|
|
.pipe_in_fixture(FOOBAR_TXT)
|
|
.run()
|
|
.stdout_is_fixture("follow_stdin.expected");
|
|
}
|
|
|
|
// FixME: test PASSES for usual windows builds, but fails for coverage testing builds (likely related to the specific RUSTFLAGS '-Zpanic_abort_tests -Cpanic=abort') This test also breaks tty settings under bash requiring a 'stty sane' or reset. // spell-checker:disable-line
|
|
#[cfg(disable_until_fixed)]
|
|
#[test]
|
|
fn test_follow_with_pid() {
|
|
use std::process::{Command, Stdio};
|
|
use std::thread::sleep;
|
|
use std::time::Duration;
|
|
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
#[cfg(unix)]
|
|
let dummy_cmd = "sh";
|
|
#[cfg(windows)]
|
|
let dummy_cmd = "cmd";
|
|
|
|
let mut dummy = Command::new(dummy_cmd)
|
|
.stdout(Stdio::null())
|
|
.spawn()
|
|
.unwrap();
|
|
let pid = dummy.id();
|
|
|
|
let mut child = ucmd
|
|
.arg("-f")
|
|
.arg(format!("--pid={}", pid))
|
|
.arg(FOOBAR_TXT)
|
|
.arg(FOOBAR_2_TXT)
|
|
.run_no_wait();
|
|
|
|
let expected = at.read("foobar_follow_multiple.expected");
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
let first_append = "trois\n";
|
|
at.append(FOOBAR_2_TXT, first_append);
|
|
assert_eq!(read_size(&mut child, first_append.len()), first_append);
|
|
|
|
let second_append = "twenty\nthirty\n";
|
|
let expected = at.read("foobar_follow_multiple_appended.expected");
|
|
at.append(FOOBAR_TXT, second_append);
|
|
assert_eq!(read_size(&mut child, expected.len()), expected);
|
|
|
|
// kill the dummy process and give tail time to notice this
|
|
dummy.kill().unwrap();
|
|
let _ = dummy.wait();
|
|
sleep(Duration::from_secs(1));
|
|
|
|
let third_append = "should\nbe\nignored\n";
|
|
at.append(FOOBAR_TXT, third_append);
|
|
assert_eq!(read_size(&mut child, 1), "\u{0}");
|
|
|
|
// On Unix, trying to kill a process that's already dead is fine; on Windows it's an error.
|
|
#[cfg(unix)]
|
|
child.kill().unwrap();
|
|
#[cfg(windows)]
|
|
assert_eq!(child.kill().is_err(), true);
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_big_args() {
|
|
const FILE: &str = "single_big_args.txt";
|
|
const EXPECTED_FILE: &str = "single_big_args_expected.txt";
|
|
const LINES: usize = 1_000_000;
|
|
const N_ARG: usize = 100_000;
|
|
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let mut big_input = at.make_file(FILE);
|
|
for i in 0..LINES {
|
|
writeln!(big_input, "Line {}", i).expect("Could not write to FILE");
|
|
}
|
|
big_input.flush().expect("Could not flush FILE");
|
|
|
|
let mut big_expected = at.make_file(EXPECTED_FILE);
|
|
for i in (LINES - N_ARG)..LINES {
|
|
writeln!(big_expected, "Line {}", i).expect("Could not write to EXPECTED_FILE");
|
|
}
|
|
big_expected.flush().expect("Could not flush EXPECTED_FILE");
|
|
|
|
ucmd.arg(FILE)
|
|
.arg("-n")
|
|
.arg(format!("{}", N_ARG))
|
|
.run()
|
|
.stdout_is(at.read(EXPECTED_FILE));
|
|
}
|
|
|
|
#[test]
|
|
fn test_bytes_single() {
|
|
new_ucmd!()
|
|
.arg("-c")
|
|
.arg("10")
|
|
.arg(FOOBAR_TXT)
|
|
.run()
|
|
.stdout_is_fixture("foobar_bytes_single.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_bytes_stdin() {
|
|
new_ucmd!()
|
|
.arg("-c")
|
|
.arg("13")
|
|
.pipe_in_fixture(FOOBAR_TXT)
|
|
.run()
|
|
.stdout_is_fixture("foobar_bytes_stdin.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_bytes_big() {
|
|
const FILE: &str = "test_bytes_big.txt";
|
|
const EXPECTED_FILE: &str = "test_bytes_big_expected.txt";
|
|
const BYTES: usize = 1_000_000;
|
|
const N_ARG: usize = 100_000;
|
|
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let mut big_input = at.make_file(FILE);
|
|
for i in 0..BYTES {
|
|
let digit = from_digit((i % 10) as u32, 10).unwrap();
|
|
write!(big_input, "{}", digit).expect("Could not write to FILE");
|
|
}
|
|
big_input.flush().expect("Could not flush FILE");
|
|
|
|
let mut big_expected = at.make_file(EXPECTED_FILE);
|
|
for i in (BYTES - N_ARG)..BYTES {
|
|
let digit = from_digit((i % 10) as u32, 10).unwrap();
|
|
write!(big_expected, "{}", digit).expect("Could not write to EXPECTED_FILE");
|
|
}
|
|
big_expected.flush().expect("Could not flush EXPECTED_FILE");
|
|
|
|
let result = ucmd
|
|
.arg(FILE)
|
|
.arg("-c")
|
|
.arg(format!("{}", N_ARG))
|
|
.succeeds()
|
|
.stdout_move_str();
|
|
let expected = at.read(EXPECTED_FILE);
|
|
|
|
assert_eq!(result.len(), expected.len());
|
|
for (actual_char, expected_char) in result.chars().zip(expected.chars()) {
|
|
assert_eq!(actual_char, expected_char);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_lines_with_size_suffix() {
|
|
const FILE: &str = "test_lines_with_size_suffix.txt";
|
|
const EXPECTED_FILE: &str = "test_lines_with_size_suffix_expected.txt";
|
|
const LINES: usize = 3_000;
|
|
const N_ARG: usize = 2 * 1024;
|
|
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
let mut big_input = at.make_file(FILE);
|
|
for i in 0..LINES {
|
|
writeln!(big_input, "Line {}", i).expect("Could not write to FILE");
|
|
}
|
|
big_input.flush().expect("Could not flush FILE");
|
|
|
|
let mut big_expected = at.make_file(EXPECTED_FILE);
|
|
for i in (LINES - N_ARG)..LINES {
|
|
writeln!(big_expected, "Line {}", i).expect("Could not write to EXPECTED_FILE");
|
|
}
|
|
big_expected.flush().expect("Could not flush EXPECTED_FILE");
|
|
|
|
ucmd.arg(FILE)
|
|
.arg("-n")
|
|
.arg("2K")
|
|
.run()
|
|
.stdout_is_fixture(EXPECTED_FILE);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_input_files() {
|
|
new_ucmd!()
|
|
.arg(FOOBAR_TXT)
|
|
.arg(FOOBAR_2_TXT)
|
|
.run()
|
|
.stdout_is_fixture("foobar_follow_multiple.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_input_files_missing() {
|
|
new_ucmd!()
|
|
.arg(FOOBAR_TXT)
|
|
.arg("missing1")
|
|
.arg(FOOBAR_2_TXT)
|
|
.arg("missing2")
|
|
.run()
|
|
.stdout_is_fixture("foobar_follow_multiple.expected")
|
|
.stderr_is(
|
|
"tail: cannot open 'missing1' for reading: No such file or directory\n\
|
|
tail: cannot open 'missing2' for reading: No such file or directory",
|
|
)
|
|
.code_is(1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_follow_missing() {
|
|
// Ensure that --follow=name does not imply --retry.
|
|
// Ensure that --follow={descriptor,name} (without --retry) does *not wait* for the
|
|
// file to appear.
|
|
for follow_mode in &["--follow=descriptor", "--follow=name"] {
|
|
new_ucmd!()
|
|
.arg(follow_mode)
|
|
.arg("missing")
|
|
.run()
|
|
.stderr_is(
|
|
"tail: cannot open 'missing' for reading: No such file or directory\n\
|
|
tail: no files remaining",
|
|
)
|
|
.code_is(1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_follow_name_stdin() {
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
at.touch("FILE1");
|
|
at.touch("FILE2");
|
|
ts.ucmd()
|
|
.arg("--follow=name")
|
|
.arg("-")
|
|
.run()
|
|
.stderr_is("tail: cannot follow '-' by name")
|
|
.code_is(1);
|
|
ts.ucmd()
|
|
.arg("--follow=name")
|
|
.arg("FILE1")
|
|
.arg("-")
|
|
.arg("FILE2")
|
|
.run()
|
|
.stderr_is("tail: cannot follow '-' by name")
|
|
.code_is(1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_input_files_with_suppressed_headers() {
|
|
new_ucmd!()
|
|
.arg(FOOBAR_TXT)
|
|
.arg(FOOBAR_2_TXT)
|
|
.arg("-q")
|
|
.run()
|
|
.stdout_is_fixture("foobar_multiple_quiet.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_input_quiet_flag_overrides_verbose_flag_for_suppressing_headers() {
|
|
new_ucmd!()
|
|
.arg(FOOBAR_TXT)
|
|
.arg(FOOBAR_2_TXT)
|
|
.arg("-v")
|
|
.arg("-q")
|
|
.run()
|
|
.stdout_is_fixture("foobar_multiple_quiet.expected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_dir() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
at.mkdir("DIR");
|
|
ucmd.arg("DIR")
|
|
.run()
|
|
.stderr_is("tail: error reading 'DIR': Is a directory\n")
|
|
.code_is(1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dir_follow() {
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
at.mkdir("DIR");
|
|
for mode in &["--follow=descriptor", "--follow=name"] {
|
|
ts.ucmd()
|
|
.arg(mode)
|
|
.arg("DIR")
|
|
.run()
|
|
.stderr_is(
|
|
"tail: error reading 'DIR': Is a directory\n\
|
|
tail: DIR: cannot follow end of this type of file; giving up on this name\n\
|
|
tail: no files remaining\n",
|
|
)
|
|
.code_is(1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_dir_follow_retry() {
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
at.mkdir("DIR");
|
|
ts.ucmd()
|
|
.arg("--follow=descriptor")
|
|
.arg("--retry")
|
|
.arg("DIR")
|
|
.run()
|
|
.stderr_is(
|
|
"tail: warning: --retry only effective for the initial open\n\
|
|
tail: error reading 'DIR': Is a directory\n\
|
|
tail: DIR: cannot follow end of this type of file\n\
|
|
tail: no files remaining\n",
|
|
)
|
|
.code_is(1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_negative_indexing() {
|
|
let positive_lines_index = new_ucmd!().arg("-n").arg("5").arg(FOOBAR_TXT).run();
|
|
|
|
let negative_lines_index = new_ucmd!().arg("-n").arg("-5").arg(FOOBAR_TXT).run();
|
|
|
|
let positive_bytes_index = new_ucmd!().arg("-c").arg("20").arg(FOOBAR_TXT).run();
|
|
|
|
let negative_bytes_index = new_ucmd!().arg("-c").arg("-20").arg(FOOBAR_TXT).run();
|
|
|
|
assert_eq!(positive_lines_index.stdout(), negative_lines_index.stdout());
|
|
assert_eq!(positive_bytes_index.stdout(), negative_bytes_index.stdout());
|
|
}
|
|
|
|
#[test]
|
|
fn test_sleep_interval() {
|
|
new_ucmd!().arg("-s").arg("10").arg(FOOBAR_TXT).succeeds();
|
|
new_ucmd!().arg("-s").arg(".1").arg(FOOBAR_TXT).succeeds();
|
|
new_ucmd!().arg("-s.1").arg(FOOBAR_TXT).succeeds();
|
|
new_ucmd!().arg("-s").arg("-1").arg(FOOBAR_TXT).fails();
|
|
new_ucmd!()
|
|
.arg("-s")
|
|
.arg("1..1")
|
|
.arg(FOOBAR_TXT)
|
|
.fails()
|
|
.stderr_contains("invalid number of seconds: '1..1'")
|
|
.code_is(1);
|
|
}
|
|
|
|
/// Test for reading all but the first NUM bytes: `tail -c +3`.
|
|
#[test]
|
|
fn test_positive_bytes() {
|
|
new_ucmd!()
|
|
.args(&["-c", "+3"])
|
|
.pipe_in("abcde")
|
|
.succeeds()
|
|
.stdout_is("cde");
|
|
}
|
|
|
|
/// Test for reading all bytes, specified by `tail -c +0`.
|
|
#[test]
|
|
fn test_positive_zero_bytes() {
|
|
new_ucmd!()
|
|
.args(&["-c", "+0"])
|
|
.pipe_in("abcde")
|
|
.succeeds()
|
|
.stdout_is("abcde");
|
|
}
|
|
|
|
/// Test for reading all but the first NUM lines: `tail -n +3`.
|
|
#[test]
|
|
fn test_positive_lines() {
|
|
new_ucmd!()
|
|
.args(&["-n", "+3"])
|
|
.pipe_in("a\nb\nc\nd\ne\n")
|
|
.succeeds()
|
|
.stdout_is("c\nd\ne\n");
|
|
}
|
|
|
|
/// Test for reading all but the first NUM lines of a file: `tail -n +3 infile`.
|
|
#[test]
|
|
fn test_positive_lines_file() {
|
|
new_ucmd!()
|
|
.args(&["-n", "+7", "foobar.txt"])
|
|
.succeeds()
|
|
.stdout_is(
|
|
"siette
|
|
ocho
|
|
nueve
|
|
diez
|
|
once
|
|
",
|
|
);
|
|
}
|
|
|
|
/// Test for reading all but the first NUM bytes of a file: `tail -c +3 infile`.
|
|
#[test]
|
|
fn test_positive_bytes_file() {
|
|
new_ucmd!()
|
|
.args(&["-c", "+42", "foobar.txt"])
|
|
.succeeds()
|
|
.stdout_is(
|
|
"ho
|
|
nueve
|
|
diez
|
|
once
|
|
",
|
|
);
|
|
}
|
|
|
|
/// Test for reading all but the first NUM lines: `tail -3`.
|
|
#[test]
|
|
fn test_obsolete_syntax_positive_lines() {
|
|
new_ucmd!()
|
|
.args(&["-3"])
|
|
.pipe_in("a\nb\nc\nd\ne\n")
|
|
.succeeds()
|
|
.stdout_is("c\nd\ne\n");
|
|
}
|
|
|
|
/// Test for reading all but the first NUM lines: `tail -n -10`.
|
|
#[test]
|
|
fn test_small_file() {
|
|
new_ucmd!()
|
|
.args(&["-n -10"])
|
|
.pipe_in("a\nb\nc\nd\ne\n")
|
|
.succeeds()
|
|
.stdout_is("a\nb\nc\nd\ne\n");
|
|
}
|
|
|
|
/// Test for reading all but the first NUM lines: `tail -10`.
|
|
#[test]
|
|
fn test_obsolete_syntax_small_file() {
|
|
new_ucmd!()
|
|
.args(&["-10"])
|
|
.pipe_in("a\nb\nc\nd\ne\n")
|
|
.succeeds()
|
|
.stdout_is("a\nb\nc\nd\ne\n");
|
|
}
|
|
|
|
/// Test for reading all lines, specified by `tail -n +0`.
|
|
#[test]
|
|
fn test_positive_zero_lines() {
|
|
new_ucmd!()
|
|
.args(&["-n", "+0"])
|
|
.pipe_in("a\nb\nc\nd\ne\n")
|
|
.succeeds()
|
|
.stdout_is("a\nb\nc\nd\ne\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_num() {
|
|
new_ucmd!()
|
|
.args(&["-c", "1024R", "emptyfile.txt"])
|
|
.fails()
|
|
.stderr_is("tail: invalid number of bytes: '1024R'");
|
|
new_ucmd!()
|
|
.args(&["-n", "1024R", "emptyfile.txt"])
|
|
.fails()
|
|
.stderr_is("tail: invalid number of lines: '1024R'");
|
|
#[cfg(not(target_pointer_width = "128"))]
|
|
new_ucmd!()
|
|
.args(&["-c", "1Y", "emptyfile.txt"])
|
|
.fails()
|
|
.stderr_is("tail: invalid number of bytes: '1Y': Value too large for defined data type");
|
|
#[cfg(not(target_pointer_width = "128"))]
|
|
new_ucmd!()
|
|
.args(&["-n", "1Y", "emptyfile.txt"])
|
|
.fails()
|
|
.stderr_is("tail: invalid number of lines: '1Y': Value too large for defined data type");
|
|
#[cfg(target_pointer_width = "32")]
|
|
{
|
|
let sizes = ["1000G", "10T"];
|
|
for size in &sizes {
|
|
new_ucmd!()
|
|
.args(&["-c", size])
|
|
.fails()
|
|
.code_is(1)
|
|
.stderr_only("tail: Insufficient addressable memory");
|
|
}
|
|
}
|
|
new_ucmd!()
|
|
.args(&["-c", "-³"])
|
|
.fails()
|
|
.stderr_is("tail: invalid number of bytes: '³'");
|
|
}
|
|
|
|
#[test]
|
|
fn test_num_with_undocumented_sign_bytes() {
|
|
// tail: '-' is not documented (8.32 man pages)
|
|
// head: '+' is not documented (8.32 man pages)
|
|
const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
|
|
new_ucmd!()
|
|
.args(&["-c", "5"])
|
|
.pipe_in(ALPHABET)
|
|
.succeeds()
|
|
.stdout_is("vwxyz");
|
|
new_ucmd!()
|
|
.args(&["-c", "-5"])
|
|
.pipe_in(ALPHABET)
|
|
.succeeds()
|
|
.stdout_is("vwxyz");
|
|
new_ucmd!()
|
|
.args(&["-c", "+5"])
|
|
.pipe_in(ALPHABET)
|
|
.succeeds()
|
|
.stdout_is("efghijklmnopqrstuvwxyz");
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_bytes_for_funny_files() {
|
|
// gnu/tests/tail-2/tail-c.sh
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
for file in ["/proc/version", "/sys/kernel/profiling"] {
|
|
if !at.file_exists(file) {
|
|
continue;
|
|
}
|
|
let args = ["--bytes", "1", file];
|
|
let result = ts.ucmd().args(&args).run();
|
|
let exp_result = unwrap_or_return!(expected_result(&ts, &args));
|
|
result
|
|
.stdout_is(exp_result.stdout_str())
|
|
.stderr_is(exp_result.stderr_str())
|
|
.code_is(exp_result.code());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_retry1() {
|
|
// gnu/tests/tail-2/retry.sh
|
|
// Ensure --retry without --follow results in a warning.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let file_name = "FILE";
|
|
at.touch("FILE");
|
|
|
|
let result = ts.ucmd().arg(file_name).arg("--retry").run();
|
|
result
|
|
.stderr_is("tail: warning: --retry ignored; --retry is useful only when following\n")
|
|
.code_is(0);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_retry2() {
|
|
// gnu/tests/tail-2/retry.sh
|
|
// The same as test_retry2 with a missing file: expect error message and exit 1.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let missing = "missing";
|
|
|
|
let result = ts.ucmd().arg(missing).arg("--retry").run();
|
|
result
|
|
.stderr_is(
|
|
"tail: warning: --retry ignored; --retry is useful only when following\n\
|
|
tail: cannot open 'missing' for reading: No such file or directory\n",
|
|
)
|
|
.code_is(1);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_retry3() {
|
|
// gnu/tests/tail-2/retry.sh
|
|
// Ensure that `tail --retry --follow=name` waits for the file to appear.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let missing = "missing";
|
|
|
|
let expected_stderr = "tail: cannot open 'missing' for reading: No such file or directory\n\
|
|
tail: 'missing' has appeared; following new file\n";
|
|
let expected_stdout = "X\n";
|
|
let delay = 1000;
|
|
let mut args = vec!["--follow=name", "--retry", missing, "--use-polling"];
|
|
for _ in 0..2 {
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
sleep(Duration::from_millis(delay));
|
|
at.touch(missing);
|
|
sleep(Duration::from_millis(delay));
|
|
at.truncate(missing, "X\n");
|
|
sleep(Duration::from_millis(2 * delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
|
|
at.remove(missing);
|
|
args.pop();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_retry4() {
|
|
// gnu/tests/tail-2/retry.sh
|
|
// Ensure that `tail --retry --follow=descriptor` waits for the file to appear.
|
|
// Ensure truncation is detected.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let missing = "missing";
|
|
|
|
let expected_stderr = "tail: warning: --retry only effective for the initial open\n\
|
|
tail: cannot open 'missing' for reading: No such file or directory\n\
|
|
tail: 'missing' has appeared; following new file\n\
|
|
tail: missing: file truncated\n";
|
|
let expected_stdout = "X1\nX\n";
|
|
let delay = 100;
|
|
let mut args = vec!["-s.1", "--max-unchanged-stats=1", "--follow=descriptor", "--retry", missing, "---disable-inotify"];
|
|
for _ in 0..2 {
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
sleep(Duration::from_millis(delay));
|
|
at.touch(missing);
|
|
sleep(Duration::from_millis(delay));
|
|
at.truncate(missing, "X1\n");
|
|
sleep(Duration::from_millis(delay));
|
|
at.truncate(missing, "X\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
|
|
at.remove(missing);
|
|
args.pop();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_retry5() {
|
|
// gnu/tests/tail-2/retry.sh
|
|
// Ensure that `tail --follow=descriptor --retry` exits when the file appears untailable.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let missing = "missing";
|
|
|
|
let expected_stderr = "tail: warning: --retry only effective for the initial open\n\
|
|
tail: cannot open 'missing' for reading: No such file or directory\n\
|
|
tail: 'missing' has been replaced with an untailable file; giving up on this name\n\
|
|
tail: no files remaining\n";
|
|
let delay = 1000;
|
|
let mut args = vec!["--follow=descriptor", "--retry", missing, "--use-polling"];
|
|
for _ in 0..2 {
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
sleep(Duration::from_millis(delay));
|
|
at.mkdir(missing);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert!(buf_stdout.is_empty());
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
|
|
at.rmdir(missing);
|
|
args.pop();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_retry6() {
|
|
// gnu/tests/tail-2/retry.sh
|
|
// Ensure that --follow=descriptor (without --retry) does *not* try
|
|
// to open a file after an initial fail, even when there are other tailable files.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let missing = "missing";
|
|
let existing = "existing";
|
|
at.touch(existing);
|
|
|
|
let expected_stderr = "tail: cannot open 'missing' for reading: No such file or directory\n";
|
|
let expected_stdout = "==> existing <==\nX\n";
|
|
|
|
let mut p = ts
|
|
.ucmd()
|
|
.arg("--follow=descriptor")
|
|
.arg("missing")
|
|
.arg("existing")
|
|
.run_no_wait();
|
|
|
|
let delay = 1000;
|
|
sleep(Duration::from_millis(delay));
|
|
at.truncate(missing, "Y\n");
|
|
sleep(Duration::from_millis(delay));
|
|
at.truncate(existing, "X\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_retry7() {
|
|
// gnu/tests/tail-2/retry.sh
|
|
// Ensure that `tail -F` retries when the file is initially untailable.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let untailable = "untailable";
|
|
|
|
let expected_stderr = "tail: error reading 'untailable': Is a directory\n\
|
|
tail: untailable: cannot follow end of this type of file\n\
|
|
tail: 'untailable' has appeared; following new file\n\
|
|
tail: 'untailable' has become inaccessible: No such file or directory\n\
|
|
tail: 'untailable' has been replaced with an untailable file\n\
|
|
tail: 'untailable' has appeared; following new file\n";
|
|
let expected_stdout = "foo\nbar\n";
|
|
|
|
let delay = 1000;
|
|
|
|
let mut args = vec![
|
|
"-s.1",
|
|
"--max-unchanged-stats=1",
|
|
"-F",
|
|
untailable,
|
|
"--use-polling",
|
|
];
|
|
for _ in 0..2 {
|
|
at.mkdir(untailable);
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
// tail: 'untailable' has become accessible
|
|
// or (The first is the common case, "has appeared" arises with slow rmdir):
|
|
// tail: 'untailable' has appeared; following new file
|
|
at.rmdir(untailable);
|
|
at.truncate(untailable, "foo\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
// NOTE: GNU's `tail` only shows "become inaccessible"
|
|
// if there's a delay between rm and mkdir.
|
|
// tail: 'untailable' has become inaccessible: No such file or directory
|
|
at.remove(untailable);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
// tail: 'untailable' has been replaced with an untailable file\n";
|
|
at.mkdir(untailable);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
// full circle, back to the beginning
|
|
at.rmdir(untailable);
|
|
at.truncate(untailable, "bar\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
|
|
args.pop();
|
|
at.remove(untailable);
|
|
sleep(Duration::from_millis(delay));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android
|
|
fn test_retry8() {
|
|
// Ensure that inotify will switch to polling mode if directory
|
|
// of the watched file was initially missing and later created.
|
|
// This is similar to test_retry9, but without:
|
|
// tail: directory containing watched file was removed\n\
|
|
// tail: inotify cannot be used, reverting to polling\n\
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let watched_file = std::path::Path::new("watched_file");
|
|
let parent_dir = std::path::Path::new("parent_dir");
|
|
let user_path = parent_dir.join(watched_file);
|
|
let parent_dir = parent_dir.to_str().unwrap();
|
|
let user_path = user_path.to_str().unwrap();
|
|
|
|
let expected_stderr = "\
|
|
tail: cannot open 'parent_dir/watched_file' for reading: No such file or directory\n\
|
|
tail: 'parent_dir/watched_file' has appeared; following new file\n\
|
|
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
|
|
tail: 'parent_dir/watched_file' has appeared; following new file\n";
|
|
let expected_stdout = "foo\nbar\n";
|
|
|
|
let delay = 1000;
|
|
|
|
let mut p = ts
|
|
.ucmd()
|
|
.arg("-F")
|
|
.arg("-s.1")
|
|
.arg("--max-unchanged-stats=1")
|
|
.arg(user_path)
|
|
.run_no_wait();
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.mkdir(parent_dir);
|
|
at.append(user_path, "foo\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.remove(user_path);
|
|
at.rmdir(parent_dir);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.mkdir(parent_dir);
|
|
at.append(user_path, "bar\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android
|
|
fn test_retry9() {
|
|
// gnu/tests/tail-2/inotify-dir-recreate.sh
|
|
// Ensure that inotify will switch to polling mode if directory
|
|
// of the watched file was removed and recreated.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let watched_file = std::path::Path::new("watched_file");
|
|
let parent_dir = std::path::Path::new("parent_dir");
|
|
let user_path = parent_dir.join(watched_file);
|
|
let parent_dir = parent_dir.to_str().unwrap();
|
|
let user_path = user_path.to_str().unwrap();
|
|
|
|
let expected_stderr = format!(
|
|
"\
|
|
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
|
|
tail: directory containing watched file was removed\n\
|
|
tail: {} cannot be used, reverting to polling\n\
|
|
tail: 'parent_dir/watched_file' has appeared; following new file\n\
|
|
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
|
|
tail: 'parent_dir/watched_file' has appeared; following new file\n\
|
|
tail: 'parent_dir/watched_file' has become inaccessible: No such file or directory\n\
|
|
tail: 'parent_dir/watched_file' has appeared; following new file\n",
|
|
BACKEND
|
|
);
|
|
let expected_stdout = "foo\nbar\nfoo\nbar\n";
|
|
|
|
let delay = 1000;
|
|
|
|
at.mkdir(parent_dir);
|
|
at.truncate(user_path, "foo\n");
|
|
let mut p = ts
|
|
.ucmd()
|
|
.arg("-F")
|
|
.arg("-s.1")
|
|
.arg("--max-unchanged-stats=1")
|
|
.arg(user_path)
|
|
.run_no_wait();
|
|
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.remove(user_path);
|
|
at.rmdir(parent_dir);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.mkdir(parent_dir);
|
|
at.truncate(user_path, "bar\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.remove(user_path);
|
|
at.rmdir(parent_dir);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.mkdir(parent_dir);
|
|
at.truncate(user_path, "foo\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.remove(user_path);
|
|
at.rmdir(parent_dir);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.mkdir(parent_dir);
|
|
at.truncate(user_path, "bar\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
// println!("stdout:\n{}\nstderr:\n{}", buf_stdout, buf_stderr); // dbg
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_follow_descriptor_vs_rename1() {
|
|
// gnu/tests/tail-2/descriptor-vs-rename.sh
|
|
// $ ((rm -f A && touch A && sleep 1 && echo -n "A\n" >> A && sleep 1 && \
|
|
// mv A B && sleep 1 && echo -n "B\n" >> B &)>/dev/null 2>&1 &) ; \
|
|
// sleep 1 && target/debug/tail --follow=descriptor A ---disable-inotify
|
|
// $ A
|
|
// $ B
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let file_a = "FILE_A";
|
|
let file_b = "FILE_B";
|
|
let file_c = "FILE_C";
|
|
|
|
let mut args = vec![
|
|
"--follow=descriptor",
|
|
"-s.1",
|
|
"--max-unchanged-stats=1",
|
|
file_a,
|
|
"---disable-inotify",
|
|
];
|
|
|
|
let delay = 500;
|
|
for _ in 0..2 {
|
|
at.touch(file_a);
|
|
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.append(file_a, "A\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.rename(file_a, file_b);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.append(file_b, "B\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.rename(file_b, file_c);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
at.append(file_c, "C\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, "A\nB\nC\n");
|
|
assert!(buf_stderr.is_empty());
|
|
|
|
args.pop();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_follow_descriptor_vs_rename2() {
|
|
// Ensure the headers are correct for --verbose.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
let file_a = "FILE_A";
|
|
let file_b = "FILE_B";
|
|
let file_c = "FILE_C";
|
|
|
|
let mut args = vec![
|
|
"--follow=descriptor",
|
|
"-s.1",
|
|
"--max-unchanged-stats=1",
|
|
file_a,
|
|
file_b,
|
|
"--verbose",
|
|
"---disable-inotify",
|
|
];
|
|
|
|
let delay = 100;
|
|
for _ in 0..2 {
|
|
at.touch(file_a);
|
|
at.touch(file_b);
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
sleep(Duration::from_millis(delay));
|
|
at.rename(file_a, file_c);
|
|
sleep(Duration::from_millis(1000));
|
|
at.append(file_c, "x\n");
|
|
sleep(Duration::from_millis(delay));
|
|
p.kill().unwrap();
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(
|
|
buf_stdout,
|
|
"==> FILE_A <==\n\n==> FILE_B <==\n\n==> FILE_A <==\nx\n"
|
|
);
|
|
assert!(buf_stderr.is_empty());
|
|
|
|
args.pop();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_follow_name_remove() {
|
|
// This test triggers a remove event while `tail --follow=name logfile` is running.
|
|
// ((sleep 2 && rm logfile &)>/dev/null 2>&1 &) ; tail --follow=name logfile
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
|
|
let source = FOLLOW_NAME_TXT;
|
|
let source_copy = "source_copy";
|
|
at.copy(source, source_copy);
|
|
|
|
let expected_stdout = at.read(FOLLOW_NAME_SHORT_EXP);
|
|
let expected_stderr = format!(
|
|
"{}: {}: No such file or directory\n{0}: no files remaining\n",
|
|
ts.util_name, source_copy
|
|
);
|
|
|
|
let delay = 2000;
|
|
let mut args = vec!["--follow=name", source_copy, "--use-polling"];
|
|
|
|
for _ in 0..2 {
|
|
at.copy(source, source_copy);
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
sleep(Duration::from_millis(delay));
|
|
at.remove(source_copy);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
|
|
args.pop();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_follow_name_truncate1() {
|
|
// This test triggers a truncate event while `tail --follow=name logfile` is running.
|
|
// $ cp logfile backup && head logfile > logfile && sleep 1 && cp backup logfile
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
|
|
let source = FOLLOW_NAME_TXT;
|
|
let backup = "backup";
|
|
|
|
let expected_stdout = at.read(FOLLOW_NAME_EXP);
|
|
let expected_stderr = format!("{}: {}: file truncated\n", ts.util_name, source);
|
|
|
|
let args = ["--follow=name", source];
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
let delay = 1000;
|
|
|
|
at.copy(source, backup);
|
|
sleep(Duration::from_millis(delay));
|
|
at.touch(source); // trigger truncate
|
|
sleep(Duration::from_millis(delay));
|
|
at.copy(backup, source);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_follow_name_truncate2() {
|
|
// This test triggers a truncate event while `tail --follow=name logfile` is running.
|
|
// $ ((sleep 1 && echo -n "x\nx\nx\n" >> logfile && sleep 1 && \
|
|
// echo -n "x\n" > logfile &)>/dev/null 2>&1 &) ; tail --follow=name logfile
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
|
|
let source = "logfile";
|
|
at.touch(source);
|
|
|
|
let expected_stdout = "x\nx\nx\nx\n";
|
|
let expected_stderr = format!("{}: {}: file truncated\n", ts.util_name, source);
|
|
|
|
let args = ["--follow=name", source];
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
let delay = 1000;
|
|
|
|
at.append(source, "x\n");
|
|
sleep(Duration::from_millis(delay));
|
|
at.append(source, "x\n");
|
|
sleep(Duration::from_millis(delay));
|
|
at.append(source, "x\n");
|
|
sleep(Duration::from_millis(delay));
|
|
at.truncate(source, "x\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")] // FIXME: fix this test for BSD/macOS
|
|
fn test_follow_name_truncate3() {
|
|
// Opening an empty file in truncate mode should not trigger a truncate event while.
|
|
// $ rm -f logfile && touch logfile
|
|
// $ ((sleep 1 && echo -n "x\n" > logfile &)>/dev/null 2>&1 &) ; tail --follow=name logfile
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
|
|
let source = "logfile";
|
|
at.touch(source);
|
|
|
|
let expected_stdout = "x\n";
|
|
|
|
let args = ["--follow=name", source];
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
let delay = 1000;
|
|
sleep(Duration::from_millis(delay));
|
|
at.truncate(source, "x\n");
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert!(buf_stderr.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android
|
|
fn test_follow_name_move_create() {
|
|
// This test triggers a move/create event while `tail --follow=name logfile` is running.
|
|
// ((sleep 2 && mv logfile backup && sleep 2 && cp backup logfile &)>/dev/null 2>&1 &) ; tail --follow=name logfile
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
|
|
let source = FOLLOW_NAME_TXT;
|
|
let backup = "backup";
|
|
|
|
#[cfg(target_os = "linux")]
|
|
let expected_stdout = at.read(FOLLOW_NAME_EXP);
|
|
#[cfg(target_os = "linux")]
|
|
let expected_stderr = format!(
|
|
"{}: {}: No such file or directory\n{0}: '{1}' has appeared; following new file\n",
|
|
ts.util_name, source
|
|
);
|
|
|
|
// NOTE: We are less strict if not on Linux (inotify backend).
|
|
#[cfg(not(target_os = "linux"))]
|
|
let expected_stdout = at.read(FOLLOW_NAME_SHORT_EXP);
|
|
#[cfg(not(target_os = "linux"))]
|
|
let expected_stderr = format!("{}: {}: No such file or directory\n", ts.util_name, source);
|
|
|
|
let args = ["--follow=name", source];
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
let delay = 2000;
|
|
|
|
sleep(Duration::from_millis(delay));
|
|
at.rename(source, backup);
|
|
sleep(Duration::from_millis(delay));
|
|
at.copy(backup, source);
|
|
sleep(Duration::from_millis(delay));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_follow_name_move() {
|
|
// This test triggers a move event while `tail --follow=name logfile` is running.
|
|
// ((sleep 2 && mv logfile backup &)>/dev/null 2>&1 &) ; tail --follow=name logfile
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
let at = &ts.fixtures;
|
|
|
|
let source = FOLLOW_NAME_TXT;
|
|
let backup = "backup";
|
|
|
|
let expected_stdout = at.read(FOLLOW_NAME_SHORT_EXP);
|
|
let expected_stderr = [
|
|
format!(
|
|
"{}: {}: No such file or directory\n{0}: no files remaining\n",
|
|
ts.util_name, source
|
|
),
|
|
format!("{}: {}: No such file or directory\n", ts.util_name, source),
|
|
];
|
|
|
|
let mut args = vec!["--follow=name", source, "--use-polling"];
|
|
|
|
#[allow(clippy::needless_range_loop)]
|
|
for i in 0..2 {
|
|
let mut p = ts.ucmd().args(&args).run_no_wait();
|
|
|
|
sleep(Duration::from_millis(2000));
|
|
at.rename(source, backup);
|
|
sleep(Duration::from_millis(5000));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, expected_stdout);
|
|
assert_eq!(buf_stderr, expected_stderr[i]);
|
|
|
|
at.rename(backup, source);
|
|
args.pop();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn test_follow_inotify_only_regular() {
|
|
// The GNU test inotify-only-regular.sh uses strace to ensure that `tail -f`
|
|
// doesn't make inotify syscalls and only uses inotify for regular files or fifos.
|
|
// We just check if tailing a character device has the same behavior as GNU's tail.
|
|
|
|
let ts = TestScenario::new(util_name!());
|
|
|
|
let mut p = ts.ucmd().arg("-f").arg("/dev/null").run_no_wait();
|
|
sleep(Duration::from_millis(200));
|
|
|
|
p.kill().unwrap();
|
|
|
|
let (buf_stdout, buf_stderr) = take_stdout_stderr(&mut p);
|
|
assert_eq!(buf_stdout, "".to_string());
|
|
assert_eq!(buf_stderr, "".to_string());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn take_stdout_stderr(p: &mut std::process::Child) -> (String, String) {
|
|
let mut buf_stdout = String::new();
|
|
let mut p_stdout = p.stdout.take().unwrap();
|
|
p_stdout.read_to_string(&mut buf_stdout).unwrap();
|
|
let mut buf_stderr = String::new();
|
|
let mut p_stderr = p.stderr.take().unwrap();
|
|
p_stderr.read_to_string(&mut buf_stderr).unwrap();
|
|
(buf_stdout, buf_stderr)
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_such_file() {
|
|
new_ucmd!()
|
|
.arg("bogusfile")
|
|
.fails()
|
|
.no_stdout()
|
|
.stderr_contains("cannot open 'bogusfile' for reading: No such file or directory");
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_trailing_newline() {
|
|
new_ucmd!().pipe_in("x").succeeds().stdout_only("x");
|
|
}
|
|
|
|
#[test]
|
|
fn test_lines_zero_terminated() {
|
|
new_ucmd!()
|
|
.args(&["-z", "-n", "2"])
|
|
.pipe_in("a\0b\0c\0d\0e\0")
|
|
.succeeds()
|
|
.stdout_only("d\0e\0");
|
|
new_ucmd!()
|
|
.args(&["-z", "-n", "+2"])
|
|
.pipe_in("a\0b\0c\0d\0e\0")
|
|
.succeeds()
|
|
.stdout_only("b\0c\0d\0e\0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_presume_input_pipe_default() {
|
|
new_ucmd!()
|
|
.arg("---presume-input-pipe")
|
|
.pipe_in_fixture(FOOBAR_TXT)
|
|
.run()
|
|
.stdout_is_fixture("foobar_stdin_default.expected");
|
|
}
|