From 2b5b0c82c162a19da7b0ba587d2cde8d87dd4823 Mon Sep 17 00:00:00 2001 From: Joining7943 <111500881+Joining7943@users.noreply.github.com> Date: Mon, 30 Jan 2023 08:53:28 +0100 Subject: [PATCH 1/9] tests/util: Bump rlimit version to 0.9.1 to be able to use `prlimit` on android --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- tests/common/util.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d210d303..05b2a1e1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1833,9 +1833,9 @@ dependencies = [ [[package]] name = "rlimit" -version = "0.8.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7278a1ec8bfd4a4e07515c589f5ff7b309a373f987393aef44813d9dcf87aa3" +checksum = "f8a29d87a652dc4d43c586328706bb5cdff211f3f39a530f240b53f7221dab8e" dependencies = [ "libc", ] diff --git a/Cargo.toml b/Cargo.toml index ebd8685ca..9e5e0570a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -482,7 +482,7 @@ rstest = "0.16.0" [target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] procfs = { version = "0.14", default-features = false } -rlimit = "0.8.3" +rlimit = "0.9.1" [target.'cfg(unix)'.dev-dependencies] nix = { workspace=true, features=["process", "signal", "user"] } diff --git a/tests/common/util.rs b/tests/common/util.rs index ed4ecf8e9..eee47eca9 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -8,7 +8,7 @@ #![allow(dead_code)] use pretty_assertions::assert_eq; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] use rlimit::prlimit; use rstest::rstest; #[cfg(unix)] @@ -1406,7 +1406,7 @@ impl UCommand { let child = command.spawn().unwrap(); - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "android"))] for &(resource, soft_limit, hard_limit) in &self.limits { prlimit( child.id() as i32, From fdf0f96a0195126ea1593414c3918d3ab3db1e95 Mon Sep 17 00:00:00 2001 From: Joining7943 <111500881+Joining7943@users.noreply.github.com> Date: Sun, 22 Jan 2023 17:46:25 +0100 Subject: [PATCH 2/9] tests/util: Restructure UCommand to run in shell per default. Introduce constant TESTS_BINARY. Summary of changes in tests/util: * Introduce global constant TESTS_BINARY holding the path to `coreutils` * Implement running an arbitrary command in a sh or cmd shell per default. * Implement std::fmt::Display for UCommand. * Change usages of UCommand::util_name from &Option to Option * `UCommand::new_from_tmp`: Use OsStr directly instead of TempDir -> String -> OsStr * Collect arguments in `UCommand::args` field * Build environment variables in `UCommand` itself instead of `std::process::Command` * Move building of std::process:Command from fields in UCommand to own method `build` * Remove assertions of UCommand::has_run in arg, args and env. Summary of changes in tests/by-util: * Remove usages of UCommand::raw. Fix tests to use UCommand::to_string. * test_test: Adjust test_invalid_utf8_integer_compare to use `UCommand::args` * test_chmod: run_single_test * test_pwd: symlinked_env * test_cp: Fix the usage of &Option in `test_src_base_dot` Refactor test_src_base_dot to not use UCommand::new but ts.ucmd() instead --- tests/by-util/test_chmod.rs | 7 +- tests/by-util/test_cp.rs | 6 +- tests/by-util/test_pwd.rs | 2 +- tests/by-util/test_test.rs | 16 +-- tests/common/util.rs | 258 +++++++++++++++++++++++------------- 5 files changed, 178 insertions(+), 111 deletions(-) diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index feb756632..072756604 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -48,15 +48,12 @@ fn run_single_test(test: &TestCase, at: &AtPath, mut ucmd: UCommand) { let r = ucmd.run(); if !r.succeeded() { println!("{}", r.stderr_str()); - panic!("{:?}: failed", ucmd.raw); + panic!("{}: failed", ucmd); } let perms = at.metadata(TEST_FILE).permissions().mode(); if perms != test.after { - panic!( - "{:?}: expected: {:o} got: {:o}", - ucmd.raw, test.after, perms - ); + panic!("{}: expected: {:o} got: {:o}", ucmd, test.after, perms); } } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index e6e8a6ba2..790383ded 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -2524,9 +2524,9 @@ fn test_src_base_dot() { let at = ts.fixtures.clone(); at.mkdir("x"); at.mkdir("y"); - let mut ucmd = UCommand::new(ts.bin_path, &Some(ts.util_name), at.plus("y"), true); - - ucmd.args(&["--verbose", "-r", "../x/.", "."]) + ts.ucmd() + .current_dir(at.plus("y")) + .args(&["--verbose", "-r", "../x/.", "."]) .succeeds() .no_stderr() .no_stdout(); diff --git a/tests/by-util/test_pwd.rs b/tests/by-util/test_pwd.rs index 0ae0cc909..461c597bd 100644 --- a/tests/by-util/test_pwd.rs +++ b/tests/by-util/test_pwd.rs @@ -60,7 +60,7 @@ fn symlinked_env() -> Env { // Note: on Windows this requires admin permissions at.symlink_dir("subdir", "symdir"); let root = PathBuf::from(at.root_dir_resolved()); - ucmd.raw.current_dir(root.join("symdir")); + ucmd.current_dir(root.join("symdir")); #[cfg(not(windows))] ucmd.env("PWD", root.join("symdir")); Env { diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index 146809f48..b4fb1b0ce 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -300,19 +300,15 @@ fn test_invalid_utf8_integer_compare() { let source = [0x66, 0x6f, 0x80, 0x6f]; let arg = OsStr::from_bytes(&source[..]); - let mut cmd = new_ucmd!(); - cmd.arg("123").arg("-ne"); - cmd.raw.arg(arg); - - cmd.run() + new_ucmd!() + .args(&[OsStr::new("123"), OsStr::new("-ne"), arg]) + .run() .code_is(2) .stderr_is("test: invalid integer $'fo\\x80o'\n"); - let mut cmd = new_ucmd!(); - cmd.raw.arg(arg); - cmd.arg("-eq").arg("456"); - - cmd.run() + new_ucmd!() + .args(&[arg, OsStr::new("-eq"), OsStr::new("456")]) + .run() .code_is(2) .stderr_is("test: invalid integer $'fo\\x80o'\n"); } diff --git a/tests/common/util.rs b/tests/common/util.rs index eee47eca9..0f3f0d7d8 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -3,7 +3,7 @@ // * 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 +//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL #![allow(dead_code)] @@ -15,7 +15,7 @@ use rstest::rstest; use std::borrow::Cow; #[cfg(not(windows))] use std::ffi::CString; -use std::ffi::OsStr; +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)] @@ -34,7 +34,6 @@ use std::thread::{sleep, JoinHandle}; use std::time::{Duration, Instant}; use std::{env, hint, thread}; use tempfile::{Builder, TempDir}; -use uucore::Args; static TESTS_DIR: &str = "tests"; static FIXTURES_DIR: &str = "fixtures"; @@ -46,6 +45,8 @@ static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin"; +pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_coreutils"); + /// Test if the program is running under CI pub fn is_ci() -> bool { std::env::var("CI") @@ -1096,7 +1097,7 @@ impl TestScenario { pub fn new(util_name: &str) -> Self { let tmpd = Rc::new(TempDir::new().unwrap()); let ts = Self { - bin_path: PathBuf::from(env!("CARGO_BIN_EXE_coreutils")), + bin_path: PathBuf::from(TESTS_BINARY), util_name: String::from(util_name), fixtures: AtPath::new(tmpd.as_ref().path()), tmpd, @@ -1127,13 +1128,13 @@ impl TestScenario { util_name: T, env_clear: bool, ) -> UCommand { - UCommand::new_from_tmp(bin, &Some(util_name), self.tmpd.clone(), env_clear) + UCommand::new_from_tmp(bin, Some(util_name), self.tmpd.clone(), env_clear) } /// 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: S) -> UCommand { - UCommand::new_from_tmp::(bin, &None, self.tmpd.clone(), true) + UCommand::new_from_tmp::(bin, None, self.tmpd.clone(), true) } /// Returns builder for invoking any uutils command. Paths given are treated @@ -1153,7 +1154,7 @@ impl TestScenario { /// Differs from the builder returned by `cmd` in that `cmd_keepenv` does not call /// `Command::env_clear` (Clears the entire environment map for the child process.) pub fn cmd_keepenv>(&self, bin: S) -> UCommand { - UCommand::new_from_tmp::(bin, &None, self.tmpd.clone(), false) + UCommand::new_from_tmp::(bin, None, self.tmpd.clone(), false) } } @@ -1165,16 +1166,19 @@ impl TestScenario { /// 3. it provides convenience construction arguments to set the Command working directory and/or clear its environment. #[derive(Debug)] pub struct UCommand { - pub raw: Command, - comm_string: String, - bin_path: String, - util_name: Option, + args: Vec, + env_vars: Vec<(OsString, OsString)>, + current_dir: Option, + env_clear: bool, + bin_path: Option, + util_name: Option, has_run: bool, ignore_stdin_write_error: bool, stdin: Option, stdout: Option, stderr: Option, bytes_into_stdin: Option>, + // TODO: Why android? #[cfg(any(target_os = "linux", target_os = "android"))] limits: Vec<(rlimit::Resource, u64, u64)>, stderr_to_stdout: bool, @@ -1183,74 +1187,51 @@ pub struct UCommand { } impl UCommand { - pub fn new, S: AsRef, U: AsRef>( - bin_path: T, - util_name: &Option, - curdir: U, - env_clear: bool, - ) -> Self { - let bin_path = bin_path.as_ref(); - let util_name = util_name.as_ref().map(std::convert::AsRef::as_ref); - - let mut ucmd = Self { + pub fn new() -> Self { + Self { tmpd: None, has_run: false, - raw: { - let mut cmd = Command::new(bin_path); - cmd.current_dir(curdir.as_ref()); - if env_clear { - cmd.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") { - cmd.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") { - cmd.env("LD_PRELOAD", ld_preload); - } - } - } - cmd - }, - comm_string: String::from(bin_path.to_str().unwrap()), - bin_path: bin_path.to_str().unwrap().to_string(), - util_name: util_name.map(|un| un.to_str().unwrap().to_string()), + bin_path: None, + current_dir: None, + args: vec![], + env_clear: true, + env_vars: vec![], + util_name: None, ignore_stdin_write_error: false, bytes_into_stdin: None, stdin: None, stdout: None, stderr: None, + // TODO: Why android? #[cfg(any(target_os = "linux", target_os = "android"))] limits: vec![], stderr_to_stdout: false, timeout: Some(Duration::from_secs(30)), - }; - - if let Some(un) = util_name { - ucmd.arg(un); } - - ucmd } pub fn new_from_tmp, S: AsRef>( bin_path: T, - util_name: &Option, + util_name: Option, tmpd: Rc, env_clear: bool, ) -> Self { - let tmpd_path_buf = String::from(tmpd.as_ref().path().to_str().unwrap()); - let mut ucmd: Self = Self::new(bin_path, util_name, tmpd_path_buf, env_clear); + let mut ucmd: Self = Self::new(); + ucmd.bin_path = Some(PathBuf::from(bin_path.as_ref())); + ucmd.util_name = util_name.map(|s| s.as_ref().to_os_string()); ucmd.tmpd = Some(tmpd); + ucmd.env_clear = env_clear; ucmd } + pub fn current_dir(&mut self, current_dir: T) -> &mut Self + where + T: AsRef, + { + self.current_dir = Some(current_dir.as_ref().into()); + self + } + pub fn set_stdin>(&mut self, stdin: T) -> &mut Self { self.stdin = Some(stdin.into()); self @@ -1274,29 +1255,14 @@ impl UCommand { /// 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 { - assert!(!self.has_run, "{}", ALREADY_RUN); - self.comm_string.push(' '); - self.comm_string - .push_str(arg.as_ref().to_str().unwrap_or_default()); - self.raw.arg(arg.as_ref()); + self.args.push(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 { - assert!(!self.has_run, "{}", MULTIPLE_STDIN_MEANINGLESS); - let strings = args - .iter() - .map(|s| s.as_ref().to_os_string()) - .collect_ignore(); - - for s in strings { - self.comm_string.push(' '); - self.comm_string.push_str(&s); - } - - self.raw.args(args.as_ref()); + self.args.extend(args.iter().map(|s| s.as_ref().into())); self } @@ -1331,11 +1297,12 @@ impl UCommand { K: AsRef, V: AsRef, { - assert!(!self.has_run, "{}", ALREADY_RUN); - self.raw.env(key, val); + self.env_vars + .push((key.as_ref().into(), val.as_ref().into())); self } + // TODO: Why android? #[cfg(any(target_os = "linux", target_os = "android"))] pub fn with_limit( &mut self, @@ -1359,26 +1326,85 @@ impl UCommand { self } - /// 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; - log_info("run", &self.comm_string); + // TODO: make public? + fn build(&mut self) -> (Command, Option, Option) { + if self.bin_path.is_some() { + if let Some(util_name) = &self.util_name { + self.args.insert(0, OsString::from(util_name)); + } + } else if let Some(util_name) = &self.util_name { + self.bin_path = Some(PathBuf::from(TESTS_BINARY)); + self.args.insert(0, OsString::from(util_name)); + } else if cfg!(unix) { + let bin_path = if cfg!(target_os = "android") { + PathBuf::from("/system/bin/sh") + } else { + PathBuf::from("/bin/sh") + }; + self.bin_path = Some(bin_path); + let c_arg = OsString::from("-c"); + if !self.args.contains(&c_arg) { + self.args.insert(0, 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.insert(0, c_arg); + } + }; + + let mut command = Command::new(self.bin_path.as_ref().unwrap()); + command.args(&self.args); + + if self.tmpd.is_none() { + self.tmpd = Some(Rc::new(tempfile::tempdir().unwrap())); + } + + if let Some(current_dir) = &self.current_dir { + command.current_dir(current_dir); + } else { + command.current_dir(self.tmpd.as_ref().unwrap().path()); + } + + if self.env_clear { + 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); + } + } + } + + for (key, value) in &self.env_vars { + command.env(key, value); + } let mut captured_stdout = None; let mut captured_stderr = None; - let command = if self.stderr_to_stdout { + if self.stderr_to_stdout { let mut output = CapturedOutput::default(); - let command = self - .raw + 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); - - command } else { let stdout = if self.stdout.is_some() { self.stdout.take().unwrap() @@ -1398,12 +1424,25 @@ impl UCommand { stdio }; - self.raw + command .stdin(self.stdin.take().unwrap_or_else(Stdio::null)) .stdout(stdout) - .stderr(stderr) + .stderr(stderr); }; + (command, captured_stdout, captured_stderr) + } + + /// Spawns the command, feeds the stdin if any, and returns the + /// child process immediately. + pub fn run_no_wait(&mut self) -> UChild { + // TODO: remove? + assert!(!self.has_run, "{}", ALREADY_RUN); + self.has_run = true; + + let (mut command, captured_stdout, captured_stderr) = self.build(); + log_info("run", self.to_string()); + let child = command.spawn().unwrap(); #[cfg(any(target_os = "linux", target_os = "android"))] @@ -1465,6 +1504,17 @@ impl UCommand { } } +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("".to_string(), |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)] @@ -1704,8 +1754,11 @@ impl UChild { ) -> Self { Self { raw: child, - bin_path: ucommand.bin_path.clone(), - util_name: ucommand.util_name.clone(), + bin_path: ucommand.bin_path.as_ref().unwrap().display().to_string(), + util_name: ucommand + .util_name + .clone() + .map(|s| s.to_string_lossy().to_string()), captured_stdout, captured_stderr, ignore_stdin_write_error: ucommand.ignore_stdin_write_error, @@ -2443,7 +2496,7 @@ mod tests { pub fn run_cmd>(cmd: T) -> CmdResult { let mut ucmd = UCommand::new_from_tmp::<&str, String>( "sh", - &None, + None, Rc::new(tempfile::tempdir().unwrap()), true, ); @@ -2456,7 +2509,7 @@ mod tests { pub fn run_cmd>(cmd: T) -> CmdResult { let mut ucmd = UCommand::new_from_tmp::<&str, String>( "cmd", - &None, + None, Rc::new(tempfile::tempdir().unwrap()), true, ); @@ -3200,4 +3253,25 @@ mod tests { 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!("{} echo -n hello", TESTS_BINARY); + + 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()); + } } From 1c230fd7795932dadd89f5cc29bedfda7b157bb8 Mon Sep 17 00:00:00 2001 From: Joining7943 <111500881+Joining7943@users.noreply.github.com> Date: Wed, 25 Jan 2023 03:40:39 +0100 Subject: [PATCH 3/9] tests/util: Refactor UCommand and TestScenario. Summary of changes in UCommand: * Extend UCommand by builder methods and simplify methods in TestScenario * Simplify code structures where possible. Add documentation. * Store bin_path as PathBuf and util_name as String in all structs * Remove UCommand::util and make bin_path, temp_dir private * Rename UCommand::with_limit -> UCommand::limit Summary of changes in TestScenario: * Rename some parameters in TestScenario methods to be more descriptive * Remove ucmd_keepenv, cmd_keepenv from TestScenario. Use UCommand::keep_env instead. --- tests/by-util/test_cat.rs | 2 +- tests/by-util/test_chmod.rs | 2 +- tests/by-util/test_chown.rs | 14 +- tests/by-util/test_cp.rs | 11 +- tests/by-util/test_env.rs | 3 +- tests/by-util/test_mktemp.rs | 6 +- tests/by-util/test_nproc.rs | 51 +++-- tests/by-util/test_printenv.rs | 6 +- tests/by-util/test_sort.rs | 21 +- tests/by-util/test_uptime.rs | 3 +- tests/by-util/test_users.rs | 3 +- tests/common/util.rs | 339 +++++++++++++++++++-------------- 12 files changed, 276 insertions(+), 185 deletions(-) diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index bef9e76e4..8b6022c93 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -103,7 +103,7 @@ fn test_closes_file_descriptors() { "alpha.txt", "alpha.txt", ]) - .with_limit(Resource::NOFILE, 9, 9) + .limit(Resource::NOFILE, 9, 9) .succeeds(); } diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 072756604..52b43d32e 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -48,7 +48,7 @@ fn run_single_test(test: &TestCase, at: &AtPath, mut ucmd: UCommand) { let r = ucmd.run(); if !r.succeeded() { println!("{}", r.stderr_str()); - panic!("{}: failed", ucmd); + panic!("{ucmd}: failed"); } let perms = at.metadata(TEST_FILE).permissions().mode(); diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 5237a7cf7..c0e210435 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -396,7 +396,7 @@ fn test_chown_only_user_id() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - let result = scene.cmd_keepenv("id").arg("-u").run(); + let result = scene.cmd("id").keep_env().arg("-u").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } @@ -430,7 +430,7 @@ fn test_chown_fail_id() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - let result = scene.cmd_keepenv("id").arg("-u").run(); + let result = scene.cmd("id").keep_env().arg("-u").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } @@ -487,7 +487,7 @@ fn test_chown_only_group_id() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - let result = scene.cmd_keepenv("id").arg("-g").run(); + let result = scene.cmd("id").keep_env().arg("-g").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } @@ -551,14 +551,14 @@ fn test_chown_owner_group_id() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - let result = scene.cmd_keepenv("id").arg("-u").run(); + let result = scene.cmd("id").keep_env().arg("-u").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let user_id = String::from(result.stdout_str().trim()); assert!(!user_id.is_empty()); - let result = scene.cmd_keepenv("id").arg("-g").run(); + let result = scene.cmd("id").keep_env().arg("-g").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } @@ -612,14 +612,14 @@ fn test_chown_owner_group_mix() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - let result = scene.cmd_keepenv("id").arg("-u").run(); + let result = scene.cmd("id").keep_env().arg("-u").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let user_id = String::from(result.stdout_str().trim()); assert!(!user_id.is_empty()); - let result = scene.cmd_keepenv("id").arg("-gn").run(); + let result = scene.cmd("id").keep_env().arg("-gn").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 790383ded..01eb9bd00 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1545,7 +1545,7 @@ fn test_closes_file_descriptors() { .arg("--reflink=auto") .arg("dir_with_10_files/") .arg("dir_with_10_files_new/") - .with_limit(Resource::NOFILE, limit_fd, limit_fd) + .limit(Resource::NOFILE, limit_fd, limit_fd) .succeeds(); } @@ -1692,7 +1692,8 @@ fn test_cp_reflink_always_override() { .succeeds(); if !scene - .cmd_keepenv("env") + .cmd("env") + .keep_env() .args(&["mkfs.btrfs", "--rootdir", ROOTDIR, DISK]) .run() .succeeded() @@ -1704,7 +1705,8 @@ fn test_cp_reflink_always_override() { scene.fixtures.mkdir(MOUNTPOINT); let mount = scene - .cmd_keepenv("sudo") + .cmd("sudo") + .keep_env() .args(&["-E", "--non-interactive", "mount", DISK, MOUNTPOINT]) .run(); @@ -1730,7 +1732,8 @@ fn test_cp_reflink_always_override() { .succeeds(); scene - .cmd_keepenv("sudo") + .cmd("sudo") + .keep_env() .args(&["-E", "--non-interactive", "umount", MOUNTPOINT]) .succeeds(); } diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index ef10512ab..9b386541c 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -156,7 +156,8 @@ fn test_unset_variable() { // This test depends on the HOME variable being pre-defined by the // default shell let out = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("-u") .arg("HOME") .succeeds() diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index d6926c41b..109963edf 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -425,7 +425,8 @@ fn test_mktemp_tmpdir_one_arg() { let scene = TestScenario::new(util_name!()); let result = scene - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("--tmpdir") .arg("apt-key-gpghome.XXXXXXXXXX") .succeeds(); @@ -438,7 +439,8 @@ fn test_mktemp_directory_tmpdir() { let scene = TestScenario::new(util_name!()); let result = scene - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("--directory") .arg("--tmpdir") .arg("apt-key-gpghome.XXXXXXXXXX") diff --git a/tests/by-util/test_nproc.rs b/tests/by-util/test_nproc.rs index 3260e46e7..abae40697 100644 --- a/tests/by-util/test_nproc.rs +++ b/tests/by-util/test_nproc.rs @@ -20,7 +20,8 @@ fn test_nproc_all_omp() { assert!(nproc > 0); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "60") .succeeds(); @@ -28,7 +29,8 @@ fn test_nproc_all_omp() { assert_eq!(nproc_omp, 60); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "1") // Has no effect .arg("--all") .succeeds(); @@ -37,7 +39,8 @@ fn test_nproc_all_omp() { // If the parsing fails, returns the number of CPU let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "incorrectnumber") // returns the number CPU .succeeds(); let nproc_omp: u8 = result.stdout_str().trim().parse().unwrap(); @@ -51,7 +54,8 @@ fn test_nproc_ignore() { if nproc_total > 1 { // Ignore all CPU but one let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("--ignore") .arg((nproc_total - 1).to_string()) .succeeds(); @@ -59,7 +63,8 @@ fn test_nproc_ignore() { assert_eq!(nproc, 1); // Ignore all CPU but one with a string let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("--ignore= 1") .succeeds(); let nproc: u8 = result.stdout_str().trim().parse().unwrap(); @@ -70,7 +75,8 @@ fn test_nproc_ignore() { #[test] fn test_nproc_ignore_all_omp() { let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "42") .arg("--ignore=40") .succeeds(); @@ -81,7 +87,8 @@ fn test_nproc_ignore_all_omp() { #[test] fn test_nproc_omp_limit() { let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "42") .env("OMP_THREAD_LIMIT", "0") .succeeds(); @@ -89,7 +96,8 @@ fn test_nproc_omp_limit() { assert_eq!(nproc, 42); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "42") .env("OMP_THREAD_LIMIT", "2") .succeeds(); @@ -97,7 +105,8 @@ fn test_nproc_omp_limit() { assert_eq!(nproc, 2); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "42") .env("OMP_THREAD_LIMIT", "2bad") .succeeds(); @@ -109,14 +118,16 @@ fn test_nproc_omp_limit() { assert!(nproc_system > 0); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_THREAD_LIMIT", "1") .succeeds(); let nproc: u8 = result.stdout_str().trim().parse().unwrap(); assert_eq!(nproc, 1); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "0") .env("OMP_THREAD_LIMIT", "") .succeeds(); @@ -124,7 +135,8 @@ fn test_nproc_omp_limit() { assert_eq!(nproc, nproc_system); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "") .env("OMP_THREAD_LIMIT", "") .succeeds(); @@ -132,7 +144,8 @@ fn test_nproc_omp_limit() { assert_eq!(nproc, nproc_system); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "2,2,1") .env("OMP_THREAD_LIMIT", "") .succeeds(); @@ -140,7 +153,8 @@ fn test_nproc_omp_limit() { assert_eq!(2, nproc); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "2,ignored") .env("OMP_THREAD_LIMIT", "") .succeeds(); @@ -148,7 +162,8 @@ fn test_nproc_omp_limit() { assert_eq!(2, nproc); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "2,2,1") .env("OMP_THREAD_LIMIT", "0") .succeeds(); @@ -156,7 +171,8 @@ fn test_nproc_omp_limit() { assert_eq!(2, nproc); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "2,2,1") .env("OMP_THREAD_LIMIT", "1bad") .succeeds(); @@ -164,7 +180,8 @@ fn test_nproc_omp_limit() { assert_eq!(2, nproc); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .env("OMP_NUM_THREADS", "29,2,1") .env("OMP_THREAD_LIMIT", "1bad") .succeeds(); diff --git a/tests/by-util/test_printenv.rs b/tests/by-util/test_printenv.rs index 29ca24857..c4f32705f 100644 --- a/tests/by-util/test_printenv.rs +++ b/tests/by-util/test_printenv.rs @@ -8,7 +8,8 @@ fn test_get_all() { assert_eq!(env::var(key), Ok("VALUE".to_string())); TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .succeeds() .stdout_contains("HOME=") .stdout_contains("KEY=VALUE"); @@ -21,7 +22,8 @@ fn test_get_var() { assert_eq!(env::var(key), Ok("VALUE".to_string())); let result = TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("KEY") .succeeds(); diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 8a03432af..174ac255c 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -31,7 +31,8 @@ fn test_buffer_sizes() { let buffer_sizes = ["0", "50K", "50k", "1M", "100M"]; for buffer_size in &buffer_sizes { TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("-n") .arg("-S") .arg(buffer_size) @@ -44,7 +45,8 @@ fn test_buffer_sizes() { let buffer_sizes = ["1000G", "10T"]; for buffer_size in &buffer_sizes { TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("-n") .arg("-S") .arg(buffer_size) @@ -918,7 +920,8 @@ fn test_compress_merge() { fn test_compress_fail() { #[cfg(not(windows))] TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .args(&[ "ext_sort.txt", "-n", @@ -934,7 +937,8 @@ fn test_compress_fail() { // So, don't check the output #[cfg(windows)] TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .args(&[ "ext_sort.txt", "-n", @@ -949,7 +953,8 @@ fn test_compress_fail() { #[test] fn test_merge_batches() { TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .timeout(Duration::from_secs(120)) .args(&["ext_sort.txt", "-n", "-S", "150b"]) .succeeds() @@ -959,7 +964,8 @@ fn test_merge_batches() { #[test] fn test_merge_batch_size() { TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .arg("--batch-size=2") .arg("-m") .arg("--unique") @@ -1067,7 +1073,8 @@ fn test_output_is_input() { at.touch("file"); at.append("file", input); scene - .ucmd_keepenv() + .ucmd() + .keep_env() .args(&["-m", "-u", "-o", "file", "file", "file", "file"]) .succeeds(); assert_eq!(at.read("file"), input); diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index 946bb30fa..02cba9e8f 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -10,7 +10,8 @@ fn test_invalid_arg() { #[test] fn test_uptime() { TestScenario::new(util_name!()) - .ucmd_keepenv() + .ucmd() + .keep_env() .succeeds() .stdout_contains("load average:") .stdout_contains(" up "); diff --git a/tests/by-util/test_users.rs b/tests/by-util/test_users.rs index 747995d99..7fadb2bb2 100644 --- a/tests/by-util/test_users.rs +++ b/tests/by-util/test_users.rs @@ -22,7 +22,8 @@ fn test_users_check_name() { // note: clippy::needless_borrow *false positive* #[allow(clippy::needless_borrow)] let expected = TestScenario::new(&util_name) - .cmd_keepenv(util_name) + .cmd(util_name) + .keep_env() .env("LC_ALL", "C") .succeeds() .stdout_move_str(); diff --git a/tests/common/util.rs b/tests/common/util.rs index 0f3f0d7d8..3097a762f 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -3,7 +3,7 @@ // * 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 +//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized #![allow(dead_code)] @@ -13,6 +13,7 @@ use rlimit::prlimit; use rstest::rstest; #[cfg(unix)] use std::borrow::Cow; +use std::collections::VecDeque; #[cfg(not(windows))] use std::ffi::CString; use std::ffi::{OsStr, OsString}; @@ -65,7 +66,7 @@ fn read_scenario_fixture>(tmpd: &Option>, file_rel_p #[derive(Debug, Clone)] pub struct CmdResult { /// bin_path provided by `TestScenario` or `UCommand` - bin_path: String, + bin_path: PathBuf, /// util_name provided by `TestScenario` or `UCommand` util_name: Option, //tmpd is used for convenience functions for asserts against fixtures @@ -79,21 +80,23 @@ pub struct CmdResult { } impl CmdResult { - pub fn new( - bin_path: String, - util_name: Option, + pub fn new( + bin_path: S, + util_name: Option, tmpd: Option>, exit_status: Option, - stdout: T, - stderr: U, + stdout: U, + stderr: V, ) -> Self where - T: Into>, + S: Into, + T: AsRef, U: Into>, + V: Into>, { Self { - bin_path, - util_name, + bin_path: bin_path.into(), + util_name: util_name.map(|s| s.as_ref().into()), tmpd, exit_status, stdout: stdout.into(), @@ -635,7 +638,7 @@ impl CmdResult { 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, + self.bin_path.display(), msg.as_ref() )) } @@ -1094,18 +1097,21 @@ pub struct TestScenario { } impl TestScenario { - pub fn new(util_name: &str) -> Self { + 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: String::from(util_name), + 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); + 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(); @@ -1117,68 +1123,57 @@ impl TestScenario { /// 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 { - self.composite_cmd(&self.bin_path, &self.util_name, true) - } - - /// Returns builder for invoking the target uutils binary. Paths given are - /// treated relative to the environment's unique temporary test directory. - pub fn composite_cmd, T: AsRef>( - &self, - bin: S, - util_name: T, - env_clear: bool, - ) -> UCommand { - UCommand::new_from_tmp(bin, Some(util_name), self.tmpd.clone(), env_clear) + 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: S) -> UCommand { - UCommand::new_from_tmp::(bin, None, self.tmpd.clone(), true) + 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, bin: S) -> UCommand { - self.composite_cmd(&self.bin_path, bin, true) - } - - // different names are used rather than an argument - // because the need to keep the environment is exceedingly rare. - pub fn ucmd_keepenv(&self) -> UCommand { - self.composite_cmd(&self.bin_path, &self.util_name, false) - } - - /// Returns builder for invoking any system command. Paths given are treated - /// relative to the environment's unique temporary test directory. - /// Differs from the builder returned by `cmd` in that `cmd_keepenv` does not call - /// `Command::env_clear` (Clears the entire environment map for the child process.) - pub fn cmd_keepenv>(&self, bin: S) -> UCommand { - UCommand::new_from_tmp::(bin, None, self.tmpd.clone(), false) + pub fn ccmd>(&self, util_name: S) -> UCommand { + UCommand::with_util(util_name, self.tmpd.clone()) } } -/// A `UCommand` is a wrapper around an individual Command that provides several additional features +/// 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 arguments to set the Command working directory and/or clear its environment. -#[derive(Debug)] +/// 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 `coreutils UTIL_NAME` instead of the shell +/// * [`UCommand::from_test_scenario`]: Run `coreutils UTIL_NAME` instead of the shell in the +/// temporary directory of the [`TestScenario`] +/// * [`UCommand::current_dir`]: Sets the working directory +/// * [`UCommand::keep_env`]: Keep environment variables instead of clearing them +/// * ... +#[derive(Debug, Default)] pub struct UCommand { - args: Vec, + args: VecDeque, env_vars: Vec<(OsString, OsString)>, current_dir: Option, env_clear: bool, bin_path: Option, - util_name: Option, + util_name: Option, has_run: bool, ignore_stdin_write_error: bool, stdin: Option, stdout: Option, stderr: Option, bytes_into_stdin: Option>, - // TODO: Why android? #[cfg(any(target_os = "linux", target_os = "android"))] limits: Vec<(rlimit::Resource, u64, u64)>, stderr_to_stdout: bool, @@ -1187,48 +1182,79 @@ pub struct UCommand { } 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 { - tmpd: None, - has_run: false, - bin_path: None, - current_dir: None, - args: vec![], env_clear: true, - env_vars: vec![], - util_name: None, - ignore_stdin_write_error: false, - bytes_into_stdin: None, - stdin: None, - stdout: None, - stderr: None, - // TODO: Why android? - #[cfg(any(target_os = "linux", target_os = "android"))] - limits: vec![], - stderr_to_stdout: false, - timeout: Some(Duration::from_secs(30)), + ..Default::default() } } - pub fn new_from_tmp, S: AsRef>( - bin_path: T, - util_name: Option, - tmpd: Rc, - env_clear: bool, - ) -> Self { - let mut ucmd: Self = Self::new(); - ucmd.bin_path = Some(PathBuf::from(bin_path.as_ref())); - ucmd.util_name = util_name.map(|s| s.as_ref().to_os_string()); - ucmd.tmpd = Some(tmpd); - ucmd.env_clear = env_clear; + /// Create a [`UCommand`] for a specific uutils utility. + /// + /// Sets the temporary directory to `tmpd` and the execution binary to the path where + /// `coreutils` 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 `coreutils`. + 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 + } + + /// Keep the environment variables instead of clearing them before running the command. + pub fn keep_env(&mut self) -> &mut Self { + self.env_clear = false; + 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: AsRef, + T: Into, { - self.current_dir = Some(current_dir.as_ref().into()); + self.current_dir = Some(current_dir.into()); self } @@ -1255,7 +1281,7 @@ impl UCommand { /// 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(arg.as_ref().into()); + self.args.push_back(arg.as_ref().into()); self } @@ -1302,9 +1328,8 @@ impl UCommand { self } - // TODO: Why android? #[cfg(any(target_os = "linux", target_os = "android"))] - pub fn with_limit( + pub fn limit( &mut self, resource: rlimit::Resource, soft_limit: u64, @@ -1326,25 +1351,46 @@ impl UCommand { self } - // TODO: make public? + /// 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` + /// * `env_clear`: `true`. (Almost) all environment variables will be cleared. + /// * `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) { if self.bin_path.is_some() { if let Some(util_name) = &self.util_name { - self.args.insert(0, OsString::from(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.insert(0, OsString::from(util_name)); + 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) { - let bin_path = if cfg!(target_os = "android") { - PathBuf::from("/system/bin/sh") - } else { - PathBuf::from("/bin/sh") - }; + #[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.insert(0, c_arg); + self.args.push_front(c_arg); } } else { self.bin_path = Some(PathBuf::from("cmd")); @@ -1355,21 +1401,26 @@ impl UCommand { .iter() .any(|s| s.eq_ignore_ascii_case(&c_arg) || s.eq_ignore_ascii_case(&k_arg)) { - self.args.insert(0, c_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); - if self.tmpd.is_none() { - self.tmpd = Some(Rc::new(tempfile::tempdir().unwrap())); - } - + // 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 { - command.current_dir(self.tmpd.as_ref().unwrap().path()); + 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)); } if self.env_clear { @@ -1391,8 +1442,10 @@ impl UCommand { } } - for (key, value) in &self.env_vars { - command.env(key, value); + command.envs(self.env_vars.iter().cloned()); + + if self.timeout.is_none() { + self.timeout = Some(Duration::from_secs(30)); } let mut captured_stdout = None; @@ -1436,7 +1489,6 @@ impl UCommand { /// Spawns the command, feeds the stdin if any, and returns the /// child process immediately. pub fn run_no_wait(&mut self) -> UChild { - // TODO: remove? assert!(!self.has_run, "{}", ALREADY_RUN); self.has_run = true; @@ -1509,7 +1561,7 @@ impl std::fmt::Display for UCommand { let mut comm_string: Vec = vec![self .bin_path .as_ref() - .map_or("".to_string(), |p| p.display().to_string())]; + .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(" ")) } @@ -1647,14 +1699,14 @@ impl<'a> UChildAssertion<'a> { self.uchild.stderr_exact_bytes(expected_stderr_size), ), }; - CmdResult { - bin_path: self.uchild.bin_path.clone(), - util_name: self.uchild.util_name.clone(), - tmpd: self.uchild.tmpd.clone(), + 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. @@ -1734,7 +1786,7 @@ impl<'a> UChildAssertion<'a> { /// Abstraction for a [`std::process::Child`] to handle the child process. pub struct UChild { raw: Child, - bin_path: String, + bin_path: PathBuf, util_name: Option, captured_stdout: Option, captured_stderr: Option, @@ -1754,11 +1806,8 @@ impl UChild { ) -> Self { Self { raw: child, - bin_path: ucommand.bin_path.as_ref().unwrap().display().to_string(), - util_name: ucommand - .util_name - .clone() - .map(|s| s.to_string_lossy().to_string()), + bin_path: ucommand.bin_path.clone().unwrap(), + util_name: ucommand.util_name.clone(), captured_stdout, captured_stderr, ignore_stdin_write_error: ucommand.ignore_stdin_write_error, @@ -2388,11 +2437,13 @@ fn parse_coreutil_version(version_string: &str) -> f32 { ///``` #[cfg(unix)] pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result { - println!("{}", check_coreutil_version(&ts.util_name, VERSION_MIN)?); - let util_name = &host_name_for(&ts.util_name); + 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_keepenv(util_name.as_ref()) + .cmd(util_name.as_ref()) + .keep_env() .env("LC_ALL", "C") .args(args) .run(); @@ -2464,7 +2515,8 @@ pub fn run_ucmd_as_root( // we can run sudo and we're root // run ucmd as root: Ok(ts - .cmd_keepenv("sudo") + .cmd("sudo") + .keep_env() .env("LC_ALL", "C") .arg("-E") .arg("--non-interactive") @@ -2492,30 +2544,8 @@ mod tests { // spell-checker:ignore (tests) asdfsadfa use super::*; - #[cfg(unix)] pub fn run_cmd>(cmd: T) -> CmdResult { - let mut ucmd = UCommand::new_from_tmp::<&str, String>( - "sh", - None, - Rc::new(tempfile::tempdir().unwrap()), - true, - ); - ucmd.arg("-c"); - ucmd.arg(cmd); - ucmd.run() - } - - #[cfg(windows)] - pub fn run_cmd>(cmd: T) -> CmdResult { - let mut ucmd = UCommand::new_from_tmp::<&str, String>( - "cmd", - None, - Rc::new(tempfile::tempdir().unwrap()), - true, - ); - ucmd.arg("/C"); - ucmd.arg(cmd); - ucmd.run() + UCommand::new().arg(cmd).run() } #[test] @@ -3257,7 +3287,7 @@ mod tests { #[cfg(feature = "echo")] #[test] fn test_ucommand_when_default() { - let shell_cmd = format!("{} echo -n hello", TESTS_BINARY); + let shell_cmd = format!("{TESTS_BINARY} echo -n hello"); let mut command = UCommand::new(); command.arg(&shell_cmd).succeeds().stdout_is("hello"); @@ -3274,4 +3304,31 @@ mod tests { 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()); + } } From e0470c02940ea93e8980aae225bbed52766e2bd1 Mon Sep 17 00:00:00 2001 From: Koki Ueha Date: Mon, 20 Feb 2023 11:30:33 +0000 Subject: [PATCH 4/9] tac: move help strings to markdown file --- src/uu/tac/src/tac.rs | 6 +++--- src/uu/tac/tac.md | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 src/uu/tac/tac.md diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index b0d79e6da..aef9932a2 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -19,12 +19,12 @@ use std::{ use uucore::display::Quotable; use uucore::error::UError; use uucore::error::UResult; -use uucore::{format_usage, show}; +use uucore::{format_usage, help_about, help_usage, show}; use crate::error::TacError; -static USAGE: &str = "{} [OPTION]... [FILE]..."; -static ABOUT: &str = "Write each file to standard output, last line first."; +static USAGE: &str = help_usage!("tac.md"); +static ABOUT: &str = help_about!("tac.md"); mod options { pub static BEFORE: &str = "before"; diff --git a/src/uu/tac/tac.md b/src/uu/tac/tac.md new file mode 100644 index 000000000..6787b3f49 --- /dev/null +++ b/src/uu/tac/tac.md @@ -0,0 +1,7 @@ +# tac + +``` +tac [OPTION]... [FILE]... +``` + +Write each file to standard output, last line first. From 9bf9bba7c319ee79f5fd5e5c8c2ec343488e1a20 Mon Sep 17 00:00:00 2001 From: Koki Ueha Date: Mon, 20 Feb 2023 11:56:39 +0000 Subject: [PATCH 5/9] kill: move help strings to markdown file --- src/uu/kill/kill.md | 7 +++++++ src/uu/kill/src/kill.rs | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 src/uu/kill/kill.md diff --git a/src/uu/kill/kill.md b/src/uu/kill/kill.md new file mode 100644 index 000000000..1e3e2b3d0 --- /dev/null +++ b/src/uu/kill/kill.md @@ -0,0 +1,7 @@ +# kill + +``` +kill [OPTIONS]... PID... +``` + +Send signal to processes or list information about signals. diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index c14023e82..d18a483fd 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -14,10 +14,10 @@ use std::io::Error; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::signals::{signal_by_name_or_value, ALL_SIGNALS}; -use uucore::{format_usage, show}; +use uucore::{format_usage, help_about, help_usage, show}; -static ABOUT: &str = "Send signal to processes or list information about signals."; -const USAGE: &str = "{} [OPTIONS]... PID..."; +static ABOUT: &str = help_about!("kill.md"); +const USAGE: &str = help_usage!("kill.md"); pub mod options { pub static PIDS_OR_SIGNALS: &str = "pids_or_signals"; From 72f13369aa5f8483160bec439ae280539c13cb54 Mon Sep 17 00:00:00 2001 From: pkubaj Date: Mon, 20 Feb 2023 15:20:31 +0000 Subject: [PATCH 6/9] Add FreeBSD to documentation --- docs/src/installation.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/installation.md b/docs/src/installation.md index e2e72699b..063935ed6 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -107,6 +107,13 @@ brew install uutils-coreutils port install coreutils-uutils ``` +## FreeBSD +[![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/uutils-coreutils.svg)](https://repology.org/project/uutils-coreutils/versions) + +```sh +pkg install uutils +``` + ## Windows ### Scoop From dd4299c32ebef81a450f3434fe186b8faeb18171 Mon Sep 17 00:00:00 2001 From: David Matos Date: Mon, 20 Feb 2023 22:18:07 +0100 Subject: [PATCH 7/9] chmod: supress verbose output when not verbose --- src/uu/chmod/src/chmod.rs | 10 ++++++---- tests/by-util/test_chmod.rs | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 374f22874..eb8db104f 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -190,16 +190,18 @@ impl Chmoder { let file = Path::new(filename); if !file.exists() { if file.is_symlink() { - println!( - "failed to change mode of {} from 0000 (---------) to 0000 (---------)", - filename.quote() - ); if !self.quiet { show!(USimpleError::new( 1, format!("cannot operate on dangling symlink {}", filename.quote()), )); } + if self.verbose { + println!( + "failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)", + filename.quote() + ); + } } else if !self.quiet { show!(USimpleError::new( 1, diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index feb756632..c5f731d7d 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -414,7 +414,7 @@ fn test_chmod_symlink_non_existing_file() { let non_existing = "test_chmod_symlink_non_existing_file"; let test_symlink = "test_chmod_symlink_non_existing_file_symlink"; let expected_stdout = &format!( - "failed to change mode of '{test_symlink}' from 0000 (---------) to 0000 (---------)" + "failed to change mode of '{test_symlink}' from 0000 (---------) to 1500 (r-x-----T)" ); let expected_stderr = &format!("cannot operate on dangling symlink '{test_symlink}'"); @@ -442,6 +442,17 @@ fn test_chmod_symlink_non_existing_file() { .code_is(1) .no_stderr() .stdout_contains(expected_stdout); + + // this should only include the dangling symlink message + // NOT the failure to change mode + scene + .ucmd() + .arg("755") + .arg(test_symlink) + .run() + .code_is(1) + .no_stdout() + .stderr_contains(expected_stderr); } #[test] @@ -616,7 +627,7 @@ fn test_chmod_file_symlink_after_non_existing_file() { let non_existing = "test_chmod_symlink_non_existing_file"; let test_dangling_symlink = "test_chmod_symlink_non_existing_file_symlink"; let expected_stdout = &format!( - "failed to change mode of '{test_dangling_symlink}' from 0000 (---------) to 0000 (---------)" + "failed to change mode of '{test_dangling_symlink}' from 0000 (---------) to 1500 (r-x-----T)" ); let expected_stderr = &format!("cannot operate on dangling symlink '{test_dangling_symlink}'"); From 4e7ae2d46a8550e7bd372a80449662fd646e895e Mon Sep 17 00:00:00 2001 From: papparapa <37232476+papparapa@users.noreply.github.com> Date: Wed, 22 Feb 2023 00:51:59 +0900 Subject: [PATCH 8/9] du: move help strings to markdown file (#4384) --- src/uu/du/du.md | 24 ++++++++++++++++++++++++ src/uu/du/src/du.rs | 28 +++++++--------------------- 2 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 src/uu/du/du.md diff --git a/src/uu/du/du.md b/src/uu/du/du.md new file mode 100644 index 000000000..6f154d047 --- /dev/null +++ b/src/uu/du/du.md @@ -0,0 +1,24 @@ +# du + +``` +du [OPTION]... [FILE]... +du [OPTION]... --files0-from=F +``` + +Estimate file space usage + +## After Help + +Display values are in units of the first available SIZE from --block-size, +and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. +Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). + +SIZE is an integer and optional unit (example: 10M is 10*1024*1024). +Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers +of 1000). + +PATTERN allows some advanced exclusions. For example, the following syntaxes +are supported: +? will match only one character +* will match zero or more characters +{a,b} will match a or b diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index a204adf4d..c35bbf119 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -36,7 +36,9 @@ use uucore::error::FromIo; use uucore::error::{UError, UResult}; use uucore::parse_glob; use uucore::parse_size::{parse_size, ParseSizeError}; -use uucore::{crash, format_usage, show, show_error, show_warning}; +use uucore::{ + crash, format_usage, help_about, help_section, help_usage, show, show_error, show_warning, +}; #[cfg(windows)] use windows_sys::Win32::Foundation::HANDLE; #[cfg(windows)] @@ -73,25 +75,9 @@ mod options { pub const FILE: &str = "FILE"; } -const ABOUT: &str = "Estimate file space usage"; -const LONG_HELP: &str = " -Display values are in units of the first available SIZE from --block-size, -and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. -Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). - -SIZE is an integer and optional unit (example: 10M is 10*1024*1024). -Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers -of 1000). - -PATTERN allows some advanced exclusions. For example, the following syntaxes -are supported: -? will match only one character -* will match zero or more characters -{a,b} will match a or b -"; -const USAGE: &str = "\ - {} [OPTION]... [FILE]... - {} [OPTION]... --files0-from=F"; +const ABOUT: &str = help_about!("du.md"); +const AFTER_HELP: &str = help_section!("after help", "du.md"); +const USAGE: &str = help_usage!("du.md"); // TODO: Support Z & Y (currently limited by size of u64) const UNITS: [(char, u32); 6] = [('E', 6), ('P', 5), ('T', 4), ('G', 3), ('M', 2), ('K', 1)]; @@ -705,7 +691,7 @@ pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) - .after_help(LONG_HELP) + .after_help(AFTER_HELP) .override_usage(format_usage(USAGE)) .infer_long_args(true) .disable_help_flag(true) From 7d7b9eb49ac634c9758b1143d64643fc463c96bd Mon Sep 17 00:00:00 2001 From: "Guilherme A. de Souza" <125218612+rgasnix@users.noreply.github.com> Date: Tue, 21 Feb 2023 17:52:18 -0300 Subject: [PATCH 9/9] Migrate from `atty` to `is-terminal` (#4382) --- Cargo.lock | 162 ++++++++++++++++++++++++++----------- Cargo.toml | 4 +- deny.toml | 14 +++- src/uu/cat/Cargo.toml | 2 +- src/uu/cat/src/cat.rs | 3 +- src/uu/cut/Cargo.toml | 2 +- src/uu/cut/src/cut.rs | 3 +- src/uu/ls/Cargo.toml | 2 +- src/uu/ls/src/ls.rs | 9 ++- src/uu/more/Cargo.toml | 2 +- src/uu/more/src/more.rs | 3 +- src/uu/nohup/Cargo.toml | 2 +- src/uu/nohup/src/nohup.rs | 7 +- src/uu/tail/Cargo.toml | 2 +- src/uu/tail/src/args.rs | 4 +- src/uu/tty/Cargo.toml | 2 +- src/uu/tty/src/tty.rs | 3 +- tests/by-util/test_more.rs | 5 +- 18 files changed, 161 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd2385a64..8742a8092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,7 +67,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -293,7 +293,7 @@ dependencies = [ "lazy_static", "libc", "unicode-width", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -321,7 +321,6 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" name = "coreutils" version = "0.0.17" dependencies = [ - "atty", "chrono", "clap", "clap_complete", @@ -329,6 +328,7 @@ dependencies = [ "filetime", "glob", "hex-literal", + "is-terminal", "libc", "nix", "once_cell", @@ -635,7 +635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71" dependencies = [ "nix", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -834,7 +834,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1045,6 +1045,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -1139,6 +1145,28 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074" +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes 1.0.5", + "rustix 0.36.8", + "windows-sys 0.45.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1235,6 +1263,12 @@ version = "0.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.9" @@ -1326,7 +1360,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1415,7 +1449,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", ] @@ -1545,7 +1579,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1693,7 +1727,7 @@ dependencies = [ "byteorder", "hex", "lazy_static", - "rustix", + "rustix 0.35.13", ] [[package]] @@ -1899,10 +1933,24 @@ checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9" dependencies = [ "bitflags", "errno", - "io-lifetimes", + "io-lifetimes 0.7.5", "libc", - "linux-raw-sys", - "windows-sys", + "linux-raw-sys 0.0.46", + "windows-sys 0.42.0", +] + +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes 1.0.5", + "libc", + "linux-raw-sys 0.1.4", + "windows-sys 0.45.0", ] [[package]] @@ -2161,8 +2209,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ca90c434fd12083d1a6bdcbe9f92a14f96c8a1ba600ba451734ac334521f7a" dependencies = [ - "rustix", - "windows-sys", + "rustix 0.35.13", + "windows-sys 0.42.0", ] [[package]] @@ -2334,8 +2382,8 @@ dependencies = [ name = "uu_cat" version = "0.0.17" dependencies = [ - "atty", "clap", + "is-terminal", "nix", "thiserror", "uucore", @@ -2432,9 +2480,9 @@ dependencies = [ name = "uu_cut" version = "0.0.17" dependencies = [ - "atty", "bstr", "clap", + "is-terminal", "memchr", "uucore", ] @@ -2447,7 +2495,7 @@ dependencies = [ "clap", "libc", "uucore", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -2503,7 +2551,7 @@ dependencies = [ "clap", "glob", "uucore", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -2634,7 +2682,7 @@ dependencies = [ "clap", "hostname", "uucore", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -2705,10 +2753,10 @@ dependencies = [ name = "uu_ls" version = "0.0.17" dependencies = [ - "atty", "chrono", "clap", "glob", + "is-terminal", "lscolors", "number_prefix", "once_cell", @@ -2759,9 +2807,9 @@ dependencies = [ name = "uu_more" version = "0.0.17" dependencies = [ - "atty", "clap", "crossterm", + "is-terminal", "nix", "unicode-segmentation", "unicode-width", @@ -2801,8 +2849,8 @@ dependencies = [ name = "uu_nohup" version = "0.0.17" dependencies = [ - "atty", "clap", + "is-terminal", "libc", "uucore", ] @@ -2936,7 +2984,7 @@ dependencies = [ "libc", "uucore", "walkdir", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -3079,7 +3127,7 @@ dependencies = [ "libc", "nix", "uucore", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -3097,16 +3145,16 @@ dependencies = [ name = "uu_tail" version = "0.0.17" dependencies = [ - "atty", "clap", "fundu", + "is-terminal", "libc", "memchr", "notify", "same-file", "uucore", "winapi-util", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -3146,7 +3194,7 @@ dependencies = [ "filetime", "time", "uucore", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -3186,8 +3234,8 @@ dependencies = [ name = "uu_tty" version = "0.0.17" dependencies = [ - "atty", "clap", + "is-terminal", "nix", "uucore", ] @@ -3282,7 +3330,7 @@ dependencies = [ "clap", "libc", "uucore", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -3316,7 +3364,7 @@ dependencies = [ "walkdir", "wild", "winapi-util", - "windows-sys", + "windows-sys 0.42.0", "z85", ] @@ -3478,46 +3526,70 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" +name = "windows-sys" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "xattr" diff --git a/Cargo.toml b/Cargo.toml index ebd8685ca..61d6db205 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -263,7 +263,6 @@ feat_os_windows_legacy = [ test = [ "uu_test" ] [workspace.dependencies] -atty = "0.2" bigdecimal = "0.3" binary-heap-plus = "0.5.0" bstr = "1.0" @@ -287,6 +286,7 @@ gcd = "2.2" glob = "0.3.0" half = "2.1" indicatif = "0.17" +is-terminal = "0.4.3" itertools = "0.10.0" libc = "0.2.139" lscolors = { version = "0.13.0", default-features=false, features = ["nu-ansi-term"] } @@ -476,7 +476,7 @@ time = { workspace=true, features=["local-offset"] } unindent = "0.1" uucore = { workspace=true, features=["entries", "process", "signals"] } walkdir = { workspace=true } -atty = { workspace=true } +is-terminal = { workspace=true } hex-literal = "0.3.1" rstest = "0.16.0" diff --git a/deny.toml b/deny.toml index 20995c936..aa05cdf3e 100644 --- a/deny.toml +++ b/deny.toml @@ -58,12 +58,24 @@ highlight = "all" # For each duplicate dependency, indicate the name of the dependency which # introduces it. # spell-checker: disable -skip = [] +skip = [ + # is-terminal + { name = "hermit-abi", version = "0.3.1" }, + # is-terminal + { name = "rustix", version = "0.36.8" }, + # is-terminal (via rustix) + { name = "io-lifetimes", version = "1.0.5" }, + # is-terminal + { name = "linux-raw-sys", version = "0.1.4" }, + # is-terminal + { name = "windows-sys", version = "0.45.0" }, +] # spell-checker: enable # This section is considered when running `cargo deny check sources`. # More documentation about the 'sources' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html + [sources] unknown-registry = "warn" unknown-git = "warn" diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index e707c6db0..fbf91c249 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -17,7 +17,7 @@ path = "src/cat.rs" [dependencies] clap = { workspace=true } thiserror = { workspace = true } -atty = { workspace=true } +is-terminal = { workspace = true } uucore = { workspace=true, features=["fs", "pipes"] } [target.'cfg(unix)'.dependencies] diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 85a626d5b..e90bdd116 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -12,6 +12,7 @@ // last synced with: cat (GNU coreutils) 8.13 use clap::{crate_version, Arg, ArgAction, Command}; +use is_terminal::IsTerminal; use std::fs::{metadata, File}; use std::io::{self, Read, Write}; use thiserror::Error; @@ -332,7 +333,7 @@ fn cat_path( let stdin = io::stdin(); let mut handle = InputHandle { reader: stdin, - is_interactive: atty::is(atty::Stream::Stdin), + is_interactive: std::io::stdin().is_terminal(), }; cat_handle(&mut handle, options, state) } diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index f680b67e5..8a5e8f1d2 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -19,7 +19,7 @@ clap = { workspace=true } uucore = { workspace=true } memchr = { workspace=true } bstr = { workspace=true } -atty = { workspace=true } +is-terminal = { workspace=true } [[bin]] name = "cut" diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 9db114fb2..105449cbc 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -9,6 +9,7 @@ use bstr::io::BufReadExt; use clap::{crate_version, Arg, ArgAction, Command}; +use is_terminal::IsTerminal; use std::fs::File; use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write}; use std::path::Path; @@ -136,7 +137,7 @@ enum Mode { } fn stdout_writer() -> Box { - if atty::is(atty::Stream::Stdout) { + if std::io::stdout().is_terminal() { Box::new(stdout()) } else { Box::new(BufWriter::new(stdout())) as Box diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index ade9c5e29..7c7add294 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -25,7 +25,7 @@ glob = { workspace=true } lscolors = { workspace=true } uucore = { workspace=true, features = ["entries", "fs"] } once_cell = { workspace=true } -atty = { workspace=true } +is-terminal = { workspace=true } selinux = { workspace=true, optional = true } [[bin]] diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 965248495..abf05676f 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -12,6 +12,7 @@ use clap::{ crate_version, Arg, ArgAction, Command, }; use glob::{MatchOptions, Pattern}; +use is_terminal::IsTerminal; use lscolors::LsColors; use number_prefix::NumberPrefix; use once_cell::unsync::OnceCell; @@ -451,7 +452,7 @@ impl Config { (Format::Commas, Some(options::format::COMMAS)) } else if options.get_flag(options::format::COLUMNS) { (Format::Columns, Some(options::format::COLUMNS)) - } else if atty::is(atty::Stream::Stdout) { + } else if std::io::stdout().is_terminal() { (Format::Columns, None) } else { (Format::OneLine, None) @@ -557,7 +558,7 @@ impl Config { None => options.contains_id(options::COLOR), Some(val) => match val.as_str() { "" | "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => atty::is(atty::Stream::Stdout), + "auto" | "tty" | "if-tty" => std::io::stdout().is_terminal(), /* "never" | "no" | "none" | */ _ => false, }, }; @@ -678,7 +679,7 @@ impl Config { } else if options.get_flag(options::SHOW_CONTROL_CHARS) { true } else { - !atty::is(atty::Stream::Stdout) + !std::io::stdout().is_terminal() }; let opt_quoting_style = options @@ -750,7 +751,7 @@ impl Config { "never" | "no" | "none" => IndicatorStyle::None, "always" | "yes" | "force" => IndicatorStyle::Classify, "auto" | "tty" | "if-tty" => { - if atty::is(atty::Stream::Stdout) { + if std::io::stdout().is_terminal() { IndicatorStyle::Classify } else { IndicatorStyle::None diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index 416a2359b..01bb96183 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -18,7 +18,7 @@ path = "src/more.rs" clap = { workspace=true } uucore = { workspace=true } crossterm = { workspace=true } -atty = { workspace=true } +is-terminal = { workspace=true } unicode-width = { workspace=true } unicode-segmentation = { workspace=true } diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 0309ad33e..31b1640a2 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -23,6 +23,7 @@ use crossterm::{ terminal, }; +use is_terminal::IsTerminal; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use uucore::display::Quotable; @@ -83,7 +84,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { buff.clear(); } reset_term(&mut stdout); - } else if atty::isnt(atty::Stream::Stdin) { + } else if !std::io::stdin().is_terminal() { stdin().read_to_string(&mut buff).unwrap(); let mut stdout = setup_term(); more(&buff, &mut stdout, None, silent)?; diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 9a6b4af43..f35869506 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -17,7 +17,7 @@ path = "src/nohup.rs" [dependencies] clap = { workspace=true } libc = { workspace=true } -atty = { workspace=true } +is-terminal = { workspace=true } uucore = { workspace=true, features=["fs"] } [[bin]] diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 789888ea4..d00ef4931 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -8,6 +8,7 @@ // spell-checker:ignore (ToDO) execvp SIGHUP cproc vprocmgr cstrs homeout use clap::{crate_version, Arg, ArgAction, Command}; +use is_terminal::IsTerminal; use libc::{c_char, dup2, execvp, signal}; use libc::{SIGHUP, SIG_IGN}; use std::env; @@ -129,7 +130,7 @@ pub fn uu_app() -> Command { } fn replace_fds() -> UResult<()> { - if atty::is(atty::Stream::Stdin) { + if std::io::stdin().is_terminal() { let new_stdin = File::open(Path::new("/dev/null")) .map_err(|e| NohupError::CannotReplace("STDIN", e))?; if unsafe { dup2(new_stdin.as_raw_fd(), 0) } != 0 { @@ -137,7 +138,7 @@ fn replace_fds() -> UResult<()> { } } - if atty::is(atty::Stream::Stdout) { + if std::io::stdout().is_terminal() { let new_stdout = find_stdout()?; let fd = new_stdout.as_raw_fd(); @@ -146,7 +147,7 @@ fn replace_fds() -> UResult<()> { } } - if atty::is(atty::Stream::Stderr) && unsafe { dup2(1, 2) } != 2 { + if std::io::stderr().is_terminal() && unsafe { dup2(1, 2) } != 2 { return Err(NohupError::CannotReplace("STDERR", Error::last_os_error()).into()); } Ok(()) diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index bd16132ea..88b82a8e6 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -22,7 +22,7 @@ memchr = { workspace=true } notify = { workspace=true } uucore = { workspace=true } same-file = { workspace=true } -atty = { workspace=true } +is-terminal = { workspace=true } fundu = { workspace=true } [target.'cfg(windows)'.dependencies] diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index bfc314185..0807aa912 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -7,10 +7,10 @@ use crate::paths::Input; use crate::{parse, platform, Quotable}; -use atty::Stream; use clap::crate_version; use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; use fundu::DurationParser; +use is_terminal::IsTerminal; use same_file::Handle; use std::collections::VecDeque; use std::ffi::OsString; @@ -274,7 +274,7 @@ impl Settings { .map_or(false, |meta| !meta.is_file()) }); - if !blocking_stdin && atty::is(Stream::Stdin) { + if !blocking_stdin && std::io::stdin().is_terminal() { show_warning!("following standard input indefinitely is ineffective"); } } diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index 34eb84570..ee7fe559b 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -17,7 +17,7 @@ path = "src/tty.rs" [dependencies] clap = { workspace=true } nix = { workspace=true, features=["term"] } -atty = { workspace=true } +is-terminal = { workspace=true } uucore = { workspace=true, features=["fs"] } [[bin]] diff --git a/src/uu/tty/src/tty.rs b/src/uu/tty/src/tty.rs index ad41c323c..e3fc0451e 100644 --- a/src/uu/tty/src/tty.rs +++ b/src/uu/tty/src/tty.rs @@ -10,6 +10,7 @@ // spell-checker:ignore (ToDO) ttyname filedesc use clap::{crate_version, Arg, ArgAction, Command}; +use is_terminal::IsTerminal; use std::io::Write; use std::os::unix::io::AsRawFd; use uucore::error::{set_exit_code, UResult}; @@ -30,7 +31,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // If silent, we don't need the name, only whether or not stdin is a tty. if silent { - return if atty::is(atty::Stream::Stdin) { + return if std::io::stdin().is_terminal() { Ok(()) } else { Err(1.into()) diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 5c2b3c0f6..b3a1522f6 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -1,9 +1,10 @@ use crate::common::util::*; +use is_terminal::IsTerminal; #[test] fn test_more_no_arg() { // Reading from stdin is now supported, so this must succeed - if atty::is(atty::Stream::Stdout) { + if std::io::stdout().is_terminal() { new_ucmd!().succeeds(); } else { } @@ -14,7 +15,7 @@ fn test_more_dir_arg() { // Run the test only if there's a valid terminal, else do nothing // Maybe we could capture the error, i.e. "Device not found" in that case // but I am leaving this for later - if atty::is(atty::Stream::Stdout) { + if std::io::stdout().is_terminal() { new_ucmd!() .arg(".") .fails()