diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..9d5cad83 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: +- repo: local + hooks: + - id: rust-linting + name: Rust linting + description: Run cargo fmt on files included in the commit. + entry: cargo +stable fmt -- + pass_filenames: true + types: [file, rust] + language: system + - id: rust-clippy + name: Rust clippy + description: Run cargo clippy on files included in the commit. + entry: cargo +stable clippy --workspace --all-targets --all-features -- + pass_filenames: false + types: [file, rust] + language: system diff --git a/Cargo.lock b/Cargo.lock index 3e5831ee..934eaac8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,6 +268,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e9666f4a9a948d4f1dff0c08a4512b0f7c86414b23960104c243c10d79f4c3" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" + [[package]] name = "deranged" version = "0.4.1" @@ -305,6 +321,21 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dtor" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222ef136a1c687d4aa0395c175f2c4586e379924c352fd02f7870cf7de783c23" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" + [[package]] name = "dunce" version = "1.0.5" @@ -1115,6 +1146,7 @@ dependencies = [ "clap", "clap_complete", "clap_mangen", + "ctor", "dns-lookup", "libc", "nix", @@ -1146,6 +1178,7 @@ dependencies = [ "uu_rev", "uu_setsid", "uucore", + "uutests", "xattr", ] @@ -1347,6 +1380,26 @@ version = "0.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb6d972f580f8223cb7052d8580aea2b7061e368cf476de32ea9457b19459ed" +[[package]] +name = "uutests" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f33bc1f552cd82939d3e07867b118ed7ef7bc0fef04b330e1ac69f98593cb22" +dependencies = [ + "ctor", + "glob", + "libc", + "nix", + "pretty_assertions", + "rand 0.9.0", + "regex", + "rlimit", + "tempfile", + "time", + "uucore", + "xattr", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 3798c367..1a375d44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ tempfile = "3.9.0" textwrap = { version = "0.16.0", features = ["terminal_size"] } thiserror = "2.0" uucore = "0.0.30" +uutests = "0.0.30" xattr = "1.3.1" [dependencies] @@ -107,7 +108,9 @@ pretty_assertions = "1" rand = { workspace = true } regex = { workspace = true } tempfile = { workspace = true } +uutests = { workspace = true } uucore = { workspace = true, features = ["entries", "process", "signals"] } +ctor = "0.4.1" [target.'cfg(unix)'.dev-dependencies] nix = { workspace = true, features = ["term"] } diff --git a/tests/by-util/test_blockdev.rs b/tests/by-util/test_blockdev.rs index 0fed69a7..0f0b689c 100644 --- a/tests/by-util/test_blockdev.rs +++ b/tests/by-util/test_blockdev.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { @@ -23,9 +25,12 @@ fn test_report_mutually_exclusive_with_others() { #[cfg(target_os = "linux")] mod linux { - use crate::common::util::TestScenario; use regex::Regex; + use uutests::new_ucmd; + use uutests::util::TestScenario; + use uutests::util_name; + #[test] fn test_fails_on_first_error() { new_ucmd!() @@ -56,7 +61,9 @@ mod linux { #[cfg(not(target_os = "linux"))] mod non_linux { - use crate::common::util::TestScenario; + use uutests::new_ucmd; + use uutests::util::TestScenario; + use uutests::util_name; #[test] fn test_fails_on_unsupported_platforms() { diff --git a/tests/by-util/test_ctrlaltdel.rs b/tests/by-util/test_ctrlaltdel.rs index b3c8a4f0..130cd81f 100644 --- a/tests/by-util/test_ctrlaltdel.rs +++ b/tests/by-util/test_ctrlaltdel.rs @@ -4,7 +4,11 @@ // file that was distributed with this source code. #[cfg(target_os = "linux")] -use crate::common::util::TestScenario; +use uutests::new_ucmd; +#[cfg(target_os = "linux")] +use uutests::util::TestScenario; +#[cfg(target_os = "linux")] +use uutests::util_name; #[test] #[cfg(target_os = "linux")] diff --git a/tests/by-util/test_dmesg.rs b/tests/by-util/test_dmesg.rs index f1b271b3..fa85fd5c 100644 --- a/tests/by-util/test_dmesg.rs +++ b/tests/by-util/test_dmesg.rs @@ -2,7 +2,10 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; + +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_fsfreeze.rs b/tests/by-util/test_fsfreeze.rs index b55daa8b..7bd8d307 100644 --- a/tests/by-util/test_fsfreeze.rs +++ b/tests/by-util/test_fsfreeze.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { @@ -23,7 +25,10 @@ fn test_operations_mutually_exclusive() { #[cfg(target_os = "linux")] mod linux { - use crate::common::util::TestScenario; + + use uutests::new_ucmd; + use uutests::util::TestScenario; + use uutests::util_name; #[test] fn test_fails_on_non_existing_path() { @@ -48,7 +53,9 @@ mod linux { #[cfg(not(target_os = "linux"))] mod non_linux { - use crate::common::util::TestScenario; + use uutests::new_ucmd; + use uutests::util::TestScenario; + use uutests::util_name; #[test] fn test_fails_on_unsupported_platforms() { diff --git a/tests/by-util/test_last.rs b/tests/by-util/test_last.rs index 47ec8f51..33d90379 100644 --- a/tests/by-util/test_last.rs +++ b/tests/by-util/test_last.rs @@ -4,8 +4,13 @@ // file that was distributed with this source code. // spell-checker:ignore (words) symdir somefakedir +use uutests::at_and_ucmd; #[cfg(unix)] -use crate::common::util::TestScenario; +use uutests::new_ucmd; +#[cfg(unix)] +use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util_name; #[cfg(unix)] use regex::Regex; diff --git a/tests/by-util/test_lscpu.rs b/tests/by-util/test_lscpu.rs index 474001d8..f80e6901 100644 --- a/tests/by-util/test_lscpu.rs +++ b/tests/by-util/test_lscpu.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_lslocks.rs b/tests/by-util/test_lslocks.rs index 3cd9dcd1..c1b62abe 100644 --- a/tests/by-util/test_lslocks.rs +++ b/tests/by-util/test_lslocks.rs @@ -4,7 +4,11 @@ // file that was distributed with this source code. #[cfg(target_os = "linux")] -use crate::common::util::TestScenario; +use uutests::new_ucmd; +#[cfg(target_os = "linux")] +use uutests::util::TestScenario; +#[cfg(target_os = "linux")] +use uutests::util_name; #[test] #[cfg(target_os = "linux")] diff --git a/tests/by-util/test_lsmem.rs b/tests/by-util/test_lsmem.rs index cc2cbbae..43c055c8 100644 --- a/tests/by-util/test_lsmem.rs +++ b/tests/by-util/test_lsmem.rs @@ -3,8 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; use std::path::Path; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; fn write_file_content(dir: &Path, name: &str, content: &str) { std::fs::create_dir_all(dir).unwrap(); diff --git a/tests/by-util/test_mcookie.rs b/tests/by-util/test_mcookie.rs index e711c432..c2075818 100644 --- a/tests/by-util/test_mcookie.rs +++ b/tests/by-util/test_mcookie.rs @@ -7,7 +7,9 @@ use std::io::Write; use tempfile::NamedTempFile; -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_mesg.rs b/tests/by-util/test_mesg.rs index 7715a8d3..9d05a437 100644 --- a/tests/by-util/test_mesg.rs +++ b/tests/by-util/test_mesg.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_verb() { @@ -24,7 +26,9 @@ fn test_no_terminal() { #[cfg(not(target_family = "unix"))] mod non_unix { - use crate::common::util::TestScenario; + use uutests::new_ucmd; + use uutests::util::TestScenario; + use uutests::util_name; #[test] fn test_fails_on_unsupported_platforms() { diff --git a/tests/by-util/test_mountpoint.rs b/tests/by-util/test_mountpoint.rs index ac8f5941..11f586ad 100644 --- a/tests/by-util/test_mountpoint.rs +++ b/tests/by-util/test_mountpoint.rs @@ -3,8 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; - +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); diff --git a/tests/by-util/test_renice.rs b/tests/by-util/test_renice.rs index 94285a47..b3624f85 100644 --- a/tests/by-util/test_renice.rs +++ b/tests/by-util/test_renice.rs @@ -4,7 +4,9 @@ // file that was distributed with this source code. // spell-checker:ignore (words) symdir somefakedir -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_rev.rs b/tests/by-util/test_rev.rs index 6fe73356..0e8fc8f7 100644 --- a/tests/by-util/test_rev.rs +++ b/tests/by-util/test_rev.rs @@ -3,7 +3,10 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::at_and_ucmd; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_setsid.rs b/tests/by-util/test_setsid.rs index b7b9bc68..48866b01 100644 --- a/tests/by-util/test_setsid.rs +++ b/tests/by-util/test_setsid.rs @@ -5,8 +5,11 @@ #[cfg(target_family = "unix")] mod unix { - use crate::common::util::{TestScenario, UCommand, TESTS_BINARY}; - + use uutests::new_ucmd; + use uutests::util::get_tests_binary; + use uutests::util::TestScenario; + use uutests::util::UCommand; + use uutests::util_name; #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); @@ -77,8 +80,11 @@ mod unix { #[test] fn unprivileged_user_cannot_steal_controlling_tty() { - let shell_cmd = - format!("{TESTS_BINARY} setsid -w -c {TESTS_BINARY} setsid -w -c /b/usrin/true"); + let shell_cmd = format!( + "{} setsid -w -c {} setsid -w -c /b/usrin/true", + get_tests_binary(), + get_tests_binary() + ); UCommand::new() .terminal_simulation(true) .arg(&shell_cmd) @@ -92,7 +98,8 @@ mod unix { #[test] fn unprivileged_user_can_take_new_controlling_tty() { let shell_cmd = format!( - "/usr/bin/cat /proc/self/stat; {TESTS_BINARY} setsid -w -c /usr/bin/cat /proc/self/stat" + "/usr/bin/cat /proc/self/stat; {} setsid -w -c /usr/bin/cat /proc/self/stat", + get_tests_binary() ); let cmd_result = UCommand::new() @@ -124,7 +131,8 @@ mod unix { #[test] fn setsid_takes_session_leadership() { let shell_cmd = format!( - "/usr/bin/cat /proc/self/stat; {TESTS_BINARY} setsid /usr/bin/cat /proc/self/stat" + "/usr/bin/cat /proc/self/stat; {} setsid /usr/bin/cat /proc/self/stat", + get_tests_binary() ); let cmd_result = UCommand::new() @@ -161,7 +169,9 @@ mod unix { #[cfg(not(target_family = "unix"))] mod non_unix { - use crate::common::util::TestScenario; + use uutests::new_ucmd; + use uutests::util::TestScenario; + use uutests::util_name; #[test] fn unsupported_platforms() { diff --git a/tests/common/macros.rs b/tests/common/macros.rs deleted file mode 100644 index 4902ca49..00000000 --- a/tests/common/macros.rs +++ /dev/null @@ -1,93 +0,0 @@ -// 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. - -/// Platform-independent helper for constructing a `PathBuf` from individual elements -#[macro_export] -macro_rules! path_concat { - ($e:expr, ..$n:expr) => {{ - use std::path::PathBuf; - let n = $n; - let mut pb = PathBuf::new(); - for _ in 0..n { - pb.push($e); - } - pb.to_str().unwrap().to_owned() - }}; - ($($e:expr),*) => {{ - use std::path::PathBuf; - let mut pb = PathBuf::new(); - $( - pb.push($e); - )* - pb.to_str().unwrap().to_owned() - }}; -} - -/// Deduce the name of the test binary from the test filename. -/// -/// e.g.: `tests/by-util/test_cat.rs` -> `cat` -#[macro_export] -macro_rules! util_name { - () => { - module_path!() - .split("_") - .nth(1) - .and_then(|s| s.split("::").next()) - .expect("no test name") - }; -} - -/// Convenience macro for acquiring a [`UCommand`] builder. -/// -/// Returns the following: -/// - a [`UCommand`] builder for invoking the binary to be tested -/// -/// This macro is intended for quick, single-call tests. For more complex tests -/// that require multiple invocations of the tested binary, see [`TestScenario`] -/// -/// [`UCommand`]: crate::tests::common::util::UCommand -/// [`TestScenario]: crate::tests::common::util::TestScenario -#[macro_export] -macro_rules! new_ucmd { - () => { - TestScenario::new(util_name!()).ucmd() - }; -} - -/// Convenience macro for acquiring a [`UCommand`] builder and a test path. -/// -/// Returns a tuple containing the following: -/// - an [`AtPath`] that points to a unique temporary test directory -/// - a [`UCommand`] builder for invoking the binary to be tested -/// -/// This macro is intended for quick, single-call tests. For more complex tests -/// that require multiple invocations of the tested binary, see [`TestScenario`] -/// -/// [`UCommand`]: crate::tests::common::util::UCommand -/// [`AtPath`]: crate::tests::common::util::AtPath -/// [`TestScenario]: crate::tests::common::util::TestScenario -#[macro_export] -macro_rules! at_and_ucmd { - () => {{ - let ts = TestScenario::new(util_name!()); - (ts.fixtures.clone(), ts.ucmd()) - }}; -} - -/// If `common::util::expected_result` returns an error, i.e. the `util` in `$PATH` doesn't -/// include a coreutils version string or the version is too low, -/// this macro can be used to automatically skip the test and print the reason. -#[macro_export] -macro_rules! unwrap_or_return { - ( $e:expr ) => { - match $e { - Ok(x) => x, - Err(e) => { - println!("test skipped: {}", e); - return; - } - } - }; -} diff --git a/tests/common/mod.rs b/tests/common/mod.rs deleted file mode 100644 index 05e2b138..00000000 --- a/tests/common/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -// 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. -#[macro_use] -pub mod macros; -pub mod random; -pub mod util; diff --git a/tests/common/random.rs b/tests/common/random.rs deleted file mode 100644 index 72a2b079..00000000 --- a/tests/common/random.rs +++ /dev/null @@ -1,340 +0,0 @@ -// This file is part of the uutils util-linux package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use rand::distr::{Distribution, Uniform}; -use rand::{rng, Rng}; - -/// Samples alphanumeric characters `[A-Za-z0-9]` including newline `\n` -/// -/// # Examples -/// -/// ```rust,ignore -/// use rand::{Rng, thread_rng}; -/// -/// let vec = thread_rng() -/// .sample_iter(AlphanumericNewline) -/// .take(10) -/// .collect::>(); -/// println!("Random chars: {}", String::from_utf8(vec).unwrap()); -/// ``` -#[derive(Clone, Copy, Debug)] -pub struct AlphanumericNewline; - -impl AlphanumericNewline { - /// The charset to act upon - const CHARSET: &'static [u8] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\n"; - - /// Generate a random byte from [`Self::CHARSET`] and return it as `u8`. - /// - /// # Arguments - /// - /// * `rng`: A [`rand::Rng`] - /// - /// returns: u8 - fn random(rng: &mut R) -> u8 - where - R: Rng + ?Sized, - { - let idx = rng.random_range(0..Self::CHARSET.len()); - Self::CHARSET[idx] - } -} - -impl Distribution for AlphanumericNewline { - fn sample(&self, rng: &mut R) -> u8 { - Self::random(rng) - } -} - -/// Generate a random string from a [`Distribution`] -/// -/// # Examples -/// -/// ```rust,ignore -/// use crate::common::random::{AlphanumericNewline, RandomString}; -/// use rand::distributions::Alphanumeric; -/// -/// // generates a 100 byte string with characters from AlphanumericNewline -/// let random_string = RandomString::generate(AlphanumericNewline, 100); -/// assert_eq!(100, random_string.len()); -/// -/// // generates a 100 byte string with 10 newline characters not ending with a newline -/// let string = RandomString::generate_with_delimiter(Alphanumeric, b'\n', 10, false, 100); -/// assert_eq!(100, random_string.len()); -/// ``` -pub struct RandomString; - -impl RandomString { - /// Generate a random string from the given [`Distribution`] with the given `length` in bytes. - /// - /// # Arguments - /// - /// * `dist`: A u8 [`Distribution`] - /// * `length`: the length of the resulting string in bytes - /// - /// returns: String - pub fn generate(dist: D, length: usize) -> String - where - D: Distribution, - { - rng() - .sample_iter(dist) - .take(length) - .map(|b| b as char) - .collect() - } - - /// Generate a random string from the [`Distribution`] with the given `length` in bytes. The - /// function takes a `delimiter`, which is randomly distributed in the string, such that exactly - /// `num_delimiter` amount of `delimiter`s occur. If `end_with_delimiter` is set, then the - /// string ends with the delimiter, else the string does not end with the delimiter. - /// - /// # Arguments - /// - /// * `dist`: A `u8` [`Distribution`] - /// * `delimiter`: A `u8` delimiter, which does not need to be included in the `Distribution` - /// * `num_delimiter`: The number of `delimiter`s contained in the resulting string - /// * `end_with_delimiter`: If the string shall end with the given delimiter - /// * `length`: the length of the resulting string in bytes - /// - /// returns: String - /// - /// # Examples - /// - /// ```rust,ignore - /// use crate::common::random::{AlphanumericNewline, RandomString}; - /// - /// // generates a 100 byte string with 10 '\0' byte characters not ending with a '\0' byte - /// let string = RandomString::generate_with_delimiter(AlphanumericNewline, 0, 10, false, 100); - /// assert_eq!(100, random_string.len()); - /// assert_eq!( - /// 10, - /// random_string.as_bytes().iter().filter(|p| **p == 0).count() - /// ); - /// assert!(!random_string.as_bytes().ends_with(&[0])); - /// ``` - pub fn generate_with_delimiter( - dist: D, - delimiter: u8, - num_delimiter: usize, - end_with_delimiter: bool, - length: usize, - ) -> String - where - D: Distribution, - { - if length == 0 { - return String::new(); - } else if length == 1 { - return if num_delimiter > 0 { - String::from(delimiter as char) - } else { - String::from(rng().sample(&dist) as char) - }; - } - - let samples = length - 1; - let mut result: Vec = rng().sample_iter(&dist).take(samples).collect(); - - if num_delimiter == 0 { - result.push(rng().sample(&dist)); - return String::from_utf8(result).unwrap(); - } - - let num_delimiter = if end_with_delimiter { - num_delimiter - 1 - } else { - num_delimiter - }; - - // safe to unwrap because samples is at least 1, thus high > low - let between = Uniform::new(0, samples).unwrap(); - for _ in 0..num_delimiter { - let mut pos = between.sample(&mut rng()); - let turn = pos; - while result[pos] == delimiter { - pos += 1; - if pos >= samples { - pos = 0; - } - if pos == turn { - break; - } - } - result[pos] = delimiter; - } - - if end_with_delimiter { - result.push(delimiter); - } else { - result.push(rng().sample(&dist)); - } - - String::from_utf8(result).unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::distr::Alphanumeric; - - #[test] - fn test_random_string_generate() { - let random_string = RandomString::generate(AlphanumericNewline, 0); - assert_eq!(0, random_string.len()); - - let random_string = RandomString::generate(AlphanumericNewline, 1); - assert_eq!(1, random_string.len()); - - let random_string = RandomString::generate(AlphanumericNewline, 100); - assert_eq!(100, random_string.len()); - } - - #[test] - fn test_random_string_generate_with_delimiter_when_length_is_zero() { - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 0, false, 0); - assert_eq!(0, random_string.len()); - } - - #[test] - fn test_random_string_generate_with_delimiter_when_num_delimiter_is_greater_than_length() { - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 2, false, 1); - assert_eq!(1, random_string.len()); - assert!(random_string.as_bytes().contains(&0)); - assert!(random_string.as_bytes().ends_with(&[0])); - } - - #[test] - fn test_random_string_generate_with_delimiter_should_end_with_delimiter() { - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, true, 1); - assert_eq!(1, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, false, 1); - assert_eq!(1, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, true, 2); - assert_eq!(2, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 2, true, 2); - assert_eq!(2, random_string.len()); - assert_eq!( - 2, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, true, 3); - assert_eq!(3, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - } - - #[test] - fn test_random_string_generate_with_delimiter_should_not_end_with_delimiter() { - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 0, false, 1); - assert_eq!(1, random_string.len()); - assert_eq!( - 0, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 0, true, 1); - assert_eq!(1, random_string.len()); - assert_eq!( - 0, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, false, 2); - assert_eq!(2, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, false, 3); - assert_eq!(3, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 2, false, 3); - assert_eq!(3, random_string.len()); - assert_eq!( - 2, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - } - - #[test] - fn test_generate_with_delimiter_with_greater_length() { - let random_string = - RandomString::generate_with_delimiter(Alphanumeric, 0, 100, false, 1000); - assert_eq!(1000, random_string.len()); - assert_eq!( - 100, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 100, true, 1000); - assert_eq!(1000, random_string.len()); - assert_eq!( - 100, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - } - - /// Originally used to exclude an error within the `random` module. The two - /// affected tests timed out on windows, but only in the ci. These tests are - /// also the source for the concrete numbers. The timed out tests are - /// `test_tail.rs::test_pipe_when_lines_option_given_input_size_has_multiple_size_of_buffer_size` - /// `test_tail.rs::test_pipe_when_bytes_option_given_input_size_has_multiple_size_of_buffer_size`. - #[test] - fn test_generate_random_strings_when_length_is_around_critical_buffer_sizes() { - let length = 8192 * 3; - let random_string = RandomString::generate(AlphanumericNewline, length); - assert_eq!(length, random_string.len()); - - let length = 8192 * 3 + 1; - let random_string = - RandomString::generate_with_delimiter(Alphanumeric, b'\n', 100, true, length); - assert_eq!(length, random_string.len()); - assert_eq!( - 100, - random_string - .as_bytes() - .iter() - .filter(|p| **p == b'\n') - .count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - } -} diff --git a/tests/common/util.rs b/tests/common/util.rs deleted file mode 100644 index b00a4071..00000000 --- a/tests/common/util.rs +++ /dev/null @@ -1,3701 +0,0 @@ -// This file is part of the uutils util-linux package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized openpty winsize xpixel ypixel - -#![allow(dead_code, unexpected_cfgs)] - -#[cfg(unix)] -use nix::pty::OpenptyResult; -use pretty_assertions::assert_eq; -#[cfg(any(target_os = "linux", target_os = "android"))] -use rlimit::prlimit; -#[cfg(unix)] -use std::borrow::Cow; -use std::collections::VecDeque; -#[cfg(not(windows))] -use std::ffi::CString; -use std::ffi::{OsStr, OsString}; -use std::fs::{self, hard_link, remove_file, File, OpenOptions}; -use std::io::{self, BufWriter, Read, Result, Write}; -#[cfg(unix)] -use std::os::fd::OwnedFd; -#[cfg(unix)] -use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file, PermissionsExt}; -#[cfg(unix)] -use std::os::unix::process::ExitStatusExt; -#[cfg(windows)] -use std::os::windows::fs::{symlink_dir, symlink_file}; -#[cfg(windows)] -use std::path::MAIN_SEPARATOR; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command, ExitStatus, Output, Stdio}; -use std::rc::Rc; -use std::sync::mpsc::{self, RecvTimeoutError}; -use std::thread::{sleep, JoinHandle}; -use std::time::{Duration, Instant}; -use std::{env, hint, mem, thread}; -use tempfile::{Builder, TempDir}; - -static TESTS_DIR: &str = "tests"; -static FIXTURES_DIR: &str = "fixtures"; - -static ALREADY_RUN: &str = " you have already run this UCommand, if you want to run \ - another command in the same test, use TestScenario::new instead of \ - testing();"; -static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly."; - -static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin"; -static END_OF_TRANSMISSION_SEQUENCE: &[u8] = &[b'\n', 0x04]; - -pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_util-linux"); -pub const PATH: &str = env!("PATH"); - -/// Default environment variables to run the commands with -const DEFAULT_ENV: [(&str, &str); 2] = [("LC_ALL", "C"), ("TZ", "UTC")]; - -/// Test if the program is running under CI -pub fn is_ci() -> bool { - std::env::var("CI").is_ok_and(|s| s.eq_ignore_ascii_case("true")) -} - -/// Read a test scenario fixture, returning its bytes -fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> Vec { - let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); - AtPath::new(tmpdir_path).read_bytes(file_rel_path.as_ref().to_str().unwrap()) -} - -/// A command result is the outputs of a command (streams and status code) -/// within a struct which has convenience assertion functions about those outputs -#[derive(Debug, Clone)] -pub struct CmdResult { - /// bin_path provided by `TestScenario` or `UCommand` - bin_path: PathBuf, - /// util_name provided by `TestScenario` or `UCommand` - util_name: Option, - //tmpd is used for convenience functions for asserts against fixtures - tmpd: Option>, - /// exit status for command (if there is one) - exit_status: Option, - /// captured standard output after running the Command - stdout: Vec, - /// captured standard error after running the Command - stderr: Vec, -} - -impl CmdResult { - pub fn new( - bin_path: S, - util_name: Option, - tmpd: Option>, - exit_status: Option, - stdout: U, - stderr: V, - ) -> Self - where - S: Into, - T: AsRef, - U: Into>, - V: Into>, - { - Self { - bin_path: bin_path.into(), - util_name: util_name.map(|s| s.as_ref().into()), - tmpd, - exit_status, - stdout: stdout.into(), - stderr: stderr.into(), - } - } - - /// Apply a function to `stdout` as bytes and return a new [`CmdResult`] - pub fn stdout_apply<'a, F, R>(&'a self, function: F) -> Self - where - F: Fn(&'a [u8]) -> R, - R: Into>, - { - Self::new( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - self.exit_status, - function(&self.stdout), - self.stderr.as_slice(), - ) - } - - /// Apply a function to `stdout` as `&str` and return a new [`CmdResult`] - pub fn stdout_str_apply<'a, F, R>(&'a self, function: F) -> Self - where - F: Fn(&'a str) -> R, - R: Into>, - { - Self::new( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - self.exit_status, - function(self.stdout_str()), - self.stderr.as_slice(), - ) - } - - /// Apply a function to `stderr` as bytes and return a new [`CmdResult`] - pub fn stderr_apply<'a, F, R>(&'a self, function: F) -> Self - where - F: Fn(&'a [u8]) -> R, - R: Into>, - { - Self::new( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - self.exit_status, - self.stdout.as_slice(), - function(&self.stderr), - ) - } - - /// Apply a function to `stderr` as `&str` and return a new [`CmdResult`] - pub fn stderr_str_apply<'a, F, R>(&'a self, function: F) -> Self - where - F: Fn(&'a str) -> R, - R: Into>, - { - Self::new( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - self.exit_status, - self.stdout.as_slice(), - function(self.stderr_str()), - ) - } - - /// Assert `stdout` as bytes with a predicate function returning a `bool`. - #[track_caller] - pub fn stdout_check<'a, F>(&'a self, predicate: F) -> &'a Self - where - F: Fn(&'a [u8]) -> bool, - { - assert!( - predicate(&self.stdout), - "Predicate for stdout as `bytes` evaluated to false.\nstdout='{:?}'\nstderr='{:?}'\n", - &self.stdout, - &self.stderr - ); - self - } - - /// Assert `stdout` as `&str` with a predicate function returning a `bool`. - #[track_caller] - pub fn stdout_str_check<'a, F>(&'a self, predicate: F) -> &'a Self - where - F: Fn(&'a str) -> bool, - { - assert!( - predicate(self.stdout_str()), - "Predicate for stdout as `str` evaluated to false.\nstdout='{}'\nstderr='{}'\n", - self.stdout_str(), - self.stderr_str() - ); - self - } - - /// Assert `stderr` as bytes with a predicate function returning a `bool`. - #[track_caller] - pub fn stderr_check<'a, F>(&'a self, predicate: F) -> &'a Self - where - F: Fn(&'a [u8]) -> bool, - { - assert!( - predicate(&self.stderr), - "Predicate for stderr as `bytes` evaluated to false.\nstdout='{:?}'\nstderr='{:?}'\n", - &self.stdout, - &self.stderr - ); - self - } - - /// Assert `stderr` as `&str` with a predicate function returning a `bool`. - #[track_caller] - pub fn stderr_str_check<'a, F>(&'a self, predicate: F) -> &'a Self - where - F: Fn(&'a str) -> bool, - { - assert!( - predicate(self.stderr_str()), - "Predicate for stderr as `str` evaluated to false.\nstdout='{}'\nstderr='{}'\n", - self.stdout_str(), - self.stderr_str() - ); - self - } - - /// Return the exit status of the child process, if any. - /// - /// Returns None if the child process is still running or hasn't been started. - pub fn try_exit_status(&self) -> Option { - self.exit_status - } - - /// Return the exit status of the child process. - /// - /// # Panics - /// - /// If the child process is still running or hasn't been started. - pub fn exit_status(&self) -> ExitStatus { - self.try_exit_status() - .expect("Program must be run first or has not finished, yet") - } - - /// Return the signal the child process received if any. - /// - /// # Platform specific behavior - /// - /// This method is only available on unix systems. - #[cfg(unix)] - pub fn signal(&self) -> Option { - self.exit_status().signal() - } - - /// Assert that the given signal `value` equals the signal the child process received. - /// - /// See also [`std::os::unix::process::ExitStatusExt::signal`]. - /// - /// # Platform specific behavior - /// - /// This assertion method is only available on unix systems. - #[cfg(unix)] - #[track_caller] - pub fn signal_is(&self, value: i32) -> &Self { - let actual = self.signal().unwrap_or_else(|| { - panic!( - "Expected process to be terminated by the '{}' signal, but exit status is: '{}'", - value, - self.try_exit_status() - .map_or("Not available".to_string(), |e| e.to_string()) - ) - }); - - assert_eq!(actual, value); - self - } - - /// Assert that the given signal `name` equals the signal the child process received. - /// - /// Strings like `SIGINT`, `INT` or a number like `15` are all valid names. See also - /// [`std::os::unix::process::ExitStatusExt::signal`] and - /// [`uucore::signals::signal_by_name_or_value`] - /// - /// # Platform specific behavior - /// - /// This assertion method is only available on unix systems. - #[cfg(unix)] - #[track_caller] - pub fn signal_name_is(&self, name: &str) -> &Self { - use uucore::signals::signal_by_name_or_value; - let expected: i32 = signal_by_name_or_value(name) - .unwrap_or_else(|| panic!("Invalid signal name or value: '{name}'")) - .try_into() - .unwrap(); - - let actual = self.signal().unwrap_or_else(|| { - panic!( - "Expected process to be terminated by the '{}' signal, but exit status is: '{}'", - name, - self.try_exit_status() - .map_or("Not available".to_string(), |e| e.to_string()) - ) - }); - - assert_eq!(actual, expected); - self - } - - /// Returns a reference to the program's standard output as a slice of bytes - pub fn stdout(&self) -> &[u8] { - &self.stdout - } - - /// Returns the program's standard output as a string slice - pub fn stdout_str(&self) -> &str { - std::str::from_utf8(&self.stdout).unwrap() - } - - /// Returns the program's standard output as a string - /// consumes self - pub fn stdout_move_str(self) -> String { - String::from_utf8(self.stdout).unwrap() - } - - /// Returns the program's standard output as a vec of bytes - /// consumes self - pub fn stdout_move_bytes(self) -> Vec { - self.stdout - } - - /// Returns a reference to the program's standard error as a slice of bytes - pub fn stderr(&self) -> &[u8] { - &self.stderr - } - - /// Returns the program's standard error as a string slice - pub fn stderr_str(&self) -> &str { - std::str::from_utf8(&self.stderr).unwrap() - } - - /// Returns the program's standard error as a string - /// consumes self - pub fn stderr_move_str(self) -> String { - String::from_utf8(self.stderr).unwrap() - } - - /// Returns the program's standard error as a vec of bytes - /// consumes self - pub fn stderr_move_bytes(self) -> Vec { - self.stderr - } - - /// Returns the program's exit code - /// Panics if not run or has not finished yet for example when run with `run_no_wait()` - pub fn code(&self) -> i32 { - self.exit_status().code().unwrap() - } - - #[track_caller] - pub fn code_is(&self, expected_code: i32) -> &Self { - assert_eq!(self.code(), expected_code); - self - } - - /// Returns the program's `TempDir` - /// Panics if not present - pub fn tmpd(&self) -> Rc { - match &self.tmpd { - Some(ptr) => ptr.clone(), - None => panic!("Command not associated with a TempDir"), - } - } - - /// Returns whether the program succeeded - pub fn succeeded(&self) -> bool { - self.exit_status.map_or(true, |e| e.success()) - } - - /// asserts that the command resulted in a success (zero) status code - #[track_caller] - pub fn success(&self) -> &Self { - assert!( - self.succeeded(), - "Command was expected to succeed. Exit code: {}.\nstdout = {}\n stderr = {}", - self.exit_status() - .code() - .map_or("n/a".to_string(), |code| code.to_string()), - self.stdout_str(), - self.stderr_str() - ); - self - } - - /// asserts that the command resulted in a failure (non-zero) status code - #[track_caller] - pub fn failure(&self) -> &Self { - assert!( - !self.succeeded(), - "Command was expected to fail.\nstdout = {}\n stderr = {}", - self.stdout_str(), - self.stderr_str() - ); - self - } - - /// asserts that the command resulted in empty (zero-length) stderr stream output - /// generally, it's better to use `stdout_only()` instead, - /// but you might find yourself using this function if - /// 1. you can not know exactly what stdout will be or - /// 2. you know that stdout will also be empty - #[track_caller] - pub fn no_stderr(&self) -> &Self { - assert!( - self.stderr.is_empty(), - "Expected stderr to be empty, but it's:\n{}", - self.stderr_str() - ); - self - } - - /// asserts that the command resulted in empty (zero-length) stderr stream output - /// unless asserting there was neither stdout or stderr, `stderr_only` is usually a better choice - /// generally, it's better to use `stderr_only()` instead, - /// but you might find yourself using this function if - /// 1. you can not know exactly what stderr will be or - /// 2. you know that stderr will also be empty - #[track_caller] - pub fn no_stdout(&self) -> &Self { - assert!( - self.stdout.is_empty(), - "Expected stdout to be empty, but it's:\n{}", - self.stdout_str() - ); - self - } - - /// Assert that there is output to neither stderr nor stdout. - #[track_caller] - pub fn no_output(&self) -> &Self { - self.no_stdout().no_stderr() - } - - /// asserts that the command resulted in stdout stream output that equals the - /// passed in value, trailing whitespace are kept to force strict comparison (#1235) - /// `stdout_only()` is a better choice unless stderr may or will be non-empty - #[track_caller] - pub fn stdout_is>(&self, msg: T) -> &Self { - assert_eq!(self.stdout_str(), String::from(msg.as_ref())); - self - } - - /// like `stdout_is`, but succeeds if any elements of `expected` matches stdout. - #[track_caller] - pub fn stdout_is_any + std::fmt::Debug>(&self, expected: &[T]) -> &Self { - assert!( - expected.iter().any(|msg| self.stdout_str() == msg.as_ref()), - "stdout was {}\nExpected any of {:#?}", - self.stdout_str(), - expected - ); - self - } - - /// Like `stdout_is` but newlines are normalized to `\n`. - #[track_caller] - pub fn normalized_newlines_stdout_is>(&self, msg: T) -> &Self { - let msg = msg.as_ref().replace("\r\n", "\n"); - assert_eq!(self.stdout_str().replace("\r\n", "\n"), msg); - self - } - - /// asserts that the command resulted in stdout stream output, - /// whose bytes equal those of the passed in slice - #[track_caller] - pub fn stdout_is_bytes>(&self, msg: T) -> &Self { - assert_eq!(self.stdout, msg.as_ref(), - "stdout as bytes wasn't equal to expected bytes. Result as strings:\nstdout ='{:?}'\nexpected='{:?}'", - std::str::from_utf8(&self.stdout), - std::str::from_utf8(msg.as_ref()), - ); - self - } - - /// like `stdout_is()`, but expects the contents of the file at the provided relative path - #[track_caller] - pub fn stdout_is_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_is(String::from_utf8(contents).unwrap()) - } - - /// Assert that the bytes of stdout exactly match those of the given file. - /// - /// Contrast this with [`CmdResult::stdout_is_fixture`], which - /// decodes the contents of the file as a UTF-8 [`String`] before - /// comparison with stdout. - /// - /// # Examples - /// - /// Use this method in a unit test like this: - /// - /// ```rust,ignore - /// #[test] - /// fn test_something() { - /// new_ucmd!().succeeds().stdout_is_fixture_bytes("expected.bin"); - /// } - /// ``` - #[track_caller] - pub fn stdout_is_fixture_bytes>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_is_bytes(contents) - } - - /// like `stdout_is_fixture()`, but replaces the data in fixture file based on values provided in `template_vars` - /// command output - #[track_caller] - pub fn stdout_is_templated_fixture>( - &self, - file_rel_path: T, - template_vars: &[(&str, &str)], - ) -> &Self { - let mut contents = - String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); - for kv in template_vars { - contents = contents.replace(kv.0, kv.1); - } - self.stdout_is(contents) - } - - /// like `stdout_is_templated_fixture`, but succeeds if any replacement by `template_vars` results in the actual stdout. - #[track_caller] - pub fn stdout_is_templated_fixture_any>( - &self, - file_rel_path: T, - template_vars: &[Vec<(String, String)>], - ) { - let contents = String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); - let possible_values = template_vars.iter().map(|vars| { - let mut contents = contents.clone(); - for kv in vars { - contents = contents.replace(&kv.0, &kv.1); - } - contents - }); - self.stdout_is_any(&possible_values.collect::>()); - } - - /// assert that the command resulted in stderr stream output that equals the - /// passed in value. - /// - /// `stderr_only` is a better choice unless stdout may or will be non-empty - #[track_caller] - pub fn stderr_is>(&self, msg: T) -> &Self { - assert_eq!(self.stderr_str(), msg.as_ref()); - self - } - - /// asserts that the command resulted in stderr stream output, - /// whose bytes equal those of the passed in slice - #[track_caller] - pub fn stderr_is_bytes>(&self, msg: T) -> &Self { - assert_eq!( - &self.stderr, - msg.as_ref(), - "stderr as bytes wasn't equal to expected bytes. Result as strings:\nstderr ='{:?}'\nexpected='{:?}'", - std::str::from_utf8(&self.stderr), - std::str::from_utf8(msg.as_ref()) - ); - self - } - - /// Like `stdout_is_fixture`, but for stderr - #[track_caller] - pub fn stderr_is_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stderr_is(String::from_utf8(contents).unwrap()) - } - - /// asserts that - /// 1. the command resulted in stdout stream output that equals the - /// passed in value - /// 2. the command resulted in empty (zero-length) stderr stream output - #[track_caller] - pub fn stdout_only>(&self, msg: T) -> &Self { - self.no_stderr().stdout_is(msg) - } - - /// asserts that - /// 1. the command resulted in a stdout stream whose bytes - /// equal those of the passed in value - /// 2. the command resulted in an empty stderr stream - #[track_caller] - pub fn stdout_only_bytes>(&self, msg: T) -> &Self { - self.no_stderr().stdout_is_bytes(msg) - } - - /// like `stdout_only()`, but expects the contents of the file at the provided relative path - #[track_caller] - pub fn stdout_only_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_only_bytes(contents) - } - - /// asserts that - /// 1. the command resulted in stderr stream output that equals the - /// passed in value - /// 2. the command resulted in empty (zero-length) stdout stream output - #[track_caller] - pub fn stderr_only>(&self, msg: T) -> &Self { - self.no_stdout().stderr_is(msg) - } - - /// asserts that - /// 1. the command resulted in a stderr stream whose bytes equal the ones - /// of the passed value - /// 2. the command resulted in an empty stdout stream - #[track_caller] - pub fn stderr_only_bytes>(&self, msg: T) -> &Self { - self.no_stdout().stderr_is_bytes(msg) - } - - #[track_caller] - pub fn fails_silently(&self) -> &Self { - assert!(!self.succeeded()); - assert!(self.stderr.is_empty()); - self - } - - /// asserts that - /// 1. the command resulted in stderr stream output that equals the - /// the following format - /// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."` - /// This the expected format when a `UUsageError` is returned or when `show_error!` is called - /// `msg` should be the same as the one provided to `UUsageError::new` or `show_error!` - /// - /// 2. the command resulted in empty (zero-length) stdout stream output - #[track_caller] - pub fn usage_error>(&self, msg: T) -> &Self { - self.stderr_only(format!( - "{0}: {2}\nTry '{1} {0} --help' for more information.\n", - self.util_name.as_ref().unwrap(), // This shouldn't be called using a normal command - self.bin_path.display(), - msg.as_ref() - )) - } - - #[track_caller] - pub fn stdout_contains>(&self, cmp: T) -> &Self { - assert!( - self.stdout_str().contains(cmp.as_ref()), - "'{}' does not contain '{}'", - self.stdout_str(), - cmp.as_ref() - ); - self - } - - #[track_caller] - pub fn stdout_contains_line>(&self, cmp: T) -> &Self { - assert!( - self.stdout_str().lines().any(|line| line == cmp.as_ref()), - "'{}' does not contain line '{}'", - self.stdout_str(), - cmp.as_ref() - ); - self - } - - #[track_caller] - pub fn stderr_contains>(&self, cmp: T) -> &Self { - assert!( - self.stderr_str().contains(cmp.as_ref()), - "'{}' does not contain '{}'", - self.stderr_str(), - cmp.as_ref() - ); - self - } - - #[track_caller] - pub fn stdout_does_not_contain>(&self, cmp: T) -> &Self { - assert!( - !self.stdout_str().contains(cmp.as_ref()), - "'{}' contains '{}' but should not", - self.stdout_str(), - cmp.as_ref(), - ); - self - } - - #[track_caller] - pub fn stderr_does_not_contain>(&self, cmp: T) -> &Self { - assert!(!self.stderr_str().contains(cmp.as_ref())); - self - } - - #[track_caller] - pub fn stdout_matches(&self, regex: ®ex::Regex) -> &Self { - assert!( - regex.is_match(self.stdout_str()), - "Stdout does not match regex:\n{}", - self.stdout_str() - ); - self - } - - #[track_caller] - pub fn stderr_matches(&self, regex: ®ex::Regex) -> &Self { - assert!( - regex.is_match(self.stderr_str()), - "Stderr does not match regex:\n{}", - self.stderr_str() - ); - self - } - - #[track_caller] - pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &Self { - assert!( - !regex.is_match(self.stdout_str()), - "Stdout matches regex:\n{}", - self.stdout_str() - ); - self - } -} - -pub fn log_info, U: AsRef>(msg: T, par: U) { - println!("{}: {}", msg.as_ref(), par.as_ref()); -} - -pub fn recursive_copy(src: &Path, dest: &Path) -> Result<()> { - if fs::metadata(src)?.is_dir() { - for entry in fs::read_dir(src)? { - let entry = entry?; - let mut new_dest = PathBuf::from(dest); - new_dest.push(entry.file_name()); - if fs::metadata(entry.path())?.is_dir() { - fs::create_dir(&new_dest)?; - recursive_copy(&entry.path(), &new_dest)?; - } else { - fs::copy(entry.path(), new_dest)?; - } - } - } - Ok(()) -} - -pub fn get_root_path() -> &'static str { - if cfg!(windows) { - "C:\\" - } else { - "/" - } -} - -/// Compares the extended attributes (xattrs) of two files or directories. -/// -/// # Returns -/// -/// `true` if both paths have the same set of extended attributes, `false` otherwise. -#[cfg(all(unix, not(target_os = "macos")))] -pub fn compare_xattrs>(path1: P, path2: P) -> bool { - let get_sorted_xattrs = |path: P| { - xattr::list(path) - .map(|attrs| { - let mut attrs = attrs.collect::>(); - attrs.sort(); - attrs - }) - .unwrap_or_else(|_| Vec::new()) - }; - - get_sorted_xattrs(path1) == get_sorted_xattrs(path2) -} - -/// Object-oriented path struct that represents and operates on -/// paths relative to the directory it was constructed for. -#[derive(Clone)] -pub struct AtPath { - pub subdir: PathBuf, -} - -impl AtPath { - pub fn new(subdir: &Path) -> Self { - Self { - subdir: PathBuf::from(subdir), - } - } - - pub fn as_string(&self) -> String { - self.subdir.to_str().unwrap().to_owned() - } - - pub fn plus>(&self, name: P) -> PathBuf { - let mut pathbuf = self.subdir.clone(); - pathbuf.push(name); - pathbuf - } - - pub fn plus_as_string>(&self, name: P) -> String { - self.plus(name).display().to_string() - } - - fn minus(&self, name: &str) -> PathBuf { - let prefixed = PathBuf::from(name); - if prefixed.starts_with(&self.subdir) { - let mut unprefixed = PathBuf::new(); - for component in prefixed.components().skip(self.subdir.components().count()) { - unprefixed.push(component.as_os_str().to_str().unwrap()); - } - unprefixed - } else { - prefixed - } - } - - pub fn minus_as_string(&self, name: &str) -> String { - String::from(self.minus(name).to_str().unwrap()) - } - - pub fn set_readonly(&self, name: &str) { - let metadata = fs::metadata(self.plus(name)).unwrap(); - let mut permissions = metadata.permissions(); - permissions.set_readonly(true); - fs::set_permissions(self.plus(name), permissions).unwrap(); - } - - pub fn open(&self, name: &str) -> File { - log_info("open", self.plus_as_string(name)); - File::open(self.plus(name)).unwrap() - } - - pub fn read(&self, name: &str) -> String { - let mut f = self.open(name); - let mut contents = String::new(); - f.read_to_string(&mut contents) - .unwrap_or_else(|e| panic!("Couldn't read {name}: {e}")); - contents - } - - pub fn read_bytes(&self, name: &str) -> Vec { - let mut f = self.open(name); - let mut contents = Vec::new(); - f.read_to_end(&mut contents) - .unwrap_or_else(|e| panic!("Couldn't read {name}: {e}")); - contents - } - - pub fn write(&self, name: &str, contents: &str) { - log_info("write(default)", self.plus_as_string(name)); - std::fs::write(self.plus(name), contents) - .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); - } - - pub fn write_bytes(&self, name: &str, contents: &[u8]) { - log_info("write(default)", self.plus_as_string(name)); - std::fs::write(self.plus(name), contents) - .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); - } - - pub fn append(&self, name: &str, contents: &str) { - log_info("write(append)", self.plus_as_string(name)); - let mut f = OpenOptions::new() - .append(true) - .create(true) - .open(self.plus(name)) - .unwrap(); - f.write_all(contents.as_bytes()) - .unwrap_or_else(|e| panic!("Couldn't write(append) {name}: {e}")); - } - - pub fn append_bytes(&self, name: &str, contents: &[u8]) { - log_info("write(append)", self.plus_as_string(name)); - let mut f = OpenOptions::new() - .append(true) - .create(true) - .open(self.plus(name)) - .unwrap(); - f.write_all(contents) - .unwrap_or_else(|e| panic!("Couldn't write(append) to {name}: {e}")); - } - - pub fn truncate(&self, name: &str, contents: &str) { - log_info("write(truncate)", self.plus_as_string(name)); - let mut f = OpenOptions::new() - .write(true) - .truncate(true) - .create(true) - .open(self.plus(name)) - .unwrap(); - f.write_all(contents.as_bytes()) - .unwrap_or_else(|e| panic!("Couldn't write(truncate) {name}: {e}")); - } - - pub fn rename(&self, source: &str, target: &str) { - let source = self.plus(source); - let target = self.plus(target); - log_info("rename", format!("{source:?} {target:?}")); - std::fs::rename(&source, &target) - .unwrap_or_else(|e| panic!("Couldn't rename {source:?} -> {target:?}: {e}")); - } - - pub fn remove(&self, source: &str) { - let source = self.plus(source); - log_info("remove", format!("{source:?}")); - std::fs::remove_file(&source).unwrap_or_else(|e| panic!("Couldn't remove {source:?}: {e}")); - } - - pub fn copy(&self, source: &str, target: &str) { - let source = self.plus(source); - let target = self.plus(target); - log_info("copy", format!("{source:?} {target:?}")); - std::fs::copy(&source, &target) - .unwrap_or_else(|e| panic!("Couldn't copy {source:?} -> {target:?}: {e}")); - } - - pub fn rmdir(&self, dir: &str) { - log_info("rmdir", self.plus_as_string(dir)); - fs::remove_dir(self.plus(dir)).unwrap(); - } - - pub fn mkdir>(&self, dir: P) { - let dir = dir.as_ref(); - log_info("mkdir", self.plus_as_string(dir)); - fs::create_dir(self.plus(dir)).unwrap(); - } - - pub fn mkdir_all(&self, dir: &str) { - log_info("mkdir_all", self.plus_as_string(dir)); - fs::create_dir_all(self.plus(dir)).unwrap(); - } - - pub fn make_file(&self, name: &str) -> File { - match File::create(self.plus(name)) { - Ok(f) => f, - Err(e) => panic!("{}", e), - } - } - - pub fn touch>(&self, file: P) { - let file = file.as_ref(); - log_info("touch", self.plus_as_string(file)); - File::create(self.plus(file)).unwrap(); - } - - #[cfg(not(windows))] - pub fn mkfifo(&self, fifo: &str) { - let full_path = self.plus_as_string(fifo); - log_info("mkfifo", &full_path); - unsafe { - let fifo_name: CString = CString::new(full_path).expect("CString creation failed."); - libc::mkfifo(fifo_name.as_ptr(), libc::S_IWUSR | libc::S_IRUSR); - } - } - - #[cfg(not(windows))] - pub fn is_fifo(&self, fifo: &str) -> bool { - unsafe { - let name = CString::new(self.plus_as_string(fifo)).unwrap(); - let mut stat: libc::stat = std::mem::zeroed(); - if libc::stat(name.as_ptr(), &mut stat) >= 0 { - libc::S_IFIFO & stat.st_mode as libc::mode_t != 0 - } else { - false - } - } - } - - pub fn hard_link(&self, original: &str, link: &str) { - log_info( - "hard_link", - format!( - "{},{}", - self.plus_as_string(original), - self.plus_as_string(link) - ), - ); - hard_link(self.plus(original), self.plus(link)).unwrap(); - } - - pub fn symlink_file(&self, original: &str, link: &str) { - log_info( - "symlink", - format!( - "{},{}", - self.plus_as_string(original), - self.plus_as_string(link) - ), - ); - symlink_file(self.plus(original), self.plus(link)).unwrap(); - } - - pub fn relative_symlink_file(&self, original: &str, link: &str) { - #[cfg(windows)] - let original = original.replace('/', &MAIN_SEPARATOR.to_string()); - log_info( - "symlink", - format!("{},{}", &original, &self.plus_as_string(link)), - ); - symlink_file(original, self.plus(link)).unwrap(); - } - - pub fn symlink_dir(&self, original: &str, link: &str) { - log_info( - "symlink", - format!( - "{},{}", - self.plus_as_string(original), - self.plus_as_string(link) - ), - ); - symlink_dir(self.plus(original), self.plus(link)).unwrap(); - } - - pub fn relative_symlink_dir(&self, original: &str, link: &str) { - #[cfg(windows)] - let original = original.replace('/', &MAIN_SEPARATOR.to_string()); - log_info( - "symlink", - format!("{},{}", &original, &self.plus_as_string(link)), - ); - symlink_dir(original, self.plus(link)).unwrap(); - } - - pub fn is_symlink(&self, path: &str) -> bool { - log_info("is_symlink", self.plus_as_string(path)); - match fs::symlink_metadata(self.plus(path)) { - Ok(m) => m.file_type().is_symlink(), - Err(_) => false, - } - } - - pub fn resolve_link(&self, path: &str) -> String { - log_info("resolve_link", self.plus_as_string(path)); - match fs::read_link(self.plus(path)) { - Ok(p) => self.minus_as_string(p.to_str().unwrap()), - Err(_) => String::new(), - } - } - - pub fn read_symlink(&self, path: &str) -> String { - log_info("read_symlink", self.plus_as_string(path)); - fs::read_link(self.plus(path)) - .unwrap() - .to_str() - .unwrap() - .to_owned() - } - - pub fn symlink_metadata(&self, path: &str) -> fs::Metadata { - match fs::symlink_metadata(self.plus(path)) { - Ok(m) => m, - Err(e) => panic!("{}", e), - } - } - - pub fn metadata(&self, path: &str) -> fs::Metadata { - match fs::metadata(self.plus(path)) { - Ok(m) => m, - Err(e) => panic!("{}", e), - } - } - - pub fn file_exists>(&self, path: P) -> bool { - match fs::metadata(self.plus(path)) { - Ok(m) => m.is_file(), - Err(_) => false, - } - } - - /// Decide whether the named symbolic link exists in the test directory. - pub fn symlink_exists(&self, path: &str) -> bool { - match fs::symlink_metadata(self.plus(path)) { - Ok(m) => m.file_type().is_symlink(), - Err(_) => false, - } - } - - pub fn dir_exists(&self, path: &str) -> bool { - match fs::metadata(self.plus(path)) { - Ok(m) => m.is_dir(), - Err(_) => false, - } - } - - pub fn root_dir_resolved(&self) -> String { - log_info("current_directory_resolved", ""); - let s = self - .subdir - .canonicalize() - .unwrap() - .to_str() - .unwrap() - .to_owned(); - - // Due to canonicalize()'s use of GetFinalPathNameByHandleW() on Windows, the resolved path - // starts with '\\?\' to extend the limit of a given path to 32,767 wide characters. - // - // To address this issue, we remove this prepended string if available. - // - // Source: - // http://stackoverflow.com/questions/31439011/getfinalpathnamebyhandle-without-prepended - let prefix = "\\\\?\\"; - - if let Some(stripped) = s.strip_prefix(prefix) { - String::from(stripped) - } else { - s - } - } - - /// Set the permissions of the specified file. - /// - /// # Panics - /// - /// This function panics if there is an error loading the metadata - /// or setting the permissions of the file. - #[cfg(not(windows))] - pub fn set_mode(&self, filename: &str, mode: u32) { - let path = self.plus(filename); - let mut perms = std::fs::metadata(&path).unwrap().permissions(); - perms.set_mode(mode); - std::fs::set_permissions(&path, perms).unwrap(); - } -} - -/// An environment for running a single uutils test case, serves three functions: -/// 1. centralizes logic for locating the uutils binary and calling the utility -/// 2. provides a unique temporary directory for the test case -/// 3. copies over fixtures for the utility to the temporary directory -/// -/// Fixtures can be found under `tests/fixtures/$util_name/` -pub struct TestScenario { - pub bin_path: PathBuf, - pub util_name: String, - pub fixtures: AtPath, - tmpd: Rc, -} - -impl TestScenario { - pub fn new(util_name: T) -> Self - where - T: AsRef, - { - let tmpd = Rc::new(TempDir::new().unwrap()); - let ts = Self { - bin_path: PathBuf::from(TESTS_BINARY), - util_name: util_name.as_ref().into(), - fixtures: AtPath::new(tmpd.as_ref().path()), - tmpd, - }; - let mut fixture_path_builder = env::current_dir().unwrap(); - fixture_path_builder.push(TESTS_DIR); - fixture_path_builder.push(FIXTURES_DIR); - fixture_path_builder.push(util_name.as_ref()); - if let Ok(m) = fs::metadata(&fixture_path_builder) { - if m.is_dir() { - recursive_copy(&fixture_path_builder, &ts.fixtures.subdir).unwrap(); - } - } - ts - } - - /// Returns builder for invoking the target uutils binary. Paths given are - /// treated relative to the environment's unique temporary test directory. - pub fn ucmd(&self) -> UCommand { - UCommand::from_test_scenario(self) - } - - /// Returns builder for invoking any system command. Paths given are treated - /// relative to the environment's unique temporary test directory. - pub fn cmd>(&self, bin_path: S) -> UCommand { - let mut command = UCommand::new(); - command.bin_path(bin_path); - command.temp_dir(self.tmpd.clone()); - command - } - - /// Returns builder for invoking any uutils command. Paths given are treated - /// relative to the environment's unique temporary test directory. - pub fn ccmd>(&self, util_name: S) -> UCommand { - UCommand::with_util(util_name, self.tmpd.clone()) - } -} - -/// A `UCommand` is a builder wrapping an individual Command that provides several additional features: -/// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command -/// and asserting on the results. -/// 2. it tracks arguments provided so that in test cases which may provide variations of an arg in loops -/// the test failure can display the exact call which preceded an assertion failure. -/// 3. it provides convenience construction methods to set the Command uutils utility and temporary directory. -/// -/// Per default `UCommand` runs a command given as an argument in a shell, platform independently. -/// It does so with safety in mind, so the working directory is set to an individual temporary -/// directory and the environment variables are cleared per default. -/// -/// The default behavior can be changed with builder methods: -/// * [`UCommand::with_util`]: Run `util-linux UTIL_NAME` instead of the shell -/// * [`UCommand::from_test_scenario`]: Run `util-linux UTIL_NAME` instead of the shell in the -/// temporary directory of the [`TestScenario`] -/// * [`UCommand::current_dir`]: Sets the working directory -/// * ... -#[derive(Debug, Default)] -pub struct UCommand { - args: VecDeque, - env_vars: Vec<(OsString, OsString)>, - current_dir: Option, - bin_path: Option, - util_name: Option, - has_run: bool, - ignore_stdin_write_error: bool, - stdin: Option, - stdout: Option, - stderr: Option, - bytes_into_stdin: Option>, - #[cfg(any(target_os = "linux", target_os = "android"))] - limits: Vec<(rlimit::Resource, u64, u64)>, - stderr_to_stdout: bool, - timeout: Option, - #[cfg(unix)] - terminal_simulation: bool, - #[cfg(unix)] - terminal_size: Option, - tmpd: Option>, // drop last -} - -impl UCommand { - /// Create a new plain [`UCommand`]. - /// - /// Executes a command that must be given as argument (for example with [`UCommand::arg`] in a - /// shell (`sh -c` on unix platforms or `cmd /C` on windows). - /// - /// Per default the environment is cleared and the working directory is set to an individual - /// temporary directory for safety purposes. - pub fn new() -> Self { - Self { - ..Default::default() - } - } - - /// Create a [`UCommand`] for a specific uutils utility. - /// - /// Sets the temporary directory to `tmpd` and the execution binary to the path where - /// `util-linux` is found. - pub fn with_util(util_name: T, tmpd: Rc) -> Self - where - T: AsRef, - { - let mut ucmd = Self::new(); - ucmd.util_name = Some(util_name.as_ref().into()); - ucmd.bin_path(TESTS_BINARY).temp_dir(tmpd); - ucmd - } - - /// Create a [`UCommand`] from a [`TestScenario`]. - /// - /// The temporary directory and uutils utility are inherited from the [`TestScenario`] and the - /// execution binary is set to `util-linux`. - pub fn from_test_scenario(scene: &TestScenario) -> Self { - Self::with_util(&scene.util_name, scene.tmpd.clone()) - } - - /// Set the execution binary. - /// - /// Make sure the binary found at this path is executable. It's safest to provide the - /// canonicalized path instead of just the name of the executable, since path resolution is not - /// guaranteed to work on all platforms. - fn bin_path(&mut self, bin_path: T) -> &mut Self - where - T: Into, - { - self.bin_path = Some(bin_path.into()); - self - } - - /// Set the temporary directory. - /// - /// Per default an individual temporary directory is created for every [`UCommand`]. If not - /// specified otherwise with [`UCommand::current_dir`] the working directory is set to this - /// temporary directory. - fn temp_dir(&mut self, temp_dir: Rc) -> &mut Self { - self.tmpd = Some(temp_dir); - self - } - - /// Set the working directory for this [`UCommand`] - /// - /// Per default the working directory is set to the [`UCommands`] temporary directory. - pub fn current_dir(&mut self, current_dir: T) -> &mut Self - where - T: Into, - { - self.current_dir = Some(current_dir.into()); - self - } - - pub fn set_stdin>(&mut self, stdin: T) -> &mut Self { - self.stdin = Some(stdin.into()); - self - } - - pub fn set_stdout>(&mut self, stdout: T) -> &mut Self { - self.stdout = Some(stdout.into()); - self - } - - pub fn set_stderr>(&mut self, stderr: T) -> &mut Self { - self.stderr = Some(stderr.into()); - self - } - - pub fn stderr_to_stdout(&mut self) -> &mut Self { - self.stderr_to_stdout = true; - self - } - - /// Add a parameter to the invocation. Path arguments are treated relative - /// to the test environment directory. - pub fn arg>(&mut self, arg: S) -> &mut Self { - self.args.push_back(arg.as_ref().into()); - self - } - - /// Add multiple parameters to the invocation. Path arguments are treated relative - /// to the test environment directory. - pub fn args>(&mut self, args: &[S]) -> &mut Self { - self.args.extend(args.iter().map(|s| s.as_ref().into())); - self - } - - /// provides standard input to feed in to the command when spawned - pub fn pipe_in>>(&mut self, input: T) -> &mut Self { - assert!( - self.bytes_into_stdin.is_none(), - "{}", - MULTIPLE_STDIN_MEANINGLESS - ); - self.set_stdin(Stdio::piped()); - self.bytes_into_stdin = Some(input.into()); - self - } - - /// like `pipe_in()`, but uses the contents of the file at the provided relative path as the piped in data - pub fn pipe_in_fixture>(&mut self, file_rel_path: S) -> &mut Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.pipe_in(contents) - } - - /// Ignores error caused by feeding stdin to the command. - /// This is typically useful to test non-standard workflows - /// like feeding something to a command that does not read it - pub fn ignore_stdin_write_error(&mut self) -> &mut Self { - self.ignore_stdin_write_error = true; - self - } - - pub fn env(&mut self, key: K, val: V) -> &mut Self - where - K: AsRef, - V: AsRef, - { - self.env_vars - .push((key.as_ref().into(), val.as_ref().into())); - self - } - - pub fn envs(&mut self, iter: I) -> &mut Self - where - I: IntoIterator, - K: AsRef, - V: AsRef, - { - for (k, v) in iter { - self.env(k, v); - } - self - } - - #[cfg(any(target_os = "linux", target_os = "android"))] - pub fn limit( - &mut self, - resource: rlimit::Resource, - soft_limit: u64, - hard_limit: u64, - ) -> &mut Self { - self.limits.push((resource, soft_limit, hard_limit)); - self - } - - /// Set the timeout for [`UCommand::run`] and similar methods in [`UCommand`]. - /// - /// After the timeout elapsed these `run` methods (besides [`UCommand::run_no_wait`]) will - /// panic. When [`UCommand::run_no_wait`] is used, this timeout is applied to - /// [`UChild::wait_with_output`] including all other waiting methods in [`UChild`] implicitly - /// using `wait_with_output()` and additionally [`UChild::kill`]. The default timeout of `kill` - /// will be overwritten by this `timeout`. - pub fn timeout(&mut self, timeout: Duration) -> &mut Self { - self.timeout = Some(timeout); - self - } - - /// Set if process should be run in a simulated terminal - /// - /// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`]. - /// (unix: pty, windows: ConPTY[not yet supported]) - #[cfg(unix)] - pub fn terminal_simulation(&mut self, enable: bool) -> &mut Self { - self.terminal_simulation = enable; - self - } - - /// Set if process should be run in a simulated terminal with specific size - /// - /// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`]. - /// And the size of the terminal matters additionally. - #[cfg(unix)] - pub fn terminal_size(&mut self, win_size: libc::winsize) -> &mut Self { - self.terminal_simulation(true); - self.terminal_size = Some(win_size); - self - } - - #[cfg(unix)] - fn read_from_pty(pty_fd: std::os::fd::OwnedFd, out: File) { - let read_file = std::fs::File::from(pty_fd); - let mut reader = std::io::BufReader::new(read_file); - let mut writer = std::io::BufWriter::new(out); - let result = std::io::copy(&mut reader, &mut writer); - match result { - Ok(_) => {} - // Input/output error (os error 5) is returned due to pipe closes. Buffer gets content anyway. - Err(e) if e.raw_os_error().unwrap_or_default() == 5 => {} - Err(e) => { - eprintln!("Unexpected error: {:?}", e); - panic!("error forwarding output of pty"); - } - } - } - - #[cfg(unix)] - fn spawn_reader_thread( - &self, - captured_output: Option, - pty_fd_master: OwnedFd, - name: String, - ) -> Option { - if let Some(mut captured_output_i) = captured_output { - let fd = captured_output_i.try_clone().unwrap(); - - let handle = std::thread::Builder::new() - .name(name) - .spawn(move || { - Self::read_from_pty(pty_fd_master, fd); - }) - .unwrap(); - - captured_output_i.reader_thread_handle = Some(handle); - Some(captured_output_i) - } else { - None - } - } - - /// Build the `std::process::Command` and apply the defaults on fields which were not specified - /// by the user. - /// - /// These __defaults__ are: - /// * `bin_path`: Depending on the platform and os, the native shell (unix -> `/bin/sh` etc.). - /// This default also requires to set the first argument to `-c` on unix (`/C` on windows) if - /// this argument wasn't specified explicitly by the user. - /// * `util_name`: `None`. If neither `bin_path` nor `util_name` were given the arguments are - /// run in a shell (See `bin_path` above). - /// * `temp_dir`: If `current_dir` was not set, a new temporary directory will be created in - /// which this command will be run and `current_dir` will be set to this `temp_dir`. - /// * `current_dir`: The temporary directory given by `temp_dir`. - /// * `timeout`: `30 seconds` - /// * `stdin`: `Stdio::null()` - /// * `ignore_stdin_write_error`: `false` - /// * `stdout`, `stderr`: If not specified the output will be captured with [`CapturedOutput`] - /// * `stderr_to_stdout`: `false` - /// * `bytes_into_stdin`: `None` - /// * `limits`: `None`. - fn build( - &mut self, - ) -> ( - Command, - Option, - Option, - Option, - ) { - if self.bin_path.is_some() { - if let Some(util_name) = &self.util_name { - self.args.push_front(util_name.into()); - } - } else if let Some(util_name) = &self.util_name { - self.bin_path = Some(PathBuf::from(TESTS_BINARY)); - self.args.push_front(util_name.into()); - // neither `bin_path` nor `util_name` was set so we apply the default to run the arguments - // in a platform specific shell - } else if cfg!(unix) { - #[cfg(target_os = "android")] - let bin_path = PathBuf::from("/system/bin/sh"); - #[cfg(not(target_os = "android"))] - let bin_path = PathBuf::from("/bin/sh"); - - self.bin_path = Some(bin_path); - let c_arg = OsString::from("-c"); - if !self.args.contains(&c_arg) { - self.args.push_front(c_arg); - } - } else { - self.bin_path = Some(PathBuf::from("cmd")); - let c_arg = OsString::from("/C"); - let k_arg = OsString::from("/K"); - if !self - .args - .iter() - .any(|s| s.eq_ignore_ascii_case(&c_arg) || s.eq_ignore_ascii_case(&k_arg)) - { - self.args.push_front(c_arg); - } - }; - - // unwrap is safe here because we have set `self.bin_path` before - let mut command = Command::new(self.bin_path.as_ref().unwrap()); - command.args(&self.args); - - // We use a temporary directory as working directory if not specified otherwise with - // `current_dir()`. If neither `current_dir` nor a temporary directory is available, then we - // create our own. - if let Some(current_dir) = &self.current_dir { - command.current_dir(current_dir); - } else if let Some(temp_dir) = &self.tmpd { - command.current_dir(temp_dir.path()); - } else { - let temp_dir = tempfile::tempdir().unwrap(); - self.current_dir = Some(temp_dir.path().into()); - command.current_dir(temp_dir.path()); - self.tmpd = Some(Rc::new(temp_dir)); - } - - command.env_clear(); - if cfg!(windows) { - // spell-checker:ignore (dll) rsaenh - // %SYSTEMROOT% is required on Windows to initialize crypto provider - // ... and crypto provider is required for std::rand - // From `procmon`: RegQueryValue HKLM\SOFTWARE\Microsoft\Cryptography\Defaults\Provider\Microsoft Strong Cryptographic Provider\Image Path - // SUCCESS Type: REG_SZ, Length: 66, Data: %SystemRoot%\system32\rsaenh.dll" - if let Some(systemroot) = env::var_os("SYSTEMROOT") { - command.env("SYSTEMROOT", systemroot); - } - } else { - // if someone is setting LD_PRELOAD, there's probably a good reason for it - if let Some(ld_preload) = env::var_os("LD_PRELOAD") { - command.env("LD_PRELOAD", ld_preload); - } - } - - command - .envs(DEFAULT_ENV) - .envs(self.env_vars.iter().cloned()); - - if self.timeout.is_none() { - self.timeout = Some(Duration::from_secs(30)); - } - - let mut captured_stdout = None; - let mut captured_stderr = None; - #[cfg(unix)] - let mut stdin_pty: Option = None; - #[cfg(not(unix))] - let stdin_pty: Option = None; - if self.stderr_to_stdout { - let mut output = CapturedOutput::default(); - - command - .stdin(self.stdin.take().unwrap_or_else(Stdio::null)) - .stdout(Stdio::from(output.try_clone().unwrap())) - .stderr(Stdio::from(output.try_clone().unwrap())); - captured_stdout = Some(output); - } else { - let stdout = if self.stdout.is_some() { - self.stdout.take().unwrap() - } else { - let mut stdout = CapturedOutput::default(); - let stdio = Stdio::from(stdout.try_clone().unwrap()); - captured_stdout = Some(stdout); - stdio - }; - - let stderr = if self.stderr.is_some() { - self.stderr.take().unwrap() - } else { - let mut stderr = CapturedOutput::default(); - let stdio = Stdio::from(stderr.try_clone().unwrap()); - captured_stderr = Some(stderr); - stdio - }; - - command - .stdin(self.stdin.take().unwrap_or_else(Stdio::null)) - .stdout(stdout) - .stderr(stderr); - }; - - #[cfg(unix)] - if self.terminal_simulation { - let terminal_size = self.terminal_size.unwrap_or(libc::winsize { - ws_col: 80, - ws_row: 30, - ws_xpixel: 80 * 8, - ws_ypixel: 30 * 10, - }); - - let OpenptyResult { - slave: pi_slave, - master: pi_master, - } = nix::pty::openpty(&terminal_size, None).unwrap(); - let OpenptyResult { - slave: po_slave, - master: po_master, - } = nix::pty::openpty(&terminal_size, None).unwrap(); - let OpenptyResult { - slave: pe_slave, - master: pe_master, - } = nix::pty::openpty(&terminal_size, None).unwrap(); - - stdin_pty = Some(File::from(pi_master)); - - captured_stdout = - self.spawn_reader_thread(captured_stdout, po_master, "stdout_reader".to_string()); - captured_stderr = - self.spawn_reader_thread(captured_stderr, pe_master, "stderr_reader".to_string()); - - command.stdin(pi_slave).stdout(po_slave).stderr(pe_slave); - } - - (command, captured_stdout, captured_stderr, stdin_pty) - } - - /// Spawns the command, feeds the stdin if any, and returns the - /// child process immediately. - pub fn run_no_wait(&mut self) -> UChild { - assert!(!self.has_run, "{}", ALREADY_RUN); - self.has_run = true; - - let (mut command, captured_stdout, captured_stderr, stdin_pty) = self.build(); - log_info("run", self.to_string()); - - let child = command.spawn().unwrap(); - - #[cfg(any(target_os = "linux", target_os = "android"))] - for &(resource, soft_limit, hard_limit) in &self.limits { - prlimit( - child.id() as i32, - resource, - Some((soft_limit, hard_limit)), - None, - ) - .unwrap(); - } - - let mut child = UChild::from(self, child, captured_stdout, captured_stderr, stdin_pty); - - if let Some(input) = self.bytes_into_stdin.take() { - child.pipe_in(input); - } - - child - } - - /// Spawns the command, feeds the stdin if any, waits for the result - /// and returns a command result. - /// It is recommended that you instead use succeeds() or fails() - pub fn run(&mut self) -> CmdResult { - self.run_no_wait().wait().unwrap() - } - - /// Spawns the command, feeding the passed in stdin, waits for the result - /// and returns a command result. - /// It is recommended that, instead of this, you use a combination of `pipe_in()` - /// with succeeds() or fails() - pub fn run_piped_stdin>>(&mut self, input: T) -> CmdResult { - self.pipe_in(input).run() - } - - /// Spawns the command, feeds the stdin if any, waits for the result, - /// asserts success, and returns a command result. - #[track_caller] - pub fn succeeds(&mut self) -> CmdResult { - let cmd_result = self.run(); - cmd_result.success(); - cmd_result - } - - /// Spawns the command, feeds the stdin if any, waits for the result, - /// asserts failure, and returns a command result. - #[track_caller] - pub fn fails(&mut self) -> CmdResult { - let cmd_result = self.run(); - cmd_result.failure(); - cmd_result - } - - pub fn get_full_fixture_path(&self, file_rel_path: &str) -> String { - let tmpdir_path = self.tmpd.as_ref().unwrap().path(); - format!("{}/{file_rel_path}", tmpdir_path.to_str().unwrap()) - } -} - -impl std::fmt::Display for UCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut comm_string: Vec = vec![self - .bin_path - .as_ref() - .map_or(String::new(), |p| p.display().to_string())]; - comm_string.extend(self.args.iter().map(|s| s.to_string_lossy().to_string())); - f.write_str(&comm_string.join(" ")) - } -} - -/// Stored the captured output in a temporary file. The file is deleted as soon as -/// [`CapturedOutput`] is dropped. -#[derive(Debug)] -struct CapturedOutput { - current_file: File, - output: tempfile::NamedTempFile, // drop last - reader_thread_handle: Option>, -} - -impl CapturedOutput { - /// Creates a new instance of `CapturedOutput` - fn new(output: tempfile::NamedTempFile) -> Self { - Self { - current_file: output.reopen().unwrap(), - output, - reader_thread_handle: None, - } - } - - /// Try to clone the file pointer. - fn try_clone(&mut self) -> io::Result { - self.output.as_file().try_clone() - } - - /// Return the captured output as [`String`]. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - fn output(&mut self) -> String { - String::from_utf8(self.output_bytes()).unwrap() - } - - /// Return the exact amount of bytes as `String`. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - /// - /// # Important - /// - /// This method blocks indefinitely if the amount of bytes given by `size` cannot be read - fn output_exact(&mut self, size: usize) -> String { - String::from_utf8(self.output_exact_bytes(size)).unwrap() - } - - /// Return the captured output as bytes. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - fn output_bytes(&mut self) -> Vec { - let mut buffer = Vec::::new(); - self.current_file.read_to_end(&mut buffer).unwrap(); - buffer - } - - /// Return all captured output, so far. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - fn output_all_bytes(&mut self) -> Vec { - let mut buffer = Vec::::new(); - let mut file = self.output.reopen().unwrap(); - - file.read_to_end(&mut buffer).unwrap(); - self.current_file = file; - - buffer - } - - /// Return the exact amount of bytes. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - /// - /// # Important - /// - /// This method blocks indefinitely if the amount of bytes given by `size` cannot be read - fn output_exact_bytes(&mut self, size: usize) -> Vec { - let mut buffer = vec![0; size]; - self.current_file.read_exact(&mut buffer).unwrap(); - buffer - } -} - -impl Default for CapturedOutput { - fn default() -> Self { - let mut retries = 10; - let file = loop { - let file = Builder::new().rand_bytes(10).suffix(".out").tempfile(); - if file.is_ok() || retries <= 0 { - break file.unwrap(); - } - sleep(Duration::from_millis(100)); - retries -= 1; - }; - Self { - current_file: file.reopen().unwrap(), - output: file, - reader_thread_handle: None, - } - } -} - -impl Drop for CapturedOutput { - fn drop(&mut self) { - let _ = remove_file(self.output.path()); - } -} - -#[derive(Debug, Copy, Clone)] -pub enum AssertionMode { - All, - Current, - Exact(usize, usize), -} -pub struct UChildAssertion<'a> { - uchild: &'a mut UChild, -} - -impl<'a> UChildAssertion<'a> { - pub fn new(uchild: &'a mut UChild) -> Self { - Self { uchild } - } - - fn with_output(&mut self, mode: AssertionMode) -> CmdResult { - let exit_status = if self.uchild.is_alive() { - None - } else { - Some(self.uchild.raw.wait().unwrap()) - }; - let (stdout, stderr) = match mode { - AssertionMode::All => ( - self.uchild.stdout_all_bytes(), - self.uchild.stderr_all_bytes(), - ), - AssertionMode::Current => (self.uchild.stdout_bytes(), self.uchild.stderr_bytes()), - AssertionMode::Exact(expected_stdout_size, expected_stderr_size) => ( - self.uchild.stdout_exact_bytes(expected_stdout_size), - self.uchild.stderr_exact_bytes(expected_stderr_size), - ), - }; - CmdResult::new( - self.uchild.bin_path.clone(), - self.uchild.util_name.clone(), - self.uchild.tmpd.clone(), - exit_status, - stdout, - stderr, - ) - } - - // Make assertions of [`CmdResult`] with all output from start of the process until now. - // - // This method runs [`UChild::stdout_all_bytes`] and [`UChild::stderr_all_bytes`] under the - // hood. See there for side effects - pub fn with_all_output(&mut self) -> CmdResult { - self.with_output(AssertionMode::All) - } - - // Make assertions of [`CmdResult`] with the current output. - // - // This method runs [`UChild::stdout_bytes`] and [`UChild::stderr_bytes`] under the hood. See - // there for side effects - pub fn with_current_output(&mut self) -> CmdResult { - self.with_output(AssertionMode::Current) - } - - // Make assertions of [`CmdResult`] with the exact output. - // - // This method runs [`UChild::stdout_exact_bytes`] and [`UChild::stderr_exact_bytes`] under the - // hood. See there for side effects - pub fn with_exact_output( - &mut self, - expected_stdout_size: usize, - expected_stderr_size: usize, - ) -> CmdResult { - self.with_output(AssertionMode::Exact( - expected_stdout_size, - expected_stderr_size, - )) - } - - // Assert that the child process is alive - #[track_caller] - pub fn is_alive(&mut self) -> &mut Self { - match self - .uchild - .raw - .try_wait() - { - Ok(Some(status)) => panic!( - "Assertion failed. Expected '{}' to be running but exited with status={}.\nstdout: {}\nstderr: {}", - uucore::util_name(), - status, - self.uchild.stdout_all(), - self.uchild.stderr_all() - ), - Ok(None) => {} - Err(error) => panic!("Assertion failed with error '{error:?}'"), - } - - self - } - - // Assert that the child process has exited - #[track_caller] - pub fn is_not_alive(&mut self) -> &mut Self { - match self - .uchild - .raw - .try_wait() - { - Ok(None) => panic!( - "Assertion failed. Expected '{}' to be not running but was alive.\nstdout: {}\nstderr: {}", - uucore::util_name(), - self.uchild.stdout_all(), - self.uchild.stderr_all()), - Ok(_) => {}, - Err(error) => panic!("Assertion failed with error '{error:?}'"), - } - - self - } -} - -/// Abstraction for a [`std::process::Child`] to handle the child process. -pub struct UChild { - raw: Child, - bin_path: PathBuf, - util_name: Option, - captured_stdout: Option, - captured_stderr: Option, - stdin_pty: Option, - ignore_stdin_write_error: bool, - stderr_to_stdout: bool, - join_handle: Option>>, - timeout: Option, - tmpd: Option>, // drop last -} - -impl UChild { - fn from( - ucommand: &UCommand, - child: Child, - captured_stdout: Option, - captured_stderr: Option, - stdin_pty: Option, - ) -> Self { - Self { - raw: child, - bin_path: ucommand.bin_path.clone().unwrap(), - util_name: ucommand.util_name.clone(), - captured_stdout, - captured_stderr, - stdin_pty, - ignore_stdin_write_error: ucommand.ignore_stdin_write_error, - stderr_to_stdout: ucommand.stderr_to_stdout, - join_handle: None, - timeout: ucommand.timeout, - tmpd: ucommand.tmpd.clone(), - } - } - - /// Convenience method for `sleep(Duration::from_millis(millis))` - pub fn delay(&mut self, millis: u64) -> &mut Self { - sleep(Duration::from_millis(millis)); - self - } - - /// Return the pid of the child process, similar to [`Child::id`]. - pub fn id(&self) -> u32 { - self.raw.id() - } - - /// Return true if the child process is still alive and false otherwise. - pub fn is_alive(&mut self) -> bool { - self.raw.try_wait().unwrap().is_none() - } - - /// Return true if the child process is exited and false otherwise. - #[allow(clippy::wrong_self_convention)] - pub fn is_not_alive(&mut self) -> bool { - !self.is_alive() - } - - /// Return a [`UChildAssertion`] - pub fn make_assertion(&mut self) -> UChildAssertion { - UChildAssertion::new(self) - } - - /// Convenience function for calling [`UChild::delay`] and then [`UChild::make_assertion`] - pub fn make_assertion_with_delay(&mut self, millis: u64) -> UChildAssertion { - self.delay(millis).make_assertion() - } - - /// Try to kill the child process and wait for it's termination. - /// - /// This method blocks until the child process is killed, but returns an error if `self.timeout` - /// or the default of 60s was reached. If no such error happened, the process resources are - /// released, so there is usually no need to call `wait` or alike on unix systems although it's - /// still possible to do so. - /// - /// # Platform specific behavior - /// - /// On unix systems the child process resources will be released like a call to [`Child::wait`] - /// or alike would do. - /// - /// # Error - /// - /// If [`Child::kill`] returned an error or if the child process could not be terminated within - /// `self.timeout` or the default of 60s. - pub fn try_kill(&mut self) -> io::Result<()> { - let start = Instant::now(); - self.raw.kill()?; - - let timeout = self.timeout.unwrap_or(Duration::from_secs(60)); - // As a side effect, we're cleaning up the killed child process with the implicit call to - // `Child::try_wait` in `self.is_alive`, which reaps the process id on unix systems. We - // always fail with error on timeout if `self.timeout` is set to zero. - while self.is_alive() || timeout == Duration::ZERO { - if start.elapsed() < timeout { - self.delay(10); - } else { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("kill: Timeout of '{}s' reached", timeout.as_secs_f64()), - )); - } - hint::spin_loop(); - } - - Ok(()) - } - - /// Terminate the child process unconditionally and wait for the termination. - /// - /// Ignores any errors happening during [`Child::kill`] (i.e. child process already exited) but - /// still panics on timeout. - /// - /// # Panics - /// If the child process could not be terminated within `self.timeout` or the default of 60s. - pub fn kill(&mut self) -> &mut Self { - self.try_kill() - .or_else(|error| { - // We still throw the error on timeout in the `try_kill` function - if error.kind() == io::ErrorKind::Other { - Err(error) - } else { - Ok(()) - } - }) - .unwrap(); - self - } - - /// Wait for the child process to terminate and return a [`CmdResult`]. - /// - /// See [`UChild::wait_with_output`] for details on timeouts etc. This method can also be run if - /// the child process was killed with [`UChild::kill`]. - /// - /// # Errors - /// - /// Returns the error from the call to [`UChild::wait_with_output`] if any - pub fn wait(self) -> io::Result { - let (bin_path, util_name, tmpd) = ( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - ); - - #[allow(deprecated)] - let output = self.wait_with_output()?; - - Ok(CmdResult { - bin_path, - util_name, - tmpd, - exit_status: Some(output.status), - stdout: output.stdout, - stderr: output.stderr, - }) - } - - /// Wait for the child process to terminate and return an instance of [`Output`]. - /// - /// If `self.timeout` is reached while waiting, a [`io::ErrorKind::Other`] representing a - /// timeout error is returned. If no errors happened, we join with the thread created by - /// [`UChild::pipe_in`] if any. - /// - /// # Error - /// - /// If `self.timeout` is reached while waiting or [`Child::wait_with_output`] returned an - /// error. - #[deprecated = "Please use wait() -> io::Result instead."] - pub fn wait_with_output(mut self) -> io::Result { - // some apps do not stop execution until their stdin gets closed. - // to prevent a endless waiting here, we close the stdin. - self.join(); // ensure that all pending async input is piped in - self.close_stdin(); - - let output = if let Some(timeout) = self.timeout { - let child = self.raw; - - let (sender, receiver) = mpsc::channel(); - let handle = thread::Builder::new() - .name("wait_with_output".to_string()) - .spawn(move || sender.send(child.wait_with_output())) - .unwrap(); - - match receiver.recv_timeout(timeout) { - Ok(result) => { - // unwraps are safe here because we got a result from the sender and there was no panic - // causing a disconnect. - handle.join().unwrap().unwrap(); - result - } - Err(RecvTimeoutError::Timeout) => Err(io::Error::new( - io::ErrorKind::Other, - format!("wait: Timeout of '{}s' reached", timeout.as_secs_f64()), - )), - Err(RecvTimeoutError::Disconnected) => { - handle.join().expect("Panic caused disconnect").unwrap(); - panic!("Error receiving from waiting thread because of unexpected disconnect"); - } - } - } else { - self.raw.wait_with_output() - }; - - let mut output = output?; - - if let Some(join_handle) = self.join_handle.take() { - join_handle - .join() - .expect("Error joining with the piping stdin thread") - .unwrap(); - }; - - if let Some(stdout) = self.captured_stdout.as_mut() { - if let Some(handle) = stdout.reader_thread_handle.take() { - handle.join().unwrap(); - } - output.stdout = stdout.output_bytes(); - } - if let Some(stderr) = self.captured_stderr.as_mut() { - if let Some(handle) = stderr.reader_thread_handle.take() { - handle.join().unwrap(); - } - output.stderr = stderr.output_bytes(); - } - - Ok(output) - } - - /// Read, consume and return the output as [`String`] from [`Child`]'s stdout. - /// - /// See also [`UChild::stdout_bytes`] for side effects. - pub fn stdout(&mut self) -> String { - String::from_utf8(self.stdout_bytes()).unwrap() - } - - /// Read and return all child's output in stdout as String. - /// - /// Note, that a subsequent call of any of these functions - /// - /// * [`UChild::stdout`] - /// * [`UChild::stdout_bytes`] - /// * [`UChild::stdout_exact_bytes`] - /// - /// will operate on the subsequent output of the child process. - pub fn stdout_all(&mut self) -> String { - String::from_utf8(self.stdout_all_bytes()).unwrap() - } - - /// Read, consume and return the output as bytes from [`Child`]'s stdout. - /// - /// Each subsequent call to any of the functions below will operate on the subsequent output of - /// the child process: - /// - /// * [`UChild::stdout`] - /// * [`UChild::stdout_exact_bytes`] - /// * and the call to itself [`UChild::stdout_bytes`] - pub fn stdout_bytes(&mut self) -> Vec { - match self.captured_stdout.as_mut() { - Some(output) => output.output_bytes(), - None if self.raw.stdout.is_some() => { - let mut buffer: Vec = vec![]; - let stdout = self.raw.stdout.as_mut().unwrap(); - stdout.read_to_end(&mut buffer).unwrap(); - buffer - } - None => vec![], - } - } - - /// Read and return all output from start of the child process until now. - /// - /// Each subsequent call of any of the methods below will operate on the subsequent output of - /// the child process. This method will panic if the output wasn't captured (for example if - /// [`UCommand::set_stdout`] was used). - /// - /// * [`UChild::stdout`] - /// * [`UChild::stdout_bytes`] - /// * [`UChild::stdout_exact_bytes`] - pub fn stdout_all_bytes(&mut self) -> Vec { - match self.captured_stdout.as_mut() { - Some(output) => output.output_all_bytes(), - None => { - panic!("Usage error: This method cannot be used if the output wasn't captured.") - } - } - } - - /// Read, consume and return the exact amount of bytes from `stdout`. - /// - /// This method may block indefinitely if the `size` amount of bytes exceeds the amount of bytes - /// that can be read. See also [`UChild::stdout_bytes`] for side effects. - pub fn stdout_exact_bytes(&mut self, size: usize) -> Vec { - match self.captured_stdout.as_mut() { - Some(output) => output.output_exact_bytes(size), - None if self.raw.stdout.is_some() => { - let mut buffer = vec![0; size]; - let stdout = self.raw.stdout.as_mut().unwrap(); - stdout.read_exact(&mut buffer).unwrap(); - buffer - } - None => vec![], - } - } - - /// Read, consume and return the child's stderr as String. - /// - /// See also [`UChild::stdout_bytes`] for side effects. If stderr is redirected to stdout with - /// [`UCommand::stderr_to_stdout`] then always an empty string will be returned. - pub fn stderr(&mut self) -> String { - String::from_utf8(self.stderr_bytes()).unwrap() - } - - /// Read and return all child's output in stderr as String. - /// - /// Note, that a subsequent call of any of these functions - /// - /// * [`UChild::stderr`] - /// * [`UChild::stderr_bytes`] - /// * [`UChild::stderr_exact_bytes`] - /// - /// will operate on the subsequent output of the child process. If stderr is redirected to - /// stdout with [`UCommand::stderr_to_stdout`] then always an empty string will be returned. - pub fn stderr_all(&mut self) -> String { - String::from_utf8(self.stderr_all_bytes()).unwrap() - } - - /// Read, consume and return the currently available bytes from child's stderr. - /// - /// If stderr is redirected to stdout with [`UCommand::stderr_to_stdout`] then always zero bytes - /// are returned. See also [`UChild::stdout_bytes`] for side effects. - pub fn stderr_bytes(&mut self) -> Vec { - match self.captured_stderr.as_mut() { - Some(output) => output.output_bytes(), - None if self.raw.stderr.is_some() => { - let mut buffer: Vec = vec![]; - let stderr = self.raw.stderr.as_mut().unwrap(); - stderr.read_to_end(&mut buffer).unwrap(); - buffer - } - None => vec![], - } - } - - /// Read and return all output from start of the child process until now. - /// - /// Each subsequent call of any of the methods below will operate on the subsequent output of - /// the child process. This method will panic if the output wasn't captured (for example if - /// [`UCommand::set_stderr`] was used). If [`UCommand::stderr_to_stdout`] was used always zero - /// bytes are returned. - /// - /// * [`UChild::stderr`] - /// * [`UChild::stderr_bytes`] - /// * [`UChild::stderr_exact_bytes`] - pub fn stderr_all_bytes(&mut self) -> Vec { - match self.captured_stderr.as_mut() { - Some(output) => output.output_all_bytes(), - None if self.stderr_to_stdout => vec![], - None => { - panic!("Usage error: This method cannot be used if the output wasn't captured.") - } - } - } - - /// Read, consume and return the exact amount of bytes from stderr. - /// - /// If stderr is redirect to stdout with [`UCommand::stderr_to_stdout`] then always zero bytes - /// are returned. - /// - /// # Important - /// This method blocks indefinitely if the `size` amount of bytes cannot be read. - pub fn stderr_exact_bytes(&mut self, size: usize) -> Vec { - match self.captured_stderr.as_mut() { - Some(output) => output.output_exact_bytes(size), - None if self.raw.stderr.is_some() => { - let stderr = self.raw.stderr.as_mut().unwrap(); - let mut buffer = vec![0; size]; - stderr.read_exact(&mut buffer).unwrap(); - buffer - } - None => vec![], - } - } - - fn access_stdin_as_writer<'a>(&'a mut self) -> Box { - if let Some(stdin_fd) = &self.stdin_pty { - Box::new(BufWriter::new(stdin_fd.try_clone().unwrap())) - } else { - let stdin: &mut std::process::ChildStdin = self.raw.stdin.as_mut().unwrap(); - Box::new(BufWriter::new(stdin)) - } - } - - fn take_stdin_as_writer(&mut self) -> Box { - if let Some(stdin_fd) = mem::take(&mut self.stdin_pty) { - Box::new(BufWriter::new(stdin_fd)) - } else { - let stdin = self - .raw - .stdin - .take() - .expect("Could not pipe into child process. Was it set to Stdio::null()?"); - - Box::new(BufWriter::new(stdin)) - } - } - - /// Pipe data into [`Child`] stdin in a separate thread to avoid deadlocks. - /// - /// In contrast to [`UChild::write_in`], this method is designed to simulate a pipe on the - /// command line and can be used only once or else panics. Note, that [`UCommand::set_stdin`] - /// must be used together with [`Stdio::piped`] or else this method doesn't work as expected. - /// `Stdio::piped` is the current default when using [`UCommand::run_no_wait`]) without calling - /// `set_stdin`. This method stores a [`JoinHandle`] of the thread in which the writing to the - /// child processes' stdin is running. The associated thread is joined with the main process in - /// the methods below when exiting the child process. - /// - /// * [`UChild::wait`] - /// * [`UChild::wait_with_output`] - /// * [`UChild::pipe_in_and_wait`] - /// * [`UChild::pipe_in_and_wait_with_output`] - /// - /// Usually, there's no need to join manually but if needed, the [`UChild::join`] method can be - /// used . - /// - /// [`JoinHandle`]: std::thread::JoinHandle - pub fn pipe_in>>(&mut self, content: T) -> &mut Self { - let ignore_stdin_write_error = self.ignore_stdin_write_error; - let mut content: Vec = content.into(); - if self.stdin_pty.is_some() { - content.append(&mut END_OF_TRANSMISSION_SEQUENCE.to_vec()); - } - let mut writer = self.take_stdin_as_writer(); - - let join_handle = std::thread::Builder::new() - .name("pipe_in".to_string()) - .spawn( - move || match writer.write_all(&content).and_then(|()| writer.flush()) { - Err(error) if !ignore_stdin_write_error => Err(io::Error::new( - io::ErrorKind::Other, - format!("failed to write to stdin of child: {error}"), - )), - Ok(()) | Err(_) => Ok(()), - }, - ) - .unwrap(); - - self.join_handle = Some(join_handle); - self - } - - /// Call join on the thread created by [`UChild::pipe_in`] and if the thread is still running. - /// - /// This method can be called multiple times but is a noop if already joined. - pub fn join(&mut self) -> &mut Self { - if let Some(join_handle) = self.join_handle.take() { - join_handle - .join() - .expect("Error joining with the piping stdin thread") - .unwrap(); - } - self - } - - /// Convenience method for [`UChild::pipe_in`] and then [`UChild::wait`] - pub fn pipe_in_and_wait>>(mut self, content: T) -> CmdResult { - self.pipe_in(content); - self.wait().unwrap() - } - - /// Convenience method for [`UChild::pipe_in`] and then [`UChild::wait_with_output`] - #[deprecated = "Please use pipe_in_and_wait() -> CmdResult instead."] - pub fn pipe_in_and_wait_with_output>>(mut self, content: T) -> Output { - self.pipe_in(content); - - #[allow(deprecated)] - self.wait_with_output().unwrap() - } - - /// Write some bytes to the child process stdin. - /// - /// This function is meant for small data and faking user input like typing a `yes` or `no`. - /// This function blocks until all data is written but can be used multiple times in contrast to - /// [`UChild::pipe_in`]. - /// - /// # Errors - /// If [`ChildStdin::write_all`] or [`ChildStdin::flush`] returned an error - pub fn try_write_in>>(&mut self, data: T) -> io::Result<()> { - let ignore_stdin_write_error = self.ignore_stdin_write_error; - let mut writer = self.access_stdin_as_writer(); - - match writer.write_all(&data.into()).and_then(|()| writer.flush()) { - Err(error) if !ignore_stdin_write_error => Err(io::Error::new( - io::ErrorKind::Other, - format!("failed to write to stdin of child: {error}"), - )), - Ok(()) | Err(_) => Ok(()), - } - } - - /// Convenience function for [`UChild::try_write_in`] and a following `unwrap`. - pub fn write_in>>(&mut self, data: T) -> &mut Self { - self.try_write_in(data).unwrap(); - self - } - - /// Close the child process stdout. - /// - /// Note this will have no effect if the output was captured with [`CapturedOutput`] which is the - /// default if [`UCommand::set_stdout`] wasn't called. - pub fn close_stdout(&mut self) -> &mut Self { - self.raw.stdout.take(); - self - } - - /// Close the child process stderr. - /// - /// Note this will have no effect if the output was captured with [`CapturedOutput`] which is the - /// default if [`UCommand::set_stderr`] wasn't called. - pub fn close_stderr(&mut self) -> &mut Self { - self.raw.stderr.take(); - self - } - - /// Close the child process stdin. - /// - /// Note, this does not have any effect if using the [`UChild::pipe_in`] method. - pub fn close_stdin(&mut self) -> &mut Self { - self.raw.stdin.take(); - if self.stdin_pty.is_some() { - // a pty can not be closed. We need to send a EOT: - let _ = self.try_write_in(END_OF_TRANSMISSION_SEQUENCE); - self.stdin_pty.take(); - } - self - } -} - -pub fn vec_of_size(n: usize) -> Vec { - let result = vec![b'a'; n]; - assert_eq!(result.len(), n); - result -} - -pub fn whoami() -> String { - // Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. - // - // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" - // whoami: cannot find name for user ID 1001 - // id --name: cannot find name for user ID 1001 - // id --name: cannot find name for group ID 116 - // - // However, when running "id" from within "/bin/bash" it looks fine: - // id: "uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),101(systemd-journal)" - // whoami: "runner" - - // Use environment variable to get current user instead of - // invoking `whoami` and fall back to user "nobody" on error. - std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .unwrap_or_else(|e| { - println!("{UUTILS_WARNING}: {e}, using \"nobody\" instead"); - "nobody".to_string() - }) -} - -/// Add prefix 'g' for `util_name` if not on linux -#[cfg(unix)] -pub fn host_name_for(util_name: &str) -> Cow { - // In some environments, e.g. macOS/freebsd, the GNU util-linux are prefixed with "g" - // to not interfere with the BSD counterparts already in `$PATH`. - #[cfg(not(target_os = "linux"))] - { - // make call to `host_name_for` idempotent - if util_name.starts_with('g') && util_name != "groups" { - util_name.into() - } else { - format!("g{util_name}").into() - } - } - #[cfg(target_os = "linux")] - util_name.into() -} - -// GNU util-linux version 8.32 is the reference version since it is the latest version and the -// GNU test suite in "util-linux/.github/workflows/GnuTests.yml" runs against it. -// However, here 8.30 was chosen because right now there's no ubuntu image for the github actions -// CICD available with a higher version than 8.30. -// GNU util-linux versions from the CICD images for comparison: -// ubuntu-2004: 8.30 (latest) -// ubuntu-1804: 8.28 -// macos-latest: 8.32 -const VERSION_MIN: &str = "8.30"; // minimum Version for the reference `coreutil` in `$PATH` - -const UUTILS_WARNING: &str = "uutils-tests-warning"; -const UUTILS_INFO: &str = "uutils-tests-info"; - -/// Run `util_name --version` and return Ok if the version is >= `version_expected`. -/// Returns an error if -/// * `util_name` cannot run -/// * the version cannot be parsed -/// * the version is too low -/// -/// This is used by `expected_result` to check if the util-linux version is >= `VERSION_MIN`. -/// It makes sense to use this manually in a test if a feature -/// is tested that was introduced after `VERSION_MIN` -/// -/// Example: -/// -/// ```no_run -/// use crate::common::util::*; -/// const VERSION_MIN_MULTIPLE_USERS: &str = "8.31"; -/// -/// #[test] -/// fn test_xyz() { -/// unwrap_or_return!(check_coreutil_version( -/// util_name!(), -/// VERSION_MIN_MULTIPLE_USERS -/// )); -/// // proceed with the test... -/// } -/// ``` -#[cfg(unix)] -pub fn check_coreutil_version( - util_name: &str, - version_expected: &str, -) -> std::result::Result { - // example: - // $ id --version | head -n 1 - // id (GNU util-linux) 8.32.162-4eda - - let util_name = &host_name_for(util_name); - log_info("run", format!("{util_name} --version")); - let version_check = match Command::new(util_name.as_ref()) - .env("LC_ALL", "C") - .arg("--version") - .output() - { - Ok(s) => s, - Err(e) => return Err(format!("{UUTILS_WARNING}: '{util_name}' {e}")), - }; - std::str::from_utf8(&version_check.stdout).unwrap() - .split('\n') - .collect::>() - .first() - .map_or_else( - || Err(format!("{UUTILS_WARNING}: unexpected output format for reference coreutil: '{util_name} --version'")), - |s| { - if s.contains(&format!("(GNU util-linux) {version_expected}")) { - Ok(format!("{UUTILS_INFO}: {s}")) - } else if s.contains("(GNU util-linux)") { - let version_found = parse_coreutil_version(s); - let version_expected = version_expected.parse::().unwrap_or_default(); - if version_found > version_expected { - Ok(format!("{UUTILS_INFO}: version for the reference coreutil '{util_name}' is higher than expected; expected: {version_expected}, found: {version_found}")) - } else { - Err(format!("{UUTILS_WARNING}: version for the reference coreutil '{util_name}' does not match; expected: {version_expected}, found: {version_found}")) } - } else { - Err(format!("{UUTILS_WARNING}: no util-linux version string found for reference util-linux '{util_name} --version'")) - } - }, - ) -} - -// simple heuristic to parse the util-linux SemVer string, e.g. "id (GNU util-linux) 8.32.263-0475" -fn parse_coreutil_version(version_string: &str) -> f32 { - version_string - .split_whitespace() - .last() - .unwrap() - .split('.') - .take(2) - .collect::>() - .join(".") - .parse::() - .unwrap_or_default() -} - -/// This runs the GNU util-linux `util_name` binary in `$PATH` in order to -/// dynamically gather reference values on the system. -/// If the `util_name` in `$PATH` doesn't include a util-linux version string, -/// or the version is too low, this returns an error and the test should be skipped. -/// -/// Example: -/// -/// ```no_run -/// use crate::common::util::*; -/// #[test] -/// fn test_xyz() { -/// let ts = TestScenario::new(util_name!()); -/// let result = ts.ucmd().run(); -/// let exp_result = unwrap_or_return!(expected_result(&ts, &[])); -/// result -/// .stdout_is(exp_result.stdout_str()) -/// .stderr_is(exp_result.stderr_str()) -/// .code_is(exp_result.code()); -/// } -///``` -#[cfg(unix)] -pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result { - let util_name = ts.util_name.as_str(); - println!("{}", check_coreutil_version(util_name, VERSION_MIN)?); - let util_name = host_name_for(util_name); - - let result = ts - .cmd(util_name.as_ref()) - .env("PATH", PATH) - .envs(DEFAULT_ENV) - .args(args) - .run(); - - let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") { - ( - result.stdout_str().to_string(), - result.stderr_str().to_string(), - ) - } else { - // `host_name_for` added prefix, strip 'g' prefix from results: - let from = util_name.to_string() + ":"; - let to = &from[1..]; - ( - result.stdout_str().replace(&from, to), - result.stderr_str().replace(&from, to), - ) - }; - - Ok(CmdResult::new( - ts.bin_path.as_os_str().to_str().unwrap().to_string(), - Some(ts.util_name.clone()), - Some(result.tmpd()), - result.exit_status, - stdout.as_bytes(), - stderr.as_bytes(), - )) -} - -/// This is a convenience wrapper to run a ucmd with root permissions. -/// It can be used to test programs when being root is needed -/// This runs `sudo -E --non-interactive target/debug/util-linux util_name args` -/// This is primarily designed to run in an environment where whoami is in $path -/// and where non-interactive sudo is possible. -/// To check if i) non-interactive sudo is possible and ii) if sudo works, this runs: -/// `sudo -E --non-interactive whoami` first. -/// -/// This return an `Err()` if run inside CICD because there's no 'sudo'. -/// -/// Example: -/// -/// ```no_run -/// use crate::common::util::*; -/// #[test] -/// fn test_xyz() { -/// let ts = TestScenario::new("whoami"); -/// let expected = "root\n".to_string(); -/// if let Ok(result) = run_ucmd_as_root(&ts, &[]) { -/// result.stdout_is(expected); -/// } else { -/// println!("TEST SKIPPED"); -/// } -/// } -///``` -#[cfg(unix)] -pub fn run_ucmd_as_root( - ts: &TestScenario, - args: &[&str], -) -> std::result::Result { - run_ucmd_as_root_with_stdin_stdout(ts, args, None, None) -} - -#[cfg(unix)] -pub fn run_ucmd_as_root_with_stdin_stdout( - ts: &TestScenario, - args: &[&str], - stdin: Option<&str>, - stdout: Option<&str>, -) -> std::result::Result { - if is_ci() { - Err(format!("{UUTILS_INFO}: {}", "cannot run inside CI")) - } else { - // check if we can run 'sudo' - log_info("run", "sudo -E --non-interactive whoami"); - match Command::new("sudo") - .envs(DEFAULT_ENV) - .args(["-E", "--non-interactive", "whoami"]) - .output() - { - Ok(output) if String::from_utf8_lossy(&output.stdout).eq("root\n") => { - // we can run sudo and we're root - // run ucmd as root: - let mut cmd = ts.cmd("sudo"); - cmd.env("PATH", PATH) - .envs(DEFAULT_ENV) - .arg("-E") - .arg("--non-interactive") - .arg(&ts.bin_path) - .arg(&ts.util_name) - .args(args); - if let Some(stdin) = stdin { - cmd.set_stdin(File::open(stdin).unwrap()); - } - if let Some(stdout) = stdout { - cmd.set_stdout(File::open(stdout).unwrap()); - } - Ok(cmd.run()) - } - Ok(output) - if String::from_utf8_lossy(&output.stderr).eq("sudo: a password is required\n") => - { - Err("Cannot run non-interactive sudo".to_string()) - } - Ok(_output) => Err("\"sudo whoami\" didn't return \"root\"".to_string()), - Err(e) => Err(format!("{UUTILS_WARNING}: {e}")), - } - } -} - -/// Sanity checks for test utils -#[cfg(test)] -mod tests { - // spell-checker:ignore (tests) asdfsadfa - use super::*; - - pub fn run_cmd>(cmd: T) -> CmdResult { - UCommand::new().arg(cmd).run() - } - - #[test] - fn test_command_result_when_no_output_with_exit_32() { - let result = run_cmd("exit 32"); - - if cfg!(windows) { - std::assert!(result.bin_path.ends_with("cmd")); - } else { - std::assert!(result.bin_path.ends_with("sh")); - } - - std::assert!(result.util_name.is_none()); - std::assert!(result.tmpd.is_some()); - - assert!(result.exit_status.is_some()); - std::assert_eq!(result.code(), 32); - result.code_is(32); - assert!(!result.succeeded()); - result.failure(); - result.fails_silently(); - assert!(result.stderr.is_empty()); - assert!(result.stdout.is_empty()); - result.no_output(); - result.no_stderr(); - result.no_stdout(); - } - - #[test] - #[should_panic] - fn test_command_result_when_exit_32_then_success_panic() { - run_cmd("exit 32").success(); - } - - #[test] - fn test_command_result_when_no_output_with_exit_0() { - let result = run_cmd("exit 0"); - - assert!(result.exit_status.is_some()); - std::assert_eq!(result.code(), 0); - result.code_is(0); - assert!(result.succeeded()); - result.success(); - assert!(result.stderr.is_empty()); - assert!(result.stdout.is_empty()); - result.no_output(); - result.no_stderr(); - result.no_stdout(); - } - - #[test] - #[should_panic] - fn test_command_result_when_exit_0_then_failure_panics() { - run_cmd("exit 0").failure(); - } - - #[test] - #[should_panic] - fn test_command_result_when_exit_0_then_silent_failure_panics() { - run_cmd("exit 0").fails_silently(); - } - - #[test] - fn test_command_result_when_stdout_with_exit_0() { - #[cfg(windows)] - let (result, vector, string) = ( - run_cmd("echo hello& exit 0"), - vec![b'h', b'e', b'l', b'l', b'o', b'\r', b'\n'], - "hello\r\n", - ); - #[cfg(not(windows))] - let (result, vector, string) = ( - run_cmd("echo hello; exit 0"), - vec![b'h', b'e', b'l', b'l', b'o', b'\n'], - "hello\n", - ); - - assert!(result.exit_status.is_some()); - std::assert_eq!(result.code(), 0); - result.code_is(0); - assert!(result.succeeded()); - result.success(); - assert!(result.stderr.is_empty()); - std::assert_eq!(result.stdout, vector); - result.no_stderr(); - result.stdout_is(string); - result.stdout_is_bytes(&vector); - result.stdout_only(string); - result.stdout_only_bytes(&vector); - } - - #[test] - fn test_command_result_when_stderr_with_exit_0() { - #[cfg(windows)] - let (result, vector, string) = ( - run_cmd("echo hello>&2& exit 0"), - vec![b'h', b'e', b'l', b'l', b'o', b'\r', b'\n'], - "hello\r\n", - ); - #[cfg(not(windows))] - let (result, vector, string) = ( - run_cmd("echo hello >&2; exit 0"), - vec![b'h', b'e', b'l', b'l', b'o', b'\n'], - "hello\n", - ); - - assert!(result.exit_status.is_some()); - std::assert_eq!(result.code(), 0); - result.code_is(0); - assert!(result.succeeded()); - result.success(); - assert!(result.stdout.is_empty()); - result.no_stdout(); - std::assert_eq!(result.stderr, vector); - result.stderr_is(string); - result.stderr_is_bytes(&vector); - result.stderr_only(string); - result.stderr_only_bytes(&vector); - } - - #[test] - fn test_std_does_not_contain() { - #[cfg(windows)] - let res = run_cmd( - "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", - ); - #[cfg(not(windows))] - let res = run_cmd( - "echo This is a likely error message; echo This is a likely error message >&2; exit 0", - ); - res.stdout_does_not_contain("unlikely"); - res.stderr_does_not_contain("unlikely"); - } - - #[test] - #[should_panic] - fn test_stdout_does_not_contain_fail() { - #[cfg(windows)] - let res = run_cmd("echo This is a likely error message& exit 0"); - #[cfg(not(windows))] - let res = run_cmd("echo This is a likely error message; exit 0"); - - res.stdout_does_not_contain("likely"); - } - - #[test] - #[should_panic] - fn test_stderr_does_not_contain_fail() { - #[cfg(windows)] - let res = run_cmd("echo This is a likely error message>&2 & exit 0"); - #[cfg(not(windows))] - let res = run_cmd("echo This is a likely error message >&2; exit 0"); - - res.stderr_does_not_contain("likely"); - } - - #[test] - fn test_stdout_matches() { - #[cfg(windows)] - let res = run_cmd( - "(echo This is a likely error message& echo This is a likely error message>&2 ) & exit 0", - ); - #[cfg(not(windows))] - let res = run_cmd( - "echo This is a likely error message; echo This is a likely error message >&2; exit 0", - ); - - let positive = regex::Regex::new(".*likely.*").unwrap(); - let negative = regex::Regex::new(".*unlikely.*").unwrap(); - res.stdout_matches(&positive); - res.stdout_does_not_match(&negative); - } - - #[test] - #[should_panic] - fn test_stdout_matches_fail() { - #[cfg(windows)] - let res = run_cmd( - "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", - ); - #[cfg(not(windows))] - let res = run_cmd( - "echo This is a likely error message; echo This is a likely error message >&2; exit 0", - ); - - let negative = regex::Regex::new(".*unlikely.*").unwrap(); - res.stdout_matches(&negative); - } - - #[test] - #[should_panic] - fn test_stdout_not_matches_fail() { - #[cfg(windows)] - let res = run_cmd( - "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", - ); - #[cfg(not(windows))] - let res = run_cmd( - "echo This is a likely error message; echo This is a likely error message >&2; exit 0", - ); - - let positive = regex::Regex::new(".*likely.*").unwrap(); - res.stdout_does_not_match(&positive); - } - - #[cfg(feature = "echo")] - #[test] - fn test_normalized_newlines_stdout_is() { - let ts = TestScenario::new("echo"); - let res = ts.ucmd().args(&["-ne", "A\r\nB\nC"]).run(); - - res.normalized_newlines_stdout_is("A\r\nB\nC"); - res.normalized_newlines_stdout_is("A\nB\nC"); - res.normalized_newlines_stdout_is("A\nB\r\nC"); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_normalized_newlines_stdout_is_fail() { - let ts = TestScenario::new("echo"); - let res = ts.ucmd().args(&["-ne", "A\r\nB\nC"]).run(); - - res.normalized_newlines_stdout_is("A\r\nB\nC\n"); - } - - #[cfg(feature = "echo")] - #[test] - fn test_cmd_result_stdout_check_and_stdout_str_check() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - - result.stdout_str_check(|stdout| stdout.ends_with("world\n")); - result.stdout_check(|stdout| stdout.get(0..2).unwrap().eq(&[b'H', b'e'])); - result.no_stderr(); - } - - #[cfg(feature = "echo")] - #[test] - fn test_cmd_result_stderr_check_and_stderr_str_check() { - let ts = TestScenario::new("echo"); - let result = run_cmd(format!( - "{} {} Hello world >&2", - ts.bin_path.display(), - ts.util_name - )); - - result.stderr_str_check(|stderr| stderr.ends_with("world\n")); - result.stderr_check(|stderr| stderr.get(0..2).unwrap().eq(&[b'H', b'e'])); - result.no_stdout(); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stdout_str_check_when_false_then_panics() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - result.stdout_str_check(str::is_empty); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stdout_check_when_false_then_panics() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - result.stdout_check(<[u8]>::is_empty); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stderr_str_check_when_false_then_panics() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - result.stderr_str_check(|s| !s.is_empty()); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stderr_check_when_false_then_panics() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - result.stderr_check(|s| !s.is_empty()); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stdout_check_when_predicate_panics_then_panic() { - let result = TestScenario::new("echo").ucmd().run(); - result.stdout_str_check(|_| panic!("Just testing")); - } - - #[cfg(feature = "echo")] - #[cfg(unix)] - #[test] - fn test_cmd_result_signal_when_normal_exit_then_no_signal() { - let result = TestScenario::new("echo").ucmd().run(); - assert!(result.signal().is_none()); - } - - #[cfg(feature = "sleep")] - #[cfg(unix)] - #[test] - #[should_panic = "Program must be run first or has not finished"] - fn test_cmd_result_signal_when_still_running_then_panic() { - let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); - - child - .make_assertion() - .is_alive() - .with_current_output() - .signal(); - } - - #[cfg(feature = "sleep")] - #[cfg(unix)] - #[test] - fn test_cmd_result_signal_when_kill_then_signal() { - let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); - - child.kill(); - child - .make_assertion() - .is_not_alive() - .with_current_output() - .signal_is(9) - .signal_name_is("SIGKILL") - .signal_name_is("KILL") - .signal_name_is("9") - .signal() - .expect("Signal was none"); - - let result = child.wait().unwrap(); - result - .signal_is(9) - .signal_name_is("SIGKILL") - .signal_name_is("KILL") - .signal_name_is("9") - .signal() - .expect("Signal was none"); - } - - #[cfg(feature = "sleep")] - #[cfg(unix)] - #[rstest] - #[case::signal_full_name_lower_case("sigkill")] - #[case::signal_short_name_lower_case("kill")] - #[case::signal_only_part_of_name("IGKILL")] // spell-checker: disable-line - #[case::signal_just_sig("SIG")] - #[case::signal_value_too_high("100")] - #[case::signal_value_negative("-1")] - #[should_panic = "Invalid signal name or value"] - fn test_cmd_result_signal_when_invalid_signal_name_then_panic(#[case] signal_name: &str) { - let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); - child.kill(); - let result = child.wait().unwrap(); - result.signal_name_is(signal_name); - } - - #[test] - #[cfg(unix)] - fn test_parse_coreutil_version() { - use std::assert_eq; - assert_eq!( - parse_coreutil_version("id (GNU util-linux) 9.0.123-0123").to_string(), - "9" - ); - assert_eq!( - parse_coreutil_version("id (GNU util-linux) 8.32.263-0475").to_string(), - "8.32" - ); - assert_eq!( - parse_coreutil_version("id (GNU util-linux) 8.25.123-0123").to_string(), - "8.25" - ); - assert_eq!( - parse_coreutil_version("id (GNU util-linux) 9.0").to_string(), - "9" - ); - assert_eq!( - parse_coreutil_version("id (GNU util-linux) 8.32").to_string(), - "8.32" - ); - assert_eq!( - parse_coreutil_version("id (GNU util-linux) 8.25").to_string(), - "8.25" - ); - } - - #[test] - #[cfg(unix)] - fn test_check_coreutil_version() { - match check_coreutil_version("id", VERSION_MIN) { - Ok(s) => assert!(s.starts_with("uutils-tests-")), - Err(s) => assert!(s.starts_with("uutils-tests-warning")), - }; - #[cfg(target_os = "linux")] - std::assert_eq!( - check_coreutil_version("no test name", VERSION_MIN), - Err("uutils-tests-warning: 'no test name' \ - No such file or directory (os error 2)" - .to_string()) - ); - } - - #[test] - #[cfg(unix)] - fn test_expected_result() { - let ts = TestScenario::new("id"); - // assert!(expected_result(&ts, &[]).is_ok()); - match expected_result(&ts, &[]) { - Ok(r) => assert!(r.succeeded()), - Err(s) => assert!(s.starts_with("uutils-tests-warning")), - } - let ts = TestScenario::new("no test name"); - assert!(expected_result(&ts, &[]).is_err()); - } - - #[test] - #[cfg(unix)] - fn test_host_name_for() { - #[cfg(target_os = "linux")] - { - std::assert_eq!(host_name_for("id"), "id"); - std::assert_eq!(host_name_for("groups"), "groups"); - std::assert_eq!(host_name_for("who"), "who"); - } - #[cfg(not(target_os = "linux"))] - { - // spell-checker:ignore (strings) ggroups gwho - std::assert_eq!(host_name_for("id"), "gid"); - std::assert_eq!(host_name_for("groups"), "ggroups"); - std::assert_eq!(host_name_for("who"), "gwho"); - std::assert_eq!(host_name_for("gid"), "gid"); - std::assert_eq!(host_name_for("ggroups"), "ggroups"); - std::assert_eq!(host_name_for("gwho"), "gwho"); - } - } - - #[test] - #[cfg(unix)] - #[cfg(feature = "whoami")] - fn test_run_ucmd_as_root() { - if is_ci() { - println!("TEST SKIPPED (cannot run inside CI)"); - } else { - // Skip test if we can't guarantee non-interactive `sudo`, or if we're not "root" - if let Ok(output) = Command::new("sudo") - .env("LC_ALL", "C") - .args(["-E", "--non-interactive", "whoami"]) - .output() - { - if output.status.success() && String::from_utf8_lossy(&output.stdout).eq("root\n") { - let ts = TestScenario::new("whoami"); - std::assert_eq!( - run_ucmd_as_root(&ts, &[]).unwrap().stdout_str().trim(), - "root" - ); - } else { - println!("TEST SKIPPED (we're not root)"); - } - } else { - println!("TEST SKIPPED (cannot run sudo)"); - } - } - } - - // This error was first detected when running tail so tail is used here but - // should fail with any command that takes piped input. - // See also https://github.com/uutils/util-linux/issues/3895 - #[cfg(feature = "tail")] - #[test] - #[cfg_attr(not(feature = "expensive_tests"), ignore)] - fn test_when_piped_input_then_no_broken_pipe() { - let ts = TestScenario::new("tail"); - for i in 0..10000 { - dbg!(i); - let test_string = "a\nb\n"; - ts.ucmd() - .args(&["-n", "0"]) - .pipe_in(test_string) - .succeeds() - .no_stdout() - .no_stderr(); - } - } - - #[cfg(feature = "echo")] - #[test] - fn test_uchild_when_run_with_a_non_blocking_util() { - let ts = TestScenario::new("echo"); - ts.ucmd() - .arg("hello world") - .run() - .success() - .stdout_only("hello world\n"); - } - - // Test basically that most of the methods of UChild are working - #[cfg(feature = "echo")] - #[test] - fn test_uchild_when_run_no_wait_with_a_non_blocking_util() { - let ts = TestScenario::new("echo"); - let mut child = ts.ucmd().arg("hello world").run_no_wait(); - - // check `child.is_alive()` and `child.delay()` is working - let mut trials = 10; - while child.is_alive() { - assert!( - trials > 0, - "Assertion failed: child process is still alive." - ); - - child.delay(500); - trials -= 1; - } - - assert!(!child.is_alive()); - - // check `child.is_not_alive()` is working - assert!(child.is_not_alive()); - - // check the current output is correct - std::assert_eq!(child.stdout(), "hello world\n"); - assert!(child.stderr().is_empty()); - - // check the current output of echo is empty. We already called `child.stdout()` and `echo` - // exited so there's no additional output after the first call of `child.stdout()` - assert!(child.stdout().is_empty()); - assert!(child.stderr().is_empty()); - - // check that we're still able to access all output of the child process, even after exit - // and call to `child.stdout()` - std::assert_eq!(child.stdout_all(), "hello world\n"); - assert!(child.stderr_all().is_empty()); - - // we should be able to call kill without panics, even if the process already exited - child.make_assertion().is_not_alive(); - child.kill(); - - // we should be able to call wait without panics and apply some assertions - child.wait().unwrap().code_is(0).no_stdout().no_stderr(); - } - - #[cfg(feature = "cat")] - #[test] - fn test_uchild_when_pipe_in() { - let ts = TestScenario::new("cat"); - let mut child = ts.ucmd().set_stdin(Stdio::piped()).run_no_wait(); - child.pipe_in("content"); - child.wait().unwrap().stdout_only("content").success(); - - ts.ucmd().pipe_in("content").run().stdout_is("content"); - } - - #[cfg(feature = "rm")] - #[test] - fn test_uchild_when_run_no_wait_with_a_blocking_command() { - let ts = TestScenario::new("rm"); - let at = &ts.fixtures; - - at.mkdir("a"); - at.touch("a/empty"); - - #[cfg(target_vendor = "apple")] - let delay: u64 = 2000; - #[cfg(not(target_vendor = "apple"))] - let delay: u64 = 1000; - - let yes = if cfg!(windows) { "y\r\n" } else { "y\n" }; - - let mut child = ts - .ucmd() - .set_stdin(Stdio::piped()) - .stderr_to_stdout() - .args(&["-riv", "a"]) - .run_no_wait(); - child - .make_assertion_with_delay(delay) - .is_alive() - .with_current_output() - .stdout_is("rm: descend into directory 'a'? "); - - #[cfg(windows)] - let expected = "rm: descend into directory 'a'? \ - rm: remove regular empty file 'a\\empty'? "; - #[cfg(unix)] - let expected = "rm: descend into directory 'a'? \ - rm: remove regular empty file 'a/empty'? "; - child.write_in(yes); - child - .make_assertion_with_delay(delay) - .is_alive() - .with_all_output() - .stdout_is(expected); - - #[cfg(windows)] - let expected = "removed 'a\\empty'\nrm: remove directory 'a'? "; - #[cfg(unix)] - let expected = "removed 'a/empty'\nrm: remove directory 'a'? "; - - child - .write_in(yes) - .make_assertion_with_delay(delay) - .is_alive() - .with_exact_output(44, 0) - .stdout_only(expected); - - let expected = "removed directory 'a'\n"; - - child.write_in(yes); - child.wait().unwrap().stdout_only(expected).success(); - } - - #[cfg(feature = "tail")] - #[test] - fn test_uchild_when_run_with_stderr_to_stdout() { - let ts = TestScenario::new("tail"); - let at = &ts.fixtures; - - at.write("data", "file data\n"); - - let expected_stdout = "==> data <==\n\ - file data\n\ - tail: cannot open 'missing' for reading: No such file or directory\n"; - ts.ucmd() - .args(&["data", "missing"]) - .stderr_to_stdout() - .fails() - .stdout_only(expected_stdout); - } - - #[cfg(feature = "cat")] - #[cfg(unix)] - #[test] - fn test_uchild_when_no_capture_reading_from_infinite_source() { - use regex::Regex; - - let ts = TestScenario::new("cat"); - - let expected_stdout = b"\0".repeat(12345); - let mut child = ts - .ucmd() - .set_stdin(Stdio::from(File::open("/dev/zero").unwrap())) - .set_stdout(Stdio::piped()) - .run_no_wait(); - - child - .make_assertion() - .with_exact_output(12345, 0) - .stdout_only_bytes(expected_stdout); - - child - .kill() - .make_assertion() - .with_current_output() - .stdout_matches(&Regex::new("[\0].*").unwrap()) - .no_stderr(); - } - - #[cfg(feature = "sleep")] - #[test] - fn test_uchild_when_wait_and_timeout_is_reached_then_timeout_error() { - let ts = TestScenario::new("sleep"); - let child = ts - .ucmd() - .timeout(Duration::from_secs(1)) - .arg("10.0") - .run_no_wait(); - - match child.wait() { - Err(error) if error.kind() == io::ErrorKind::Other => { - std::assert_eq!(error.to_string(), "wait: Timeout of '1s' reached"); - } - Err(error) => panic!("Assertion failed: Expected error with timeout but was: {error}"), - Ok(_) => panic!("Assertion failed: Expected timeout of `wait`."), - } - } - - #[cfg(feature = "sleep")] - #[rstest] - #[timeout(Duration::from_secs(5))] - fn test_uchild_when_kill_and_timeout_higher_than_kill_time_then_no_panic() { - let ts = TestScenario::new("sleep"); - let mut child = ts - .ucmd() - .timeout(Duration::from_secs(60)) - .arg("20.0") - .run_no_wait(); - - child.kill().make_assertion().is_not_alive(); - } - - #[cfg(feature = "sleep")] - #[test] - fn test_uchild_when_try_kill_and_timeout_is_reached_then_error() { - let ts = TestScenario::new("sleep"); - let mut child = ts.ucmd().timeout(Duration::ZERO).arg("10.0").run_no_wait(); - - match child.try_kill() { - Err(error) if error.kind() == io::ErrorKind::Other => { - std::assert_eq!(error.to_string(), "kill: Timeout of '0s' reached"); - } - Err(error) => panic!("Assertion failed: Expected error with timeout but was: {error}"), - Ok(()) => panic!("Assertion failed: Expected timeout of `try_kill`."), - } - } - - #[cfg(feature = "sleep")] - #[test] - #[should_panic = "kill: Timeout of '0s' reached"] - fn test_uchild_when_kill_with_timeout_and_timeout_is_reached_then_panic() { - let ts = TestScenario::new("sleep"); - let mut child = ts.ucmd().timeout(Duration::ZERO).arg("10.0").run_no_wait(); - - child.kill(); - panic!("Assertion failed: Expected timeout of `kill`."); - } - - #[cfg(feature = "sleep")] - #[test] - #[should_panic(expected = "wait: Timeout of '1.1s' reached")] - fn test_ucommand_when_run_with_timeout_and_timeout_is_reached_then_panic() { - let ts = TestScenario::new("sleep"); - ts.ucmd() - .timeout(Duration::from_millis(1100)) - .arg("10.0") - .run(); - - panic!("Assertion failed: Expected timeout of `run`.") - } - - #[cfg(feature = "sleep")] - #[rstest] - #[timeout(Duration::from_secs(10))] - fn test_ucommand_when_run_with_timeout_higher_then_execution_time_then_no_panic() { - let ts = TestScenario::new("sleep"); - ts.ucmd().timeout(Duration::from_secs(60)).arg("1.0").run(); - } - - #[cfg(feature = "echo")] - #[test] - fn test_ucommand_when_default() { - let shell_cmd = format!("{TESTS_BINARY} echo -n hello"); - - let mut command = UCommand::new(); - command.arg(&shell_cmd).succeeds().stdout_is("hello"); - - #[cfg(target_os = "android")] - let (expected_bin, expected_arg) = (PathBuf::from("/system/bin/sh"), OsString::from("-c")); - #[cfg(all(unix, not(target_os = "android")))] - let (expected_bin, expected_arg) = (PathBuf::from("/bin/sh"), OsString::from("-c")); - #[cfg(windows)] - let (expected_bin, expected_arg) = (PathBuf::from("cmd"), OsString::from("/C")); - - std::assert_eq!(&expected_bin, command.bin_path.as_ref().unwrap()); - assert!(command.util_name.is_none()); - std::assert_eq!(command.args, &[expected_arg, OsString::from(&shell_cmd)]); - assert!(command.tmpd.is_some()); - } - - #[cfg(feature = "echo")] - #[test] - fn test_ucommand_with_util() { - let tmpd = tempfile::tempdir().unwrap(); - let mut command = UCommand::with_util("echo", Rc::new(tmpd)); - - command - .args(&["-n", "hello"]) - .succeeds() - .stdout_only("hello"); - - std::assert_eq!( - &PathBuf::from(TESTS_BINARY), - command.bin_path.as_ref().unwrap() - ); - std::assert_eq!("echo", &command.util_name.unwrap()); - std::assert_eq!( - &[ - OsString::from("echo"), - OsString::from("-n"), - OsString::from("hello") - ], - command.args.make_contiguous() - ); - assert!(command.tmpd.is_some()); - } - - #[cfg(all(unix, not(target_os = "macos")))] - #[test] - fn test_compare_xattrs() { - use tempfile::tempdir; - - let temp_dir = tempdir().unwrap(); - let file_path1 = temp_dir.path().join("test_file1.txt"); - let file_path2 = temp_dir.path().join("test_file2.txt"); - - File::create(&file_path1).unwrap(); - File::create(&file_path2).unwrap(); - - let test_attr = "user.test_attr"; - let test_value = b"test value"; - xattr::set(&file_path1, test_attr, test_value).unwrap(); - - assert!(!compare_xattrs(&file_path1, &file_path2)); - - xattr::set(&file_path2, test_attr, test_value).unwrap(); - assert!(compare_xattrs(&file_path1, &file_path2)); - } - - #[cfg(unix)] - #[test] - fn test_simulation_of_terminal_false() { - let scene = TestScenario::new("util"); - - let out = scene.cmd("env").arg("sh").arg("is_atty.sh").succeeds(); - std::assert_eq!( - String::from_utf8_lossy(out.stdout()), - "stdin is not atty\nstdout is not atty\nstderr is not atty\n" - ); - std::assert_eq!( - String::from_utf8_lossy(out.stderr()), - "This is an error message.\n" - ); - } - - #[cfg(unix)] - #[test] - fn test_simulation_of_terminal_true() { - let scene = TestScenario::new("util"); - - let out = scene - .cmd("env") - .arg("sh") - .arg("is_atty.sh") - .terminal_simulation(true) - .succeeds(); - std::assert_eq!( - String::from_utf8_lossy(out.stdout()), - "stdin is atty\r\nstdout is atty\r\nstderr is atty\r\nterminal size: 30 80\r\n" - ); - std::assert_eq!( - String::from_utf8_lossy(out.stderr()), - "This is an error message.\r\n" - ); - } - - #[cfg(unix)] - #[test] - fn test_simulation_of_terminal_size_information() { - let scene = TestScenario::new("util"); - - let out = scene - .cmd("env") - .arg("sh") - .arg("is_atty.sh") - .terminal_size(libc::winsize { - ws_col: 40, - ws_row: 10, - ws_xpixel: 40 * 8, - ws_ypixel: 10 * 10, - }) - .succeeds(); - std::assert_eq!( - String::from_utf8_lossy(out.stdout()), - "stdin is atty\r\nstdout is atty\r\nstderr is atty\r\nterminal size: 10 40\r\n" - ); - std::assert_eq!( - String::from_utf8_lossy(out.stderr()), - "This is an error message.\r\n" - ); - } - - #[cfg(unix)] - #[test] - fn test_simulation_of_terminal_pty_sends_eot_automatically() { - let scene = TestScenario::new("util"); - - let mut cmd = scene.cmd("env"); - cmd.timeout(std::time::Duration::from_secs(10)); - cmd.args(&["cat", "-"]); - cmd.terminal_simulation(true); - let child = cmd.run_no_wait(); - let out = child.wait().unwrap(); // cat would block if there is no eot - - std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); - std::assert_eq!(String::from_utf8_lossy(out.stdout()), "\r\n"); - } - - #[cfg(unix)] - #[test] - fn test_simulation_of_terminal_pty_pipes_into_data_and_sends_eot_automatically() { - let scene = TestScenario::new("util"); - - let message = "Hello stdin forwarding!"; - - let mut cmd = scene.cmd("env"); - cmd.args(&["cat", "-"]); - cmd.terminal_simulation(true); - cmd.pipe_in(message); - let child = cmd.run_no_wait(); - let out = child.wait().unwrap(); - - std::assert_eq!( - String::from_utf8_lossy(out.stdout()), - format!("{}\r\n", message) - ); - std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); - } - - #[cfg(unix)] - #[test] - fn test_simulation_of_terminal_pty_write_in_data_and_sends_eot_automatically() { - let scene = TestScenario::new("util"); - - let mut cmd = scene.cmd("env"); - cmd.args(&["cat", "-"]); - cmd.terminal_simulation(true); - let mut child = cmd.run_no_wait(); - child.write_in("Hello stdin forwarding via write_in!"); - let out = child.wait().unwrap(); - - std::assert_eq!( - String::from_utf8_lossy(out.stdout()), - "Hello stdin forwarding via write_in!\r\n" - ); - std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); - } -} diff --git a/tests/tests.rs b/tests/tests.rs index 0fb3ed14..47472878 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,10 +1,20 @@ -// This file is part of the uutils coreutils package. +// This file is part of the uutils util-linux package. // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -#[macro_use] -mod common; +use std::env; + +pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_util-linux"); + +// Use the ctor attribute to run this function before any tests +#[ctor::ctor] +fn init() { + unsafe { + // Necessary for uutests to be able to find the binary + std::env::set_var("UUTESTS_BINARY_PATH", TESTS_BINARY); + } +} #[cfg(feature = "lscpu")] #[path = "by-util/test_lscpu.rs"] mod test_lscpu;