From 5637d83e9ce1ccfb77c55ebc10ff79cafa179445 Mon Sep 17 00:00:00 2001 From: Joining7943 <111500881+Joining7943@users.noreply.github.com> Date: Sun, 9 Apr 2023 14:37:00 +0200 Subject: [PATCH] tests/tail: Add and fix tests. Some are currently failing and ignored. --- tests/by-util/test_tail.rs | 383 +++++++++++++++++++++++++------------ 1 file changed, 260 insertions(+), 123 deletions(-) diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 2f83f303bc1..be95f5bab8d 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -15,6 +15,7 @@ use crate::common::util::is_ci; use crate::common::util::TestScenario; use pretty_assertions::assert_eq; use rand::distributions::Alphanumeric; +use regex::Regex; use rstest::rstest; use std::char::from_digit; use std::fs::File; @@ -46,7 +47,6 @@ static FOLLOW_NAME_SHORT_EXP: &str = "follow_name_short.expected"; #[allow(dead_code)] static FOLLOW_NAME_EXP: &str = "follow_name.expected"; -#[cfg(not(windows))] const DEFAULT_SLEEP_INTERVAL_MILLIS: u64 = 1000; // The binary integer "10000000" is *not* a valid UTF-8 encoding @@ -56,6 +56,24 @@ const INVALID_UTF8: u8 = 0x80; #[cfg(windows)] const INVALID_UTF16: u16 = 0xD800; +/// Copied this from test_tee. test_tee uses this on linux targets only. +#[cfg(target_os = "linux")] +fn make_broken_pipe() -> File { + use libc::c_int; + use std::os::unix::io::FromRawFd; + + let mut fds: [c_int; 2] = [0, 0]; + if unsafe { libc::pipe(&mut fds as *mut c_int) } != 0 { + panic!("Failed to create pipe"); + } + + // Drop the read end of the pipe + let _ = unsafe { File::from_raw_fd(fds[0]) }; + + // Make the write end of the pipe into a Rust File + unsafe { File::from_raw_fd(fds[1]) } +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); @@ -145,12 +163,13 @@ fn test_stdin_redirect_offset() { } #[test] -#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] // FIXME: for currently not working platforms +#[cfg(all(not(target_vendor = "apple")))] +// FIXME: for currently not working platforms +// FIXME: windows: Failed because of difference in printed header. See below. +// actual : ==> - <== +// expected: ==> standard input <== +#[cfg_attr(target_os = "windows", ignore = "Fixed in #3952")] fn test_stdin_redirect_offset2() { - // FIXME: windows: Failed because of difference in printed header. See below. - // actual : ==> - <== - // expected: ==> standard input <== - // like test_stdin_redirect_offset but with multiple files let ts = TestScenario::new(util_name!()); @@ -286,12 +305,16 @@ fn test_follow_redirect_stdin_name_retry() { } #[test] -#[cfg(all( - not(target_vendor = "apple"), - not(target_os = "windows"), - not(target_os = "android"), - not(target_os = "freebsd") -))] // FIXME: for currently not working platforms +// FIXME: fix this test for Android +#[cfg(all(unix, not(any(target_os = "android"))))] +// FIXME: fix this test for apple: The error differs: +// tail: error reading 'standard input': Is a directory +#[cfg_attr(target_vendor = "apple", ignore = "Fixed in #3952")] +// FIXME: fix this test for freebsd: The error differs: +// tail: error reading 'standard input': Is a directory +#[cfg_attr(target_os = "freebsd", ignore = "Fixed in #3952")] fn test_stdin_redirect_dir() { // $ mkdir dir // $ tail < dir, $ tail - < dir @@ -316,39 +339,6 @@ fn test_stdin_redirect_dir() { .code_is(1); } -// On macOS path.is_dir() can be false for directories if it was a redirect, -// e.g. `$ tail < DIR. The library feature to detect the -// std::io::ErrorKind::IsADirectory isn't stable so we currently show the a wrong -// error message. -// FIXME: If `std::io::ErrorKind::IsADirectory` becomes stable or macos handles -// redirected directories like linux show the correct message like in -// `test_stdin_redirect_dir` -#[test] -#[cfg(target_vendor = "apple")] -fn test_stdin_redirect_dir_when_target_os_is_macos() { - // $ mkdir dir - // $ tail < dir, $ tail - < dir - // tail: error reading 'standard input': Is a directory - - let ts = TestScenario::new(util_name!()); - let at = &ts.fixtures; - at.mkdir("dir"); - - ts.ucmd() - .set_stdin(File::open(at.plus("dir")).unwrap()) - .fails() - .no_stdout() - .stderr_is("tail: cannot open 'standard input' for reading: No such file or directory\n") - .code_is(1); - ts.ucmd() - .set_stdin(File::open(at.plus("dir")).unwrap()) - .arg("-") - .fails() - .no_stdout() - .stderr_is("tail: cannot open 'standard input' for reading: No such file or directory\n") - .code_is(1); -} - #[test] fn test_follow_stdin_descriptor() { let ts = TestScenario::new(util_name!()); @@ -387,25 +377,6 @@ fn test_follow_stdin_name_retry() { } } -#[test] -fn test_follow_bad_fd() { - // Provoke a "bad file descriptor" error by closing the fd - // inspired by: "gnu/tests/tail-2/follow-stdin.sh" - - // `$ tail -f <&-` OR `$ tail -f - <&-` - // tail: cannot fstat 'standard input': Bad file descriptor - // tail: error reading 'standard input': Bad file descriptor - // tail: no files remaining - // tail: -: Bad file descriptor - // - // $ `tail <&-` - // tail: cannot fstat 'standard input': Bad file descriptor - // tail: -: Bad file descriptor - - // WONT-FIX: - // see also: https://github.com/uutils/coreutils/issues/2873 -} - #[test] fn test_single_default() { new_ucmd!() @@ -580,12 +551,21 @@ fn test_follow_multiple_untailable() { // tail: DIR2: cannot follow end of this type of file; giving up on this name // tail: no files remaining + // FIXME: DIR1 and DIR2 need to be in single quotes let expected_stdout = "==> DIR1 <==\n\n==> DIR2 <==\n"; let expected_stderr = "tail: error reading 'DIR1': Is a directory\n\ tail: DIR1: cannot follow end of this type of file; giving up on this name\n\ tail: error reading 'DIR2': Is a directory\n\ tail: DIR2: cannot follow end of this type of file; giving up on this name\n\ tail: no files remaining\n"; + // FIXME: The error message for windows and unix differs + // #[cfg(windows)] + // let expected_stderr = + // "tail: error reading 'DIR1': An operation is not supported on a directory.\n\ + // tail: DIR1: cannot follow end of this type of file; giving up on this name\n\ + // tail: error reading 'DIR2': An operation is not supported on a directory.\n\ + // tail: DIR2: cannot follow end of this type of file; giving up on this name\n\ + // tail: no files remaining\n"; let (at, mut ucmd) = at_and_ucmd!(); at.mkdir("DIR1"); @@ -907,10 +887,11 @@ fn test_multiple_input_quiet_flag_overrides_verbose_flag_for_suppressing_headers 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); + let expected = "tail: error reading 'DIR': Is a directory\n"; + // FIXME: The error message for windows and unix differs + // #[cfg(windows)] + // let expected = "tail: error reading 'DIR': An operation is not supported on a directory.\n"; + ucmd.arg("DIR").run().stderr_is(expected).code_is(1); } #[test] @@ -918,17 +899,24 @@ fn test_dir_follow() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; at.mkdir("DIR"); + + let expected = "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"; + // FIXME: The error message for windows and unix differs + // #[cfg(windows)] + // let expected = "tail: error reading 'DIR': An operation is not supported on a directory.\n\ + // tail: DIR: cannot follow end of this type of file; giving up on this name\n\ + // tail: no files remaining\n"; + + // FIXME: DIR must be single quoted for mode in &["--follow=descriptor", "--follow=name"] { ts.ucmd() .arg(mode) .arg("DIR") .run() .no_stdout() - .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", - ) + .stderr_is(expected) .code_is(1); } } @@ -938,17 +926,22 @@ fn test_dir_follow_retry() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; at.mkdir("DIR"); + let expected = "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"; + // FIXME: The error message for windows and unix differs + // #[cfg(windows)] + // let expected = "tail: warning: --retry only effective for the initial open\n\ + // tail: error reading 'DIR': An operation is not supported on a directory.\n\ + // tail: DIR: cannot follow end of this type of file\n\ + // tail: no files remaining\n"; 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", - ) + .stderr_is(expected) .code_is(1); } @@ -1406,6 +1399,7 @@ fn test_retry7() { let at = &ts.fixtures; let untailable = "untailable"; + // FIXME: >>tail: untailable: cannot follow ...<< untailable must be quoted >>tail: 'untailable': cannot follow ...<< 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 become accessible\n\ @@ -3436,10 +3430,16 @@ fn test_when_follow_retry_given_redirected_stdin_from_directory_then_correct_err let at = &ts.fixtures; at.mkdir("dir"); + #[cfg(not(windows))] let expected = "tail: warning: --retry only effective for the initial open\n\ tail: error reading 'standard input': Is a directory\n\ tail: 'standard input': cannot follow end of this type of file\n\ tail: no files remaining\n"; + #[cfg(windows)] + let expected = "tail: warning: --retry only effective for the initial open\n\ + tail: error reading 'standard input': An operation is not supported on a directory.\n\ + tail: 'standard input': cannot follow end of this type of file\n\ + tail: no files remaining\n"; ts.ucmd() .set_stdin(File::open(at.plus("dir")).unwrap()) .args(&["-f", "--retry"]) @@ -3455,6 +3455,9 @@ fn test_when_argument_file_is_a_directory() { at.mkdir("dir"); let expected = "tail: error reading 'dir': Is a directory\n"; + // FIXME: The error message for windows and unix differs + // #[cfg(windows)] + // let expected = "tail: error reading 'dir': An operation is not supported on a directory.\n"; ts.ucmd() .arg("dir") .fails() @@ -3521,7 +3524,9 @@ fn test_when_argument_file_is_a_symlink_to_directory_then_error() { // TODO: make this work on windows #[test] #[cfg(unix)] -#[cfg(disabled_until_fixed)] +// FIXME: Fails with the wrong error message: `No such file or directory` instead of the expected +// messages below +#[ignore = "Fixed in #3952"] fn test_when_argument_file_is_a_faulty_symlink_then_error() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -3529,11 +3534,11 @@ fn test_when_argument_file_is_a_faulty_symlink_then_error() { at.symlink_file("self", "self"); #[cfg(all(not(target_env = "musl"), not(target_os = "android")))] - let expected = "tail: cannot open 'self' for reading: Too many levels of symbolic links"; + let expected = "tail: cannot open 'self' for reading: Too many levels of symbolic links\n"; #[cfg(all(not(target_env = "musl"), target_os = "android"))] - let expected = "tail: cannot open 'self' for reading: Too many symbolic links encountered"; + let expected = "tail: cannot open 'self' for reading: Too many symbolic links encountered\n"; #[cfg(all(target_env = "musl", not(target_os = "android")))] - let expected = "tail: cannot open 'self' for reading: Symbolic link loop"; + let expected = "tail: cannot open 'self' for reading: Symbolic link loop\n"; ts.ucmd() .arg("self") @@ -3543,7 +3548,7 @@ fn test_when_argument_file_is_a_faulty_symlink_then_error() { at.symlink_file("missing", "broken"); - let expected = "tail: cannot open 'broken' for reading: No such file or directory"; + let expected = "tail: cannot open 'broken' for reading: No such file or directory\n"; ts.ucmd() .arg("broken") .fails() @@ -3553,7 +3558,8 @@ fn test_when_argument_file_is_a_faulty_symlink_then_error() { #[test] #[cfg(unix)] -#[cfg(disabled_until_fixed)] +// FIXME: tail is expected to fail but it succeeds without message +#[ignore = "Fixed in #3952"] fn test_when_argument_file_is_non_existent_unix_socket_address_then_error() { use std::os::unix::net; @@ -3612,7 +3618,8 @@ fn test_when_argument_file_is_non_existent_unix_socket_address_then_error() { } #[test] -#[cfg(disabled_until_fixed)] +// FIXME: Fifos are printed multiple times when `-` is given multiple times +#[ignore = "Fixed in #3952"] fn test_when_argument_files_are_simple_combinations_of_stdin_and_regular_file() { let scene = TestScenario::new(util_name!()); let fixtures = &scene.fixtures; @@ -3714,7 +3721,8 @@ fn test_when_argument_files_are_simple_combinations_of_stdin_and_regular_file() } #[test] -#[cfg(disabled_until_fixed)] +// FIXME: Fifos are printed multiple times when `-` is given multiple times +#[ignore = "Fixed in #3952"] fn test_when_argument_files_are_triple_combinations_of_fifo_pipe_and_regular_file() { let scene = TestScenario::new(util_name!()); let fixtures = &scene.fixtures; @@ -3768,25 +3776,26 @@ fn test_when_argument_files_are_triple_combinations_of_fifo_pipe_and_regular_fil // print the fifo twice. This matches the behavior, if only the pipe is present without fifo // (See test above). Note that for example a zsh shell prints the pipe data and has therefore // different output from the sh shell (or cmd shell on windows). - + // // windows: tail returns with success although there is an error message present (on some // windows systems). This error message comes from `echo` (the line ending `\r\n` indicates that - // too) which cannot write to the pipe because tail finished before echo was able to write to - // the pipe. Seems that windows `cmd` (like posix shells) ignores pipes when a fifo is present. - // This is actually the wished behavior and the test therefore succeeds. + // too) which cannot write to the pipe. This is actually the wished behavior and the test + // therefore succeeds in such cases. + // + // [`CmdResult::stdout_matches`] trims the stdout, so we ignore the final newlines in the regex. #[cfg(windows)] - let expected = "==> standard input <==\n\ + let expected = "^==> standard input <==\n\ fifo data\n\ ==> data <==\n\ file data\n\ - ==> standard input <==\n\ - (The process tried to write to a nonexistent pipe.\r\n)?"; + ==> standard input <==(\n)?\ + (The process tried to write to a nonexistent pipe.(\r\n)?)?$"; #[cfg(unix)] - let expected = "==> standard input <==\n\ + let expected = "^==> standard input <==\n\ fifo data\n\ ==> data <==\n\ file data\n\ - ==> standard input <==\n"; + ==> standard input <==(\n)?$"; #[cfg(windows)] let cmd = ["cmd", "/C"]; @@ -3800,8 +3809,9 @@ fn test_when_argument_files_are_triple_combinations_of_fifo_pipe_and_regular_fil "echo pipe data | {} tail -c +0 - data - < fifo", scene.bin_path.display(), )) + .stderr_to_stdout() .run() - .stdout_only(expected) + .stdout_matches(&Regex::new(expected).unwrap()) .success(); let expected = "==> standard input <==\n\ @@ -3825,7 +3835,8 @@ fn test_when_argument_files_are_triple_combinations_of_fifo_pipe_and_regular_fil // test system. However, this behavior shows up on the command line and, at the time of writing this // description, with this test on macos and windows. #[test] -#[cfg(disable_until_fixed)] +// FIXME: See description above +#[ignore = "Fixed in #3952"] fn test_when_follow_retry_then_initial_print_of_file_is_written_to_stdout() { let scene = TestScenario::new(util_name!()); let fixtures = &scene.fixtures; @@ -4083,6 +4094,7 @@ fn test_args_when_settings_check_warnings_follow_indefinitely_then_no_warning() // > // ==> data <== // file data + // FIXME: This should be fixed for apple in #3952 #[cfg(not(target_vendor = "apple"))] { let expected_stdout = format!( @@ -4358,28 +4370,42 @@ fn test_follow_when_file_and_symlink_are_pointing_to_same_file_and_append_data() .stdout_only(expected_stdout); } -// Fails with: -// 'Assertion failed. Expected 'tail' to be running but exited with status=exit status: 1. -// stdout: -// stderr: tail: warning: --retry ignored; --retry is useful only when following -// tail: error reading 'dir': Is a directory -// ' #[test] -#[cfg(disabled_until_fixed)] +// FIXME: The argument parsing is wrong: --retry together with -F disables the follow mode instead +// of ignoring --retry. The command therefore fails with +// `tail: warning: --retry ignored; --retry is useful only when following` +// The command is expected to be running. +#[ignore = "Fixed in #3952"] fn test_args_when_directory_given_shorthand_big_f_together_with_retry() { let scene = TestScenario::new(util_name!()); let fixtures = &scene.fixtures; let dirname = "dir"; fixtures.mkdir(dirname); + #[cfg(not(windows))] let expected_stderr = format!( "tail: error reading '{0}': Is a directory\n\ tail: {0}: cannot follow end of this type of file\n", dirname ); + #[cfg(windows)] + let expected_stderr = format!( + "tail: error reading '{0}': An operation is not supported on a directory.\n\ + tail: {0}: cannot follow end of this type of file\n", + dirname + ); let mut child = scene.ucmd().args(&["-F", "--retry", "dir"]).run_no_wait(); + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stderr_only(&expected_stderr); + + let mut child = scene.ucmd().args(&["--retry", "-F", "dir"]).run_no_wait(); + child.make_assertion_with_delay(500).is_alive(); child .kill() @@ -4395,28 +4421,16 @@ fn test_args_when_directory_given_shorthand_big_f_together_with_retry() { /// ==> /absolute/path/to/data <== /// file data -/// -/// Fails on windows with -/// Diff < left / right > : -// ==> data <== -// file data -// ==> \\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpi6lNnX\data <== -// >file data -// < -// -// Fails on freebsd with -// Diff < left / right > : +#[test] +#[cfg(all(not(target_vendor = "apple")))] +// FIXME: The output on windows and freebsd differs: // ==> data <== // file data -// ==> /tmp/.tmpZPXPlS/data <== +// ==> \\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpU13SXd\data <== // >file data // < -#[test] -#[cfg(all( - not(target_vendor = "apple"), - not(target_os = "windows"), - not(target_os = "freebsd") -))] +#[cfg_attr(target_os = "windows", ignore = "Fixed in #3952")] +#[cfg_attr(target_os = "freebsd", ignore = "Fixed in #3952")] fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_stays_same_size() { let scene = TestScenario::new(util_name!()); let fixtures = &scene.fixtures; @@ -4797,3 +4811,126 @@ fn test_obsolete_encoding_windows() { .stderr_is("tail: bad argument encoding: '-�b'\n") .code_is(1); } + +#[rstest] +#[case::when_not_follow(&["-c", "+0", "-"])] +// The error output deviates from gnu's tail which is: +// tail: cannot fstat 'standard input': Bad file descriptor +// tail: error reading 'standard input': Bad file descriptor +// tail: no files remaining +// tail: -: Bad file descriptor +#[case::when_follow(&["-c", "+0", "-f", "-"])] +// FIXME: tail should exit with error and error message but it succeeds without message +#[ignore = "Fixed in #3952"] +fn test_when_stdin_is_bad_file_descriptor(#[case] args: &[&str]) { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + // Opens the file in write only mode and leads to the bad file descriptor error when used as stdin + let file = File::create(at.plus_as_string("name")).unwrap(); + #[cfg(not(windows))] + let expected = "tail: error reading 'standard input': Bad file descriptor\n"; + #[cfg(windows)] + let expected = "tail: error reading 'standard input': Permission denied\n"; + ts.ucmd() + .args(args) + .set_stdin(file) + .fails() + .stderr_only(expected); +} + +#[test] +// FIXME: The error message starting with `tail: error reading 'standard input': ... ` is not printed +#[ignore = "Fixed in #3952"] +fn test_when_follow_two_files_and_stdin_is_bad_file_descriptor() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.write("good", "good content"); + // Opens a file in write only mode leading to the bad file descriptor error when used as stdin + // which needs read permissions + let bad_file = File::create(at.plus_as_string("name")).unwrap(); + + // The error output deviates from gnu's tail in so far, that the Bad file descriptor error + // message is printed only once: + // tail: error reading 'standard input': Bad file descriptor + // instead of: + // tail: cannot fstat 'standard input': Bad file descriptor + // tail: error reading 'standard input': Bad file descriptor + #[cfg(not(windows))] + let expected_stdout = "==> standard input <==\n\ + tail: error reading 'standard input': Bad file descriptor\n\ + \n\ + ==> good <==\n\ + good content"; + #[cfg(windows)] + let expected_stdout = "==> standard input <==\n\ + tail: error reading 'standard input': Permission denied\n\ + \n\ + ==> good <==\n\ + good content"; + + let mut child = ts + .ucmd() + .args(&["-c", "+0", "-f", "-", "good"]) + .set_stdin(bad_file) + .stderr_to_stdout() + .run_no_wait(); + + child + .make_assertion_with_delay(DEFAULT_SLEEP_INTERVAL_MILLIS) + .is_alive() + .with_current_output() + .stdout_only(expected_stdout); + + child + .kill() + .make_assertion_with_delay(DEFAULT_SLEEP_INTERVAL_MILLIS) + .is_not_alive() + .with_current_output() + .no_output(); +} + +#[rstest] +#[case::when_not_follow(&["-c", "+0", "-"])] +#[case::when_follow(&["-c", "+0", "-f", "-"])] +#[cfg(target_os = "linux")] +// FIXME: The error message is `tail: Broken pipe` instead of `tail: write error: Broken pipe`. +#[ignore = "Fixed in #3952"] +fn test_when_stdout_is_broken_pipe(#[case] args: &[&str]) { + new_ucmd!() + .args(args) + .set_stdout(make_broken_pipe()) + .fails() + .stderr_only("tail: write error: Broken pipe\n"); +} + +#[test] +#[cfg(feature = "sleep")] +#[cfg(unix)] +/// tail with follow (-f) should be smart enough to terminate when the output is a pipe and the pipe +/// breaks. Currently uu_tail is running forever even if it'll never produce output due to the +/// broken pipe. gnu's tail in contrast does terminate after the 5 seconds sleep interval (without +/// error message). +#[cfg_attr(not(feature = "test_unimplemented"), ignore)] +fn test_when_follow_and_pipe_breaks_then_terminate() { + use std::time::Duration; + + use crate::common::util::{UCommand, TESTS_BINARY}; + + let mut child = UCommand::new() + .arg(format!( + "{TESTS_BINARY} tail -f /dev/null | {TESTS_BINARY} sleep 5" + )) + .timeout(Duration::from_secs(30)) + .run_no_wait(); + + child + .make_assertion_with_delay(DEFAULT_SLEEP_INTERVAL_MILLIS) + .is_alive(); + + child + .delay(10000) + .make_assertion() + .is_not_alive() + .with_all_output() + .no_output(); +}