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()); + } }