// This file is part of the uutils util-linux package. // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. //spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized #![allow(dead_code)] use pretty_assertions::assert_eq; #[cfg(any(target_os = "linux", target_os = "android"))] use rlimit::prlimit; #[cfg(unix)] use std::borrow::Cow; use std::collections::VecDeque; #[cfg(not(windows))] use std::ffi::CString; use std::ffi::{OsStr, OsString}; use std::fs::{self, hard_link, remove_file, File, OpenOptions}; use std::io::{self, BufWriter, Read, Result, Write}; #[cfg(unix)] use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file, PermissionsExt}; #[cfg(unix)] use std::os::unix::process::ExitStatusExt; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; #[cfg(windows)] use std::path::MAIN_SEPARATOR; use std::path::{Path, PathBuf}; use std::process::{Child, Command, ExitStatus, Output, Stdio}; use std::rc::Rc; use std::sync::mpsc::{self, RecvTimeoutError}; use std::thread::{sleep, JoinHandle}; use std::time::{Duration, Instant}; use std::{env, hint, thread}; use tempfile::{Builder, TempDir}; static TESTS_DIR: &str = "tests"; static FIXTURES_DIR: &str = "fixtures"; static ALREADY_RUN: &str = " you have already run this UCommand, if you want to run \ another command in the same test, use TestScenario::new instead of \ testing();"; static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly."; static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin"; pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_bsdutils"); pub const PATH: &str = env!("PATH"); /// Default environment variables to run the commands with const DEFAULT_ENV: [(&str, &str); 2] = [("LC_ALL", "C"), ("TZ", "UTC")]; /// Test if the program is running under CI pub fn is_ci() -> bool { std::env::var("CI").is_ok_and(|s| s.eq_ignore_ascii_case("true")) } /// Read a test scenario fixture, returning its bytes fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> Vec { let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); AtPath::new(tmpdir_path).read_bytes(file_rel_path.as_ref().to_str().unwrap()) } /// A command result is the outputs of a command (streams and status code) /// within a struct which has convenience assertion functions about those outputs #[derive(Debug, Clone)] pub struct CmdResult { /// bin_path provided by `TestScenario` or `UCommand` bin_path: PathBuf, /// util_name provided by `TestScenario` or `UCommand` util_name: Option, //tmpd is used for convenience functions for asserts against fixtures tmpd: Option>, /// exit status for command (if there is one) exit_status: Option, /// captured standard output after running the Command stdout: Vec, /// captured standard error after running the Command stderr: Vec, } impl CmdResult { pub fn new( bin_path: S, util_name: Option, tmpd: Option>, exit_status: Option, stdout: U, stderr: V, ) -> Self where S: Into, T: AsRef, U: Into>, V: Into>, { Self { bin_path: bin_path.into(), util_name: util_name.map(|s| s.as_ref().into()), tmpd, exit_status, stdout: stdout.into(), stderr: stderr.into(), } } /// Apply a function to `stdout` as bytes and return a new [`CmdResult`] pub fn stdout_apply<'a, F, R>(&'a self, function: F) -> Self where F: Fn(&'a [u8]) -> R, R: Into>, { Self::new( self.bin_path.clone(), self.util_name.clone(), self.tmpd.clone(), self.exit_status, function(&self.stdout), self.stderr.as_slice(), ) } /// Apply a function to `stdout` as `&str` and return a new [`CmdResult`] pub fn stdout_str_apply<'a, F, R>(&'a self, function: F) -> Self where F: Fn(&'a str) -> R, R: Into>, { Self::new( self.bin_path.clone(), self.util_name.clone(), self.tmpd.clone(), self.exit_status, function(self.stdout_str()), self.stderr.as_slice(), ) } /// Apply a function to `stderr` as bytes and return a new [`CmdResult`] pub fn stderr_apply<'a, F, R>(&'a self, function: F) -> Self where F: Fn(&'a [u8]) -> R, R: Into>, { Self::new( self.bin_path.clone(), self.util_name.clone(), self.tmpd.clone(), self.exit_status, self.stdout.as_slice(), function(&self.stderr), ) } /// Apply a function to `stderr` as `&str` and return a new [`CmdResult`] pub fn stderr_str_apply<'a, F, R>(&'a self, function: F) -> Self where F: Fn(&'a str) -> R, R: Into>, { Self::new( self.bin_path.clone(), self.util_name.clone(), self.tmpd.clone(), self.exit_status, self.stdout.as_slice(), function(self.stderr_str()), ) } /// Assert `stdout` as bytes with a predicate function returning a `bool`. #[track_caller] pub fn stdout_check<'a, F>(&'a self, predicate: F) -> &Self where F: Fn(&'a [u8]) -> bool, { assert!( predicate(&self.stdout), "Predicate for stdout as `bytes` evaluated to false.\nstdout='{:?}'\nstderr='{:?}'\n", &self.stdout, &self.stderr ); self } /// Assert `stdout` as `&str` with a predicate function returning a `bool`. #[track_caller] pub fn stdout_str_check<'a, F>(&'a self, predicate: F) -> &Self where F: Fn(&'a str) -> bool, { assert!( predicate(self.stdout_str()), "Predicate for stdout as `str` evaluated to false.\nstdout='{}'\nstderr='{}'\n", self.stdout_str(), self.stderr_str() ); self } /// Assert `stderr` as bytes with a predicate function returning a `bool`. #[track_caller] pub fn stderr_check<'a, F>(&'a self, predicate: F) -> &Self where F: Fn(&'a [u8]) -> bool, { assert!( predicate(&self.stderr), "Predicate for stderr as `bytes` evaluated to false.\nstdout='{:?}'\nstderr='{:?}'\n", &self.stdout, &self.stderr ); self } /// Assert `stderr` as `&str` with a predicate function returning a `bool`. #[track_caller] pub fn stderr_str_check<'a, F>(&'a self, predicate: F) -> &Self where F: Fn(&'a str) -> bool, { assert!( predicate(self.stderr_str()), "Predicate for stderr as `str` evaluated to false.\nstdout='{}'\nstderr='{}'\n", self.stdout_str(), self.stderr_str() ); self } /// Return the exit status of the child process, if any. /// /// Returns None if the child process is still running or hasn't been started. pub fn try_exit_status(&self) -> Option { self.exit_status } /// Return the exit status of the child process. /// /// # Panics /// /// If the child process is still running or hasn't been started. pub fn exit_status(&self) -> ExitStatus { self.try_exit_status() .expect("Program must be run first or has not finished, yet") } /// Return the signal the child process received if any. /// /// # Platform specific behavior /// /// This method is only available on unix systems. #[cfg(unix)] pub fn signal(&self) -> Option { self.exit_status().signal() } /// Assert that the given signal `value` equals the signal the child process received. /// /// See also [`std::os::unix::process::ExitStatusExt::signal`]. /// /// # Platform specific behavior /// /// This assertion method is only available on unix systems. #[cfg(unix)] #[track_caller] pub fn signal_is(&self, value: i32) -> &Self { let actual = self.signal().unwrap_or_else(|| { panic!( "Expected process to be terminated by the '{}' signal, but exit status is: '{}'", value, self.try_exit_status() .map_or("Not available".to_string(), |e| e.to_string()) ) }); assert_eq!(actual, value); self } /// Assert that the given signal `name` equals the signal the child process received. /// /// Strings like `SIGINT`, `INT` or a number like `15` are all valid names. See also /// [`std::os::unix::process::ExitStatusExt::signal`] and /// [`uucore::signals::signal_by_name_or_value`] /// /// # Platform specific behavior /// /// This assertion method is only available on unix systems. #[cfg(unix)] #[track_caller] pub fn signal_name_is(&self, name: &str) -> &Self { use uucore::signals::signal_by_name_or_value; let expected: i32 = signal_by_name_or_value(name) .unwrap_or_else(|| panic!("Invalid signal name or value: '{name}'")) .try_into() .unwrap(); let actual = self.signal().unwrap_or_else(|| { panic!( "Expected process to be terminated by the '{}' signal, but exit status is: '{}'", name, self.try_exit_status() .map_or("Not available".to_string(), |e| e.to_string()) ) }); assert_eq!(actual, expected); self } /// Returns a reference to the program's standard output as a slice of bytes pub fn stdout(&self) -> &[u8] { &self.stdout } /// Returns the program's standard output as a string slice pub fn stdout_str(&self) -> &str { std::str::from_utf8(&self.stdout).unwrap() } /// Returns the program's standard output as a string /// consumes self pub fn stdout_move_str(self) -> String { String::from_utf8(self.stdout).unwrap() } /// Returns the program's standard output as a vec of bytes /// consumes self pub fn stdout_move_bytes(self) -> Vec { self.stdout } /// Returns a reference to the program's standard error as a slice of bytes pub fn stderr(&self) -> &[u8] { &self.stderr } /// Returns the program's standard error as a string slice pub fn stderr_str(&self) -> &str { std::str::from_utf8(&self.stderr).unwrap() } /// Returns the program's standard error as a string /// consumes self pub fn stderr_move_str(self) -> String { String::from_utf8(self.stderr).unwrap() } /// Returns the program's standard error as a vec of bytes /// consumes self pub fn stderr_move_bytes(self) -> Vec { self.stderr } /// Returns the program's exit code /// Panics if not run or has not finished yet for example when run with `run_no_wait()` pub fn code(&self) -> i32 { self.exit_status().code().unwrap() } #[track_caller] pub fn code_is(&self, expected_code: i32) -> &Self { assert_eq!(self.code(), expected_code); self } /// Returns the program's `TempDir` /// Panics if not present pub fn tmpd(&self) -> Rc { match &self.tmpd { Some(ptr) => ptr.clone(), None => panic!("Command not associated with a TempDir"), } } /// Returns whether the program succeeded pub fn succeeded(&self) -> bool { self.exit_status.map_or(true, |e| e.success()) } /// asserts that the command resulted in a success (zero) status code #[track_caller] pub fn success(&self) -> &Self { assert!( self.succeeded(), "Command was expected to succeed.\nstdout = {}\n stderr = {}", self.stdout_str(), self.stderr_str() ); self } /// asserts that the command resulted in a failure (non-zero) status code #[track_caller] pub fn failure(&self) -> &Self { assert!( !self.succeeded(), "Command was expected to fail.\nstdout = {}\n stderr = {}", self.stdout_str(), self.stderr_str() ); self } /// asserts that the command resulted in empty (zero-length) stderr stream output /// generally, it's better to use `stdout_only()` instead, /// but you might find yourself using this function if /// 1. you can not know exactly what stdout will be or /// 2. you know that stdout will also be empty #[track_caller] pub fn no_stderr(&self) -> &Self { assert!( self.stderr.is_empty(), "Expected stderr to be empty, but it's:\n{}", self.stderr_str() ); self } /// asserts that the command resulted in empty (zero-length) stderr stream output /// unless asserting there was neither stdout or stderr, `stderr_only` is usually a better choice /// generally, it's better to use `stderr_only()` instead, /// but you might find yourself using this function if /// 1. you can not know exactly what stderr will be or /// 2. you know that stderr will also be empty #[track_caller] pub fn no_stdout(&self) -> &Self { assert!( self.stdout.is_empty(), "Expected stdout to be empty, but it's:\n{}", self.stdout_str() ); self } /// Assert that there is output to neither stderr nor stdout. #[track_caller] pub fn no_output(&self) -> &Self { self.no_stdout().no_stderr() } /// asserts that the command resulted in stdout stream output that equals the /// passed in value, trailing whitespace are kept to force strict comparison (#1235) /// `stdout_only()` is a better choice unless stderr may or will be non-empty #[track_caller] pub fn stdout_is>(&self, msg: T) -> &Self { assert_eq!(self.stdout_str(), String::from(msg.as_ref())); self } /// like `stdout_is`, but succeeds if any elements of `expected` matches stdout. #[track_caller] pub fn stdout_is_any + std::fmt::Debug>(&self, expected: &[T]) -> &Self { assert!( expected.iter().any(|msg| self.stdout_str() == msg.as_ref()), "stdout was {}\nExpected any of {:#?}", self.stdout_str(), expected ); self } /// Like `stdout_is` but newlines are normalized to `\n`. #[track_caller] pub fn normalized_newlines_stdout_is>(&self, msg: T) -> &Self { let msg = msg.as_ref().replace("\r\n", "\n"); assert_eq!(self.stdout_str().replace("\r\n", "\n"), msg); self } /// asserts that the command resulted in stdout stream output, /// whose bytes equal those of the passed in slice #[track_caller] pub fn stdout_is_bytes>(&self, msg: T) -> &Self { assert_eq!(self.stdout, msg.as_ref(), "stdout as bytes wasn't equal to expected bytes. Result as strings:\nstdout ='{:?}'\nexpected='{:?}'", std::str::from_utf8(&self.stdout), std::str::from_utf8(msg.as_ref()), ); self } /// like `stdout_is()`, but expects the contents of the file at the provided relative path #[track_caller] pub fn stdout_is_fixture>(&self, file_rel_path: T) -> &Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stdout_is(String::from_utf8(contents).unwrap()) } /// Assert that the bytes of stdout exactly match those of the given file. /// /// Contrast this with [`CmdResult::stdout_is_fixture`], which /// decodes the contents of the file as a UTF-8 [`String`] before /// comparison with stdout. /// /// # Examples /// /// Use this method in a unit test like this: /// /// ```rust,ignore /// #[test] /// fn test_something() { /// new_ucmd!().succeeds().stdout_is_fixture_bytes("expected.bin"); /// } /// ``` #[track_caller] pub fn stdout_is_fixture_bytes>(&self, file_rel_path: T) -> &Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stdout_is_bytes(contents) } /// like `stdout_is_fixture()`, but replaces the data in fixture file based on values provided in `template_vars` /// command output #[track_caller] pub fn stdout_is_templated_fixture>( &self, file_rel_path: T, template_vars: &[(&str, &str)], ) -> &Self { let mut contents = String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); for kv in template_vars { contents = contents.replace(kv.0, kv.1); } self.stdout_is(contents) } /// like `stdout_is_templated_fixture`, but succeeds if any replacement by `template_vars` results in the actual stdout. #[track_caller] pub fn stdout_is_templated_fixture_any>( &self, file_rel_path: T, template_vars: &[Vec<(String, String)>], ) { let contents = String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); let possible_values = template_vars.iter().map(|vars| { let mut contents = contents.clone(); for kv in vars { contents = contents.replace(&kv.0, &kv.1); } contents }); self.stdout_is_any(&possible_values.collect::>()); } /// assert that the command resulted in stderr stream output that equals the /// passed in value. /// /// `stderr_only` is a better choice unless stdout may or will be non-empty #[track_caller] pub fn stderr_is>(&self, msg: T) -> &Self { assert_eq!(self.stderr_str(), msg.as_ref()); self } /// asserts that the command resulted in stderr stream output, /// whose bytes equal those of the passed in slice #[track_caller] pub fn stderr_is_bytes>(&self, msg: T) -> &Self { assert_eq!( &self.stderr, msg.as_ref(), "stderr as bytes wasn't equal to expected bytes. Result as strings:\nstderr ='{:?}'\nexpected='{:?}'", std::str::from_utf8(&self.stderr), std::str::from_utf8(msg.as_ref()) ); self } /// Like `stdout_is_fixture`, but for stderr #[track_caller] pub fn stderr_is_fixture>(&self, file_rel_path: T) -> &Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stderr_is(String::from_utf8(contents).unwrap()) } /// asserts that /// 1. the command resulted in stdout stream output that equals the /// passed in value /// 2. the command resulted in empty (zero-length) stderr stream output #[track_caller] pub fn stdout_only>(&self, msg: T) -> &Self { self.no_stderr().stdout_is(msg) } /// asserts that /// 1. the command resulted in a stdout stream whose bytes /// equal those of the passed in value /// 2. the command resulted in an empty stderr stream #[track_caller] pub fn stdout_only_bytes>(&self, msg: T) -> &Self { self.no_stderr().stdout_is_bytes(msg) } /// like `stdout_only()`, but expects the contents of the file at the provided relative path #[track_caller] pub fn stdout_only_fixture>(&self, file_rel_path: T) -> &Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stdout_only_bytes(contents) } /// asserts that /// 1. the command resulted in stderr stream output that equals the /// passed in value /// 2. the command resulted in empty (zero-length) stdout stream output #[track_caller] pub fn stderr_only>(&self, msg: T) -> &Self { self.no_stdout().stderr_is(msg) } /// asserts that /// 1. the command resulted in a stderr stream whose bytes equal the ones /// of the passed value /// 2. the command resulted in an empty stdout stream #[track_caller] pub fn stderr_only_bytes>(&self, msg: T) -> &Self { self.no_stdout().stderr_is_bytes(msg) } #[track_caller] pub fn fails_silently(&self) -> &Self { assert!(!self.succeeded()); assert!(self.stderr.is_empty()); self } /// asserts that /// 1. the command resulted in stderr stream output that equals the /// the following format /// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."` /// This the expected format when a `UUsageError` is returned or when `show_error!` is called /// `msg` should be the same as the one provided to `UUsageError::new` or `show_error!` /// /// 2. the command resulted in empty (zero-length) stdout stream output #[track_caller] pub fn usage_error>(&self, msg: T) -> &Self { self.stderr_only(format!( "{0}: {2}\nTry '{1} {0} --help' for more information.\n", self.util_name.as_ref().unwrap(), // This shouldn't be called using a normal command self.bin_path.display(), msg.as_ref() )) } #[track_caller] pub fn stdout_contains>(&self, cmp: T) -> &Self { assert!( self.stdout_str().contains(cmp.as_ref()), "'{}' does not contain '{}'", self.stdout_str(), cmp.as_ref() ); self } #[track_caller] pub fn stdout_contains_line>(&self, cmp: T) -> &Self { assert!( self.stdout_str().lines().any(|line| line == cmp.as_ref()), "'{}' does not contain line '{}'", self.stdout_str(), cmp.as_ref() ); self } #[track_caller] pub fn stderr_contains>(&self, cmp: T) -> &Self { assert!( self.stderr_str().contains(cmp.as_ref()), "'{}' does not contain '{}'", self.stderr_str(), cmp.as_ref() ); self } #[track_caller] pub fn stdout_does_not_contain>(&self, cmp: T) -> &Self { assert!( !self.stdout_str().contains(cmp.as_ref()), "'{}' contains '{}' but should not", self.stdout_str(), cmp.as_ref(), ); self } #[track_caller] pub fn stderr_does_not_contain>(&self, cmp: T) -> &Self { assert!(!self.stderr_str().contains(cmp.as_ref())); self } #[track_caller] pub fn stdout_matches(&self, regex: ®ex::Regex) -> &Self { assert!( regex.is_match(self.stdout_str()), "Stdout does not match regex:\n{}", self.stdout_str() ); self } #[track_caller] pub fn stderr_matches(&self, regex: ®ex::Regex) -> &Self { assert!( regex.is_match(self.stderr_str()), "Stderr does not match regex:\n{}", self.stderr_str() ); self } #[track_caller] pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &Self { assert!( !regex.is_match(self.stdout_str()), "Stdout matches regex:\n{}", self.stdout_str() ); self } } pub fn log_info, U: AsRef>(msg: T, par: U) { println!("{}: {}", msg.as_ref(), par.as_ref()); } pub fn recursive_copy(src: &Path, dest: &Path) -> Result<()> { if fs::metadata(src)?.is_dir() { for entry in fs::read_dir(src)? { let entry = entry?; let mut new_dest = PathBuf::from(dest); new_dest.push(entry.file_name()); if fs::metadata(entry.path())?.is_dir() { fs::create_dir(&new_dest)?; recursive_copy(&entry.path(), &new_dest)?; } else { fs::copy(entry.path(), new_dest)?; } } } Ok(()) } pub fn get_root_path() -> &'static str { if cfg!(windows) { "C:\\" } else { "/" } } /// Compares the extended attributes (xattrs) of two files or directories. /// /// # Returns /// /// `true` if both paths have the same set of extended attributes, `false` otherwise. #[cfg(all(unix, not(target_os = "macos")))] pub fn compare_xattrs>(path1: P, path2: P) -> bool { let get_sorted_xattrs = |path: P| { xattr::list(path) .map(|attrs| { let mut attrs = attrs.collect::>(); attrs.sort(); attrs }) .unwrap_or_else(|_| Vec::new()) }; get_sorted_xattrs(path1) == get_sorted_xattrs(path2) } /// Object-oriented path struct that represents and operates on /// paths relative to the directory it was constructed for. #[derive(Clone)] pub struct AtPath { pub subdir: PathBuf, } impl AtPath { pub fn new(subdir: &Path) -> Self { Self { subdir: PathBuf::from(subdir), } } pub fn as_string(&self) -> String { self.subdir.to_str().unwrap().to_owned() } pub fn plus>(&self, name: P) -> PathBuf { let mut pathbuf = self.subdir.clone(); pathbuf.push(name); pathbuf } pub fn plus_as_string>(&self, name: P) -> String { self.plus(name).display().to_string() } fn minus(&self, name: &str) -> PathBuf { let prefixed = PathBuf::from(name); if prefixed.starts_with(&self.subdir) { let mut unprefixed = PathBuf::new(); for component in prefixed.components().skip(self.subdir.components().count()) { unprefixed.push(component.as_os_str().to_str().unwrap()); } unprefixed } else { prefixed } } pub fn minus_as_string(&self, name: &str) -> String { String::from(self.minus(name).to_str().unwrap()) } pub fn set_readonly(&self, name: &str) { let metadata = fs::metadata(self.plus(name)).unwrap(); let mut permissions = metadata.permissions(); permissions.set_readonly(true); fs::set_permissions(self.plus(name), permissions).unwrap(); } pub fn open(&self, name: &str) -> File { log_info("open", self.plus_as_string(name)); File::open(self.plus(name)).unwrap() } pub fn read(&self, name: &str) -> String { let mut f = self.open(name); let mut contents = String::new(); f.read_to_string(&mut contents) .unwrap_or_else(|e| panic!("Couldn't read {name}: {e}")); contents } pub fn read_bytes(&self, name: &str) -> Vec { let mut f = self.open(name); let mut contents = Vec::new(); f.read_to_end(&mut contents) .unwrap_or_else(|e| panic!("Couldn't read {name}: {e}")); contents } pub fn write(&self, name: &str, contents: &str) { log_info("write(default)", self.plus_as_string(name)); std::fs::write(self.plus(name), contents) .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); } pub fn write_bytes(&self, name: &str, contents: &[u8]) { log_info("write(default)", self.plus_as_string(name)); std::fs::write(self.plus(name), contents) .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); } pub fn append(&self, name: &str, contents: &str) { log_info("write(append)", self.plus_as_string(name)); let mut f = OpenOptions::new() .write(true) .append(true) .create(true) .open(self.plus(name)) .unwrap(); f.write_all(contents.as_bytes()) .unwrap_or_else(|e| panic!("Couldn't write(append) {name}: {e}")); } pub fn append_bytes(&self, name: &str, contents: &[u8]) { log_info("write(append)", self.plus_as_string(name)); let mut f = OpenOptions::new() .write(true) .append(true) .create(true) .open(self.plus(name)) .unwrap(); f.write_all(contents) .unwrap_or_else(|e| panic!("Couldn't write(append) to {name}: {e}")); } pub fn truncate(&self, name: &str, contents: &str) { log_info("write(truncate)", self.plus_as_string(name)); let mut f = OpenOptions::new() .write(true) .truncate(true) .create(true) .open(self.plus(name)) .unwrap(); f.write_all(contents.as_bytes()) .unwrap_or_else(|e| panic!("Couldn't write(truncate) {name}: {e}")); } pub fn rename(&self, source: &str, target: &str) { let source = self.plus(source); let target = self.plus(target); log_info("rename", format!("{source:?} {target:?}")); std::fs::rename(&source, &target) .unwrap_or_else(|e| panic!("Couldn't rename {source:?} -> {target:?}: {e}")); } pub fn remove(&self, source: &str) { let source = self.plus(source); log_info("remove", format!("{source:?}")); std::fs::remove_file(&source).unwrap_or_else(|e| panic!("Couldn't remove {source:?}: {e}")); } pub fn copy(&self, source: &str, target: &str) { let source = self.plus(source); let target = self.plus(target); log_info("copy", format!("{source:?} {target:?}")); std::fs::copy(&source, &target) .unwrap_or_else(|e| panic!("Couldn't copy {source:?} -> {target:?}: {e}")); } pub fn rmdir(&self, dir: &str) { log_info("rmdir", self.plus_as_string(dir)); fs::remove_dir(self.plus(dir)).unwrap(); } pub fn mkdir>(&self, dir: P) { let dir = dir.as_ref(); log_info("mkdir", self.plus_as_string(dir)); fs::create_dir(self.plus(dir)).unwrap(); } pub fn mkdir_all(&self, dir: &str) { log_info("mkdir_all", self.plus_as_string(dir)); fs::create_dir_all(self.plus(dir)).unwrap(); } pub fn make_file(&self, name: &str) -> File { match File::create(self.plus(name)) { Ok(f) => f, Err(e) => panic!("{}", e), } } pub fn touch>(&self, file: P) { let file = file.as_ref(); log_info("touch", self.plus_as_string(file)); File::create(self.plus(file)).unwrap(); } #[cfg(not(windows))] pub fn mkfifo(&self, fifo: &str) { let full_path = self.plus_as_string(fifo); log_info("mkfifo", &full_path); unsafe { let fifo_name: CString = CString::new(full_path).expect("CString creation failed."); libc::mkfifo(fifo_name.as_ptr(), libc::S_IWUSR | libc::S_IRUSR); } } #[cfg(not(windows))] pub fn is_fifo(&self, fifo: &str) -> bool { unsafe { let name = CString::new(self.plus_as_string(fifo)).unwrap(); let mut stat: libc::stat = std::mem::zeroed(); if libc::stat(name.as_ptr(), &mut stat) >= 0 { libc::S_IFIFO & stat.st_mode as libc::mode_t != 0 } else { false } } } pub fn hard_link(&self, original: &str, link: &str) { log_info( "hard_link", format!( "{},{}", self.plus_as_string(original), self.plus_as_string(link) ), ); hard_link(self.plus(original), self.plus(link)).unwrap(); } pub fn symlink_file(&self, original: &str, link: &str) { log_info( "symlink", format!( "{},{}", self.plus_as_string(original), self.plus_as_string(link) ), ); symlink_file(self.plus(original), self.plus(link)).unwrap(); } pub fn relative_symlink_file(&self, original: &str, link: &str) { #[cfg(windows)] let original = original.replace('/', &MAIN_SEPARATOR.to_string()); log_info( "symlink", format!("{},{}", &original, &self.plus_as_string(link)), ); symlink_file(original, self.plus(link)).unwrap(); } pub fn symlink_dir(&self, original: &str, link: &str) { log_info( "symlink", format!( "{},{}", self.plus_as_string(original), self.plus_as_string(link) ), ); symlink_dir(self.plus(original), self.plus(link)).unwrap(); } pub fn relative_symlink_dir(&self, original: &str, link: &str) { #[cfg(windows)] let original = original.replace('/', &MAIN_SEPARATOR.to_string()); log_info( "symlink", format!("{},{}", &original, &self.plus_as_string(link)), ); symlink_dir(original, self.plus(link)).unwrap(); } pub fn is_symlink(&self, path: &str) -> bool { log_info("is_symlink", self.plus_as_string(path)); match fs::symlink_metadata(self.plus(path)) { Ok(m) => m.file_type().is_symlink(), Err(_) => false, } } pub fn resolve_link(&self, path: &str) -> String { log_info("resolve_link", self.plus_as_string(path)); match fs::read_link(self.plus(path)) { Ok(p) => self.minus_as_string(p.to_str().unwrap()), Err(_) => String::new(), } } pub fn read_symlink(&self, path: &str) -> String { log_info("read_symlink", self.plus_as_string(path)); fs::read_link(self.plus(path)) .unwrap() .to_str() .unwrap() .to_owned() } pub fn symlink_metadata(&self, path: &str) -> fs::Metadata { match fs::symlink_metadata(self.plus(path)) { Ok(m) => m, Err(e) => panic!("{}", e), } } pub fn metadata(&self, path: &str) -> fs::Metadata { match fs::metadata(self.plus(path)) { Ok(m) => m, Err(e) => panic!("{}", e), } } pub fn file_exists>(&self, path: P) -> bool { match fs::metadata(self.plus(path)) { Ok(m) => m.is_file(), Err(_) => false, } } /// Decide whether the named symbolic link exists in the test directory. pub fn symlink_exists(&self, path: &str) -> bool { match fs::symlink_metadata(self.plus(path)) { Ok(m) => m.file_type().is_symlink(), Err(_) => false, } } pub fn dir_exists(&self, path: &str) -> bool { match fs::metadata(self.plus(path)) { Ok(m) => m.is_dir(), Err(_) => false, } } pub fn root_dir_resolved(&self) -> String { log_info("current_directory_resolved", ""); let s = self .subdir .canonicalize() .unwrap() .to_str() .unwrap() .to_owned(); // Due to canonicalize()'s use of GetFinalPathNameByHandleW() on Windows, the resolved path // starts with '\\?\' to extend the limit of a given path to 32,767 wide characters. // // To address this issue, we remove this prepended string if available. // // Source: // http://stackoverflow.com/questions/31439011/getfinalpathnamebyhandle-without-prepended let prefix = "\\\\?\\"; if let Some(stripped) = s.strip_prefix(prefix) { String::from(stripped) } else { s } } /// Set the permissions of the specified file. /// /// # Panics /// /// This function panics if there is an error loading the metadata /// or setting the permissions of the file. #[cfg(not(windows))] pub fn set_mode(&self, filename: &str, mode: u32) { let path = self.plus(filename); let mut perms = std::fs::metadata(&path).unwrap().permissions(); perms.set_mode(mode); std::fs::set_permissions(&path, perms).unwrap(); } } /// An environment for running a single uutils test case, serves three functions: /// 1. centralizes logic for locating the uutils binary and calling the utility /// 2. provides a unique temporary directory for the test case /// 3. copies over fixtures for the utility to the temporary directory /// /// Fixtures can be found under `tests/fixtures/$util_name/` pub struct TestScenario { pub bin_path: PathBuf, pub util_name: String, pub fixtures: AtPath, tmpd: Rc, } impl TestScenario { pub fn new(util_name: T) -> Self where T: AsRef, { let tmpd = Rc::new(TempDir::new().unwrap()); let ts = Self { bin_path: PathBuf::from(TESTS_BINARY), util_name: util_name.as_ref().into(), fixtures: AtPath::new(tmpd.as_ref().path()), tmpd, }; let mut fixture_path_builder = env::current_dir().unwrap(); fixture_path_builder.push(TESTS_DIR); fixture_path_builder.push(FIXTURES_DIR); fixture_path_builder.push(util_name.as_ref()); if let Ok(m) = fs::metadata(&fixture_path_builder) { if m.is_dir() { recursive_copy(&fixture_path_builder, &ts.fixtures.subdir).unwrap(); } } ts } /// Returns builder for invoking the target uutils binary. Paths given are /// treated relative to the environment's unique temporary test directory. pub fn ucmd(&self) -> UCommand { UCommand::from_test_scenario(self) } /// Returns builder for invoking any system command. Paths given are treated /// relative to the environment's unique temporary test directory. pub fn cmd>(&self, bin_path: S) -> UCommand { let mut command = UCommand::new(); command.bin_path(bin_path); command.temp_dir(self.tmpd.clone()); command } /// Returns builder for invoking any uutils command. Paths given are treated /// relative to the environment's unique temporary test directory. pub fn ccmd>(&self, util_name: S) -> UCommand { UCommand::with_util(util_name, self.tmpd.clone()) } } /// A `UCommand` is a builder wrapping an individual Command that provides several additional features: /// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command /// and asserting on the results. /// 2. it tracks arguments provided so that in test cases which may provide variations of an arg in loops /// the test failure can display the exact call which preceded an assertion failure. /// 3. it provides convenience construction methods to set the Command uutils utility and temporary directory. /// /// Per default `UCommand` runs a command given as an argument in a shell, platform independently. /// It does so with safety in mind, so the working directory is set to an individual temporary /// directory and the environment variables are cleared per default. /// /// The default behavior can be changed with builder methods: /// * [`UCommand::with_util`]: Run `util-linux UTIL_NAME` instead of the shell /// * [`UCommand::from_test_scenario`]: Run `util-linux UTIL_NAME` instead of the shell in the /// temporary directory of the [`TestScenario`] /// * [`UCommand::current_dir`]: Sets the working directory /// * ... #[derive(Debug, Default)] pub struct UCommand { args: VecDeque, env_vars: Vec<(OsString, OsString)>, current_dir: Option, bin_path: Option, util_name: Option, has_run: bool, ignore_stdin_write_error: bool, stdin: Option, stdout: Option, stderr: Option, bytes_into_stdin: Option>, #[cfg(any(target_os = "linux", target_os = "android"))] limits: Vec<(rlimit::Resource, u64, u64)>, stderr_to_stdout: bool, timeout: Option, tmpd: Option>, // drop last } impl UCommand { /// Create a new plain [`UCommand`]. /// /// Executes a command that must be given as argument (for example with [`UCommand::arg`] in a /// shell (`sh -c` on unix platforms or `cmd /C` on windows). /// /// Per default the environment is cleared and the working directory is set to an individual /// temporary directory for safety purposes. pub fn new() -> Self { Self { ..Default::default() } } /// Create a [`UCommand`] for a specific uutils utility. /// /// Sets the temporary directory to `tmpd` and the execution binary to the path where /// `util-linux` is found. pub fn with_util(util_name: T, tmpd: Rc) -> Self where T: AsRef, { let mut ucmd = Self::new(); ucmd.util_name = Some(util_name.as_ref().into()); ucmd.bin_path(TESTS_BINARY).temp_dir(tmpd); ucmd } /// Create a [`UCommand`] from a [`TestScenario`]. /// /// The temporary directory and uutils utility are inherited from the [`TestScenario`] and the /// execution binary is set to `util-linux`. pub fn from_test_scenario(scene: &TestScenario) -> Self { Self::with_util(&scene.util_name, scene.tmpd.clone()) } /// Set the execution binary. /// /// Make sure the binary found at this path is executable. It's safest to provide the /// canonicalized path instead of just the name of the executable, since path resolution is not /// guaranteed to work on all platforms. fn bin_path(&mut self, bin_path: T) -> &mut Self where T: Into, { self.bin_path = Some(bin_path.into()); self } /// Set the temporary directory. /// /// Per default an individual temporary directory is created for every [`UCommand`]. If not /// specified otherwise with [`UCommand::current_dir`] the working directory is set to this /// temporary directory. fn temp_dir(&mut self, temp_dir: Rc) -> &mut Self { self.tmpd = Some(temp_dir); self } /// Set the working directory for this [`UCommand`] /// /// Per default the working directory is set to the [`UCommands`] temporary directory. pub fn current_dir(&mut self, current_dir: T) -> &mut Self where T: Into, { self.current_dir = Some(current_dir.into()); self } pub fn set_stdin>(&mut self, stdin: T) -> &mut Self { self.stdin = Some(stdin.into()); self } pub fn set_stdout>(&mut self, stdout: T) -> &mut Self { self.stdout = Some(stdout.into()); self } pub fn set_stderr>(&mut self, stderr: T) -> &mut Self { self.stderr = Some(stderr.into()); self } pub fn stderr_to_stdout(&mut self) -> &mut Self { self.stderr_to_stdout = true; self } /// Add a parameter to the invocation. Path arguments are treated relative /// to the test environment directory. pub fn arg>(&mut self, arg: S) -> &mut Self { self.args.push_back(arg.as_ref().into()); self } /// Add multiple parameters to the invocation. Path arguments are treated relative /// to the test environment directory. pub fn args>(&mut self, args: &[S]) -> &mut Self { self.args.extend(args.iter().map(|s| s.as_ref().into())); self } /// provides standard input to feed in to the command when spawned pub fn pipe_in>>(&mut self, input: T) -> &mut Self { assert!( self.bytes_into_stdin.is_none(), "{}", MULTIPLE_STDIN_MEANINGLESS ); self.set_stdin(Stdio::piped()); self.bytes_into_stdin = Some(input.into()); self } /// like `pipe_in()`, but uses the contents of the file at the provided relative path as the piped in data pub fn pipe_in_fixture>(&mut self, file_rel_path: S) -> &mut Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.pipe_in(contents) } /// Ignores error caused by feeding stdin to the command. /// This is typically useful to test non-standard workflows /// like feeding something to a command that does not read it pub fn ignore_stdin_write_error(&mut self) -> &mut Self { self.ignore_stdin_write_error = true; self } pub fn env(&mut self, key: K, val: V) -> &mut Self where K: AsRef, V: AsRef, { self.env_vars .push((key.as_ref().into(), val.as_ref().into())); self } pub fn envs(&mut self, iter: I) -> &mut Self where I: IntoIterator, K: AsRef, V: AsRef, { for (k, v) in iter { self.env(k, v); } self } #[cfg(any(target_os = "linux", target_os = "android"))] pub fn limit( &mut self, resource: rlimit::Resource, soft_limit: u64, hard_limit: u64, ) -> &mut Self { self.limits.push((resource, soft_limit, hard_limit)); self } /// Set the timeout for [`UCommand::run`] and similar methods in [`UCommand`]. /// /// After the timeout elapsed these `run` methods (besides [`UCommand::run_no_wait`]) will /// panic. When [`UCommand::run_no_wait`] is used, this timeout is applied to /// [`UChild::wait_with_output`] including all other waiting methods in [`UChild`] implicitly /// using `wait_with_output()` and additionally [`UChild::kill`]. The default timeout of `kill` /// will be overwritten by this `timeout`. pub fn timeout(&mut self, timeout: Duration) -> &mut Self { self.timeout = Some(timeout); self } /// Build the `std::process::Command` and apply the defaults on fields which were not specified /// by the user. /// /// These __defaults__ are: /// * `bin_path`: Depending on the platform and os, the native shell (unix -> `/bin/sh` etc.). /// This default also requires to set the first argument to `-c` on unix (`/C` on windows) if /// this argument wasn't specified explicitly by the user. /// * `util_name`: `None`. If neither `bin_path` nor `util_name` were given the arguments are /// run in a shell (See `bin_path` above). /// * `temp_dir`: If `current_dir` was not set, a new temporary directory will be created in /// which this command will be run and `current_dir` will be set to this `temp_dir`. /// * `current_dir`: The temporary directory given by `temp_dir`. /// * `timeout`: `30 seconds` /// * `stdin`: `Stdio::null()` /// * `ignore_stdin_write_error`: `false` /// * `stdout`, `stderr`: If not specified the output will be captured with [`CapturedOutput`] /// * `stderr_to_stdout`: `false` /// * `bytes_into_stdin`: `None` /// * `limits`: `None`. fn build(&mut self) -> (Command, Option, Option) { if self.bin_path.is_some() { if let Some(util_name) = &self.util_name { self.args.push_front(util_name.into()); } } else if let Some(util_name) = &self.util_name { self.bin_path = Some(PathBuf::from(TESTS_BINARY)); self.args.push_front(util_name.into()); // neither `bin_path` nor `util_name` was set so we apply the default to run the arguments // in a platform specific shell } else if cfg!(unix) { #[cfg(target_os = "android")] let bin_path = PathBuf::from("/system/bin/sh"); #[cfg(not(target_os = "android"))] let bin_path = PathBuf::from("/bin/sh"); self.bin_path = Some(bin_path); let c_arg = OsString::from("-c"); if !self.args.contains(&c_arg) { self.args.push_front(c_arg); } } else { self.bin_path = Some(PathBuf::from("cmd")); let c_arg = OsString::from("/C"); let k_arg = OsString::from("/K"); if !self .args .iter() .any(|s| s.eq_ignore_ascii_case(&c_arg) || s.eq_ignore_ascii_case(&k_arg)) { self.args.push_front(c_arg); } }; // unwrap is safe here because we have set `self.bin_path` before let mut command = Command::new(self.bin_path.as_ref().unwrap()); command.args(&self.args); // We use a temporary directory as working directory if not specified otherwise with // `current_dir()`. If neither `current_dir` nor a temporary directory is available, then we // create our own. if let Some(current_dir) = &self.current_dir { command.current_dir(current_dir); } else if let Some(temp_dir) = &self.tmpd { command.current_dir(temp_dir.path()); } else { let temp_dir = tempfile::tempdir().unwrap(); self.current_dir = Some(temp_dir.path().into()); command.current_dir(temp_dir.path()); self.tmpd = Some(Rc::new(temp_dir)); } command.env_clear(); if cfg!(windows) { // spell-checker:ignore (dll) rsaenh // %SYSTEMROOT% is required on Windows to initialize crypto provider // ... and crypto provider is required for std::rand // From `procmon`: RegQueryValue HKLM\SOFTWARE\Microsoft\Cryptography\Defaults\Provider\Microsoft Strong Cryptographic Provider\Image Path // SUCCESS Type: REG_SZ, Length: 66, Data: %SystemRoot%\system32\rsaenh.dll" if let Some(systemroot) = env::var_os("SYSTEMROOT") { command.env("SYSTEMROOT", systemroot); } } else { // if someone is setting LD_PRELOAD, there's probably a good reason for it if let Some(ld_preload) = env::var_os("LD_PRELOAD") { command.env("LD_PRELOAD", ld_preload); } } command .envs(DEFAULT_ENV) .envs(self.env_vars.iter().cloned()); if self.timeout.is_none() { self.timeout = Some(Duration::from_secs(30)); } let mut captured_stdout = None; let mut captured_stderr = None; if self.stderr_to_stdout { let mut output = CapturedOutput::default(); command .stdin(self.stdin.take().unwrap_or_else(Stdio::null)) .stdout(Stdio::from(output.try_clone().unwrap())) .stderr(Stdio::from(output.try_clone().unwrap())); captured_stdout = Some(output); } else { let stdout = if self.stdout.is_some() { self.stdout.take().unwrap() } else { let mut stdout = CapturedOutput::default(); let stdio = Stdio::from(stdout.try_clone().unwrap()); captured_stdout = Some(stdout); stdio }; let stderr = if self.stderr.is_some() { self.stderr.take().unwrap() } else { let mut stderr = CapturedOutput::default(); let stdio = Stdio::from(stderr.try_clone().unwrap()); captured_stderr = Some(stderr); stdio }; command .stdin(self.stdin.take().unwrap_or_else(Stdio::null)) .stdout(stdout) .stderr(stderr); }; (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 { 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"))] for &(resource, soft_limit, hard_limit) in &self.limits { prlimit( child.id() as i32, resource, Some((soft_limit, hard_limit)), None, ) .unwrap(); } let mut child = UChild::from(self, child, captured_stdout, captured_stderr); if let Some(input) = self.bytes_into_stdin.take() { child.pipe_in(input); } child } /// Spawns the command, feeds the stdin if any, waits for the result /// and returns a command result. /// It is recommended that you instead use succeeds() or fails() pub fn run(&mut self) -> CmdResult { self.run_no_wait().wait().unwrap() } /// Spawns the command, feeding the passed in stdin, waits for the result /// and returns a command result. /// It is recommended that, instead of this, you use a combination of `pipe_in()` /// with succeeds() or fails() pub fn run_piped_stdin>>(&mut self, input: T) -> CmdResult { self.pipe_in(input).run() } /// Spawns the command, feeds the stdin if any, waits for the result, /// asserts success, and returns a command result. #[track_caller] pub fn succeeds(&mut self) -> CmdResult { let cmd_result = self.run(); cmd_result.success(); cmd_result } /// Spawns the command, feeds the stdin if any, waits for the result, /// asserts failure, and returns a command result. #[track_caller] pub fn fails(&mut self) -> CmdResult { let cmd_result = self.run(); cmd_result.failure(); cmd_result } pub fn get_full_fixture_path(&self, file_rel_path: &str) -> String { let tmpdir_path = self.tmpd.as_ref().unwrap().path(); format!("{}/{file_rel_path}", tmpdir_path.to_str().unwrap()) } } impl std::fmt::Display for UCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut comm_string: Vec = vec![self .bin_path .as_ref() .map_or(String::new(), |p| p.display().to_string())]; comm_string.extend(self.args.iter().map(|s| s.to_string_lossy().to_string())); f.write_str(&comm_string.join(" ")) } } /// Stored the captured output in a temporary file. The file is deleted as soon as /// [`CapturedOutput`] is dropped. #[derive(Debug)] struct CapturedOutput { current_file: File, output: tempfile::NamedTempFile, // drop last } impl CapturedOutput { /// Creates a new instance of `CapturedOutput` fn new(output: tempfile::NamedTempFile) -> Self { Self { current_file: output.reopen().unwrap(), output, } } /// Try to clone the file pointer. fn try_clone(&mut self) -> io::Result { self.output.as_file().try_clone() } /// Return the captured output as [`String`]. /// /// Subsequent calls to any of the other output methods will operate on the subsequent output. fn output(&mut self) -> String { String::from_utf8(self.output_bytes()).unwrap() } /// Return the exact amount of bytes as `String`. /// /// Subsequent calls to any of the other output methods will operate on the subsequent output. /// /// # Important /// /// This method blocks indefinitely if the amount of bytes given by `size` cannot be read fn output_exact(&mut self, size: usize) -> String { String::from_utf8(self.output_exact_bytes(size)).unwrap() } /// Return the captured output as bytes. /// /// Subsequent calls to any of the other output methods will operate on the subsequent output. fn output_bytes(&mut self) -> Vec { let mut buffer = Vec::::new(); self.current_file.read_to_end(&mut buffer).unwrap(); buffer } /// Return all captured output, so far. /// /// Subsequent calls to any of the other output methods will operate on the subsequent output. fn output_all_bytes(&mut self) -> Vec { let mut buffer = Vec::::new(); let mut file = self.output.reopen().unwrap(); file.read_to_end(&mut buffer).unwrap(); self.current_file = file; buffer } /// Return the exact amount of bytes. /// /// Subsequent calls to any of the other output methods will operate on the subsequent output. /// /// # Important /// /// This method blocks indefinitely if the amount of bytes given by `size` cannot be read fn output_exact_bytes(&mut self, size: usize) -> Vec { let mut buffer = vec![0; size]; self.current_file.read_exact(&mut buffer).unwrap(); buffer } } impl Default for CapturedOutput { fn default() -> Self { let mut retries = 10; let file = loop { let file = Builder::new().rand_bytes(10).suffix(".out").tempfile(); if file.is_ok() || retries <= 0 { break file.unwrap(); } sleep(Duration::from_millis(100)); retries -= 1; }; Self { current_file: file.reopen().unwrap(), output: file, } } } impl Drop for CapturedOutput { fn drop(&mut self) { let _ = remove_file(self.output.path()); } } #[derive(Debug, Copy, Clone)] pub enum AssertionMode { All, Current, Exact(usize, usize), } pub struct UChildAssertion<'a> { uchild: &'a mut UChild, } impl<'a> UChildAssertion<'a> { pub fn new(uchild: &'a mut UChild) -> Self { Self { uchild } } fn with_output(&mut self, mode: AssertionMode) -> CmdResult { let exit_status = if self.uchild.is_alive() { None } else { Some(self.uchild.raw.wait().unwrap()) }; let (stdout, stderr) = match mode { AssertionMode::All => ( self.uchild.stdout_all_bytes(), self.uchild.stderr_all_bytes(), ), AssertionMode::Current => (self.uchild.stdout_bytes(), self.uchild.stderr_bytes()), AssertionMode::Exact(expected_stdout_size, expected_stderr_size) => ( self.uchild.stdout_exact_bytes(expected_stdout_size), self.uchild.stderr_exact_bytes(expected_stderr_size), ), }; CmdResult::new( self.uchild.bin_path.clone(), self.uchild.util_name.clone(), self.uchild.tmpd.clone(), exit_status, stdout, stderr, ) } // Make assertions of [`CmdResult`] with all output from start of the process until now. // // This method runs [`UChild::stdout_all_bytes`] and [`UChild::stderr_all_bytes`] under the // hood. See there for side effects pub fn with_all_output(&mut self) -> CmdResult { self.with_output(AssertionMode::All) } // Make assertions of [`CmdResult`] with the current output. // // This method runs [`UChild::stdout_bytes`] and [`UChild::stderr_bytes`] under the hood. See // there for side effects pub fn with_current_output(&mut self) -> CmdResult { self.with_output(AssertionMode::Current) } // Make assertions of [`CmdResult`] with the exact output. // // This method runs [`UChild::stdout_exact_bytes`] and [`UChild::stderr_exact_bytes`] under the // hood. See there for side effects pub fn with_exact_output( &mut self, expected_stdout_size: usize, expected_stderr_size: usize, ) -> CmdResult { self.with_output(AssertionMode::Exact( expected_stdout_size, expected_stderr_size, )) } // Assert that the child process is alive #[track_caller] pub fn is_alive(&mut self) -> &mut Self { match self .uchild .raw .try_wait() { Ok(Some(status)) => panic!( "Assertion failed. Expected '{}' to be running but exited with status={}.\nstdout: {}\nstderr: {}", uucore::util_name(), status, self.uchild.stdout_all(), self.uchild.stderr_all() ), Ok(None) => {} Err(error) => panic!("Assertion failed with error '{error:?}'"), } self } // Assert that the child process has exited #[track_caller] pub fn is_not_alive(&mut self) -> &mut Self { match self .uchild .raw .try_wait() { Ok(None) => panic!( "Assertion failed. Expected '{}' to be not running but was alive.\nstdout: {}\nstderr: {}", uucore::util_name(), self.uchild.stdout_all(), self.uchild.stderr_all()), Ok(_) => {}, Err(error) => panic!("Assertion failed with error '{error:?}'"), } self } } /// Abstraction for a [`std::process::Child`] to handle the child process. pub struct UChild { raw: Child, bin_path: PathBuf, util_name: Option, captured_stdout: Option, captured_stderr: Option, ignore_stdin_write_error: bool, stderr_to_stdout: bool, join_handle: Option>>, timeout: Option, tmpd: Option>, // drop last } impl UChild { fn from( ucommand: &UCommand, child: Child, captured_stdout: Option, captured_stderr: Option, ) -> Self { Self { raw: child, 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, stderr_to_stdout: ucommand.stderr_to_stdout, join_handle: None, timeout: ucommand.timeout, tmpd: ucommand.tmpd.clone(), } } /// Convenience method for `sleep(Duration::from_millis(millis))` pub fn delay(&mut self, millis: u64) -> &mut Self { sleep(Duration::from_millis(millis)); self } /// Return the pid of the child process, similar to [`Child::id`]. pub fn id(&self) -> u32 { self.raw.id() } /// Return true if the child process is still alive and false otherwise. pub fn is_alive(&mut self) -> bool { self.raw.try_wait().unwrap().is_none() } /// Return true if the child process is exited and false otherwise. #[allow(clippy::wrong_self_convention)] pub fn is_not_alive(&mut self) -> bool { !self.is_alive() } /// Return a [`UChildAssertion`] pub fn make_assertion(&mut self) -> UChildAssertion { UChildAssertion::new(self) } /// Convenience function for calling [`UChild::delay`] and then [`UChild::make_assertion`] pub fn make_assertion_with_delay(&mut self, millis: u64) -> UChildAssertion { self.delay(millis).make_assertion() } /// Try to kill the child process and wait for it's termination. /// /// This method blocks until the child process is killed, but returns an error if `self.timeout` /// or the default of 60s was reached. If no such error happened, the process resources are /// released, so there is usually no need to call `wait` or alike on unix systems although it's /// still possible to do so. /// /// # Platform specific behavior /// /// On unix systems the child process resources will be released like a call to [`Child::wait`] /// or alike would do. /// /// # Error /// /// If [`Child::kill`] returned an error or if the child process could not be terminated within /// `self.timeout` or the default of 60s. pub fn try_kill(&mut self) -> io::Result<()> { let start = Instant::now(); self.raw.kill()?; let timeout = self.timeout.unwrap_or(Duration::from_secs(60)); // As a side effect, we're cleaning up the killed child process with the implicit call to // `Child::try_wait` in `self.is_alive`, which reaps the process id on unix systems. We // always fail with error on timeout if `self.timeout` is set to zero. while self.is_alive() || timeout == Duration::ZERO { if start.elapsed() < timeout { self.delay(10); } else { return Err(io::Error::new( io::ErrorKind::Other, format!("kill: Timeout of '{}s' reached", timeout.as_secs_f64()), )); } hint::spin_loop(); } Ok(()) } /// Terminate the child process unconditionally and wait for the termination. /// /// Ignores any errors happening during [`Child::kill`] (i.e. child process already exited) but /// still panics on timeout. /// /// # Panics /// If the child process could not be terminated within `self.timeout` or the default of 60s. pub fn kill(&mut self) -> &mut Self { self.try_kill() .or_else(|error| { // We still throw the error on timeout in the `try_kill` function if error.kind() == io::ErrorKind::Other { Err(error) } else { Ok(()) } }) .unwrap(); self } /// Wait for the child process to terminate and return a [`CmdResult`]. /// /// See [`UChild::wait_with_output`] for details on timeouts etc. This method can also be run if /// the child process was killed with [`UChild::kill`]. /// /// # Errors /// /// Returns the error from the call to [`UChild::wait_with_output`] if any pub fn wait(self) -> io::Result { let (bin_path, util_name, tmpd) = ( self.bin_path.clone(), self.util_name.clone(), self.tmpd.clone(), ); #[allow(deprecated)] let output = self.wait_with_output()?; Ok(CmdResult { bin_path, util_name, tmpd, exit_status: Some(output.status), stdout: output.stdout, stderr: output.stderr, }) } /// Wait for the child process to terminate and return an instance of [`Output`]. /// /// If `self.timeout` is reached while waiting, a [`io::ErrorKind::Other`] representing a /// timeout error is returned. If no errors happened, we join with the thread created by /// [`UChild::pipe_in`] if any. /// /// # Error /// /// If `self.timeout` is reached while waiting or [`Child::wait_with_output`] returned an /// error. #[deprecated = "Please use wait() -> io::Result instead."] pub fn wait_with_output(mut self) -> io::Result { let output = if let Some(timeout) = self.timeout { let child = self.raw; let (sender, receiver) = mpsc::channel(); let handle = thread::spawn(move || sender.send(child.wait_with_output())); match receiver.recv_timeout(timeout) { Ok(result) => { // unwraps are safe here because we got a result from the sender and there was no panic // causing a disconnect. handle.join().unwrap().unwrap(); result } Err(RecvTimeoutError::Timeout) => Err(io::Error::new( io::ErrorKind::Other, format!("wait: Timeout of '{}s' reached", timeout.as_secs_f64()), )), Err(RecvTimeoutError::Disconnected) => { handle.join().expect("Panic caused disconnect").unwrap(); panic!("Error receiving from waiting thread because of unexpected disconnect"); } } } else { self.raw.wait_with_output() }; let mut output = output?; if let Some(join_handle) = self.join_handle.take() { join_handle .join() .expect("Error joining with the piping stdin thread") .unwrap(); }; if let Some(stdout) = self.captured_stdout.as_mut() { output.stdout = stdout.output_bytes(); } if let Some(stderr) = self.captured_stderr.as_mut() { output.stderr = stderr.output_bytes(); } Ok(output) } /// Read, consume and return the output as [`String`] from [`Child`]'s stdout. /// /// See also [`UChild::stdout_bytes`] for side effects. pub fn stdout(&mut self) -> String { String::from_utf8(self.stdout_bytes()).unwrap() } /// Read and return all child's output in stdout as String. /// /// Note, that a subsequent call of any of these functions /// /// * [`UChild::stdout`] /// * [`UChild::stdout_bytes`] /// * [`UChild::stdout_exact_bytes`] /// /// will operate on the subsequent output of the child process. pub fn stdout_all(&mut self) -> String { String::from_utf8(self.stdout_all_bytes()).unwrap() } /// Read, consume and return the output as bytes from [`Child`]'s stdout. /// /// Each subsequent call to any of the functions below will operate on the subsequent output of /// the child process: /// /// * [`UChild::stdout`] /// * [`UChild::stdout_exact_bytes`] /// * and the call to itself [`UChild::stdout_bytes`] pub fn stdout_bytes(&mut self) -> Vec { match self.captured_stdout.as_mut() { Some(output) => output.output_bytes(), None if self.raw.stdout.is_some() => { let mut buffer: Vec = vec![]; let stdout = self.raw.stdout.as_mut().unwrap(); stdout.read_to_end(&mut buffer).unwrap(); buffer } None => vec![], } } /// Read and return all output from start of the child process until now. /// /// Each subsequent call of any of the methods below will operate on the subsequent output of /// the child process. This method will panic if the output wasn't captured (for example if /// [`UCommand::set_stdout`] was used). /// /// * [`UChild::stdout`] /// * [`UChild::stdout_bytes`] /// * [`UChild::stdout_exact_bytes`] pub fn stdout_all_bytes(&mut self) -> Vec { match self.captured_stdout.as_mut() { Some(output) => output.output_all_bytes(), None => { panic!("Usage error: This method cannot be used if the output wasn't captured.") } } } /// Read, consume and return the exact amount of bytes from `stdout`. /// /// This method may block indefinitely if the `size` amount of bytes exceeds the amount of bytes /// that can be read. See also [`UChild::stdout_bytes`] for side effects. pub fn stdout_exact_bytes(&mut self, size: usize) -> Vec { match self.captured_stdout.as_mut() { Some(output) => output.output_exact_bytes(size), None if self.raw.stdout.is_some() => { let mut buffer = vec![0; size]; let stdout = self.raw.stdout.as_mut().unwrap(); stdout.read_exact(&mut buffer).unwrap(); buffer } None => vec![], } } /// Read, consume and return the child's stderr as String. /// /// See also [`UChild::stdout_bytes`] for side effects. If stderr is redirected to stdout with /// [`UCommand::stderr_to_stdout`] then always an empty string will be returned. pub fn stderr(&mut self) -> String { String::from_utf8(self.stderr_bytes()).unwrap() } /// Read and return all child's output in stderr as String. /// /// Note, that a subsequent call of any of these functions /// /// * [`UChild::stderr`] /// * [`UChild::stderr_bytes`] /// * [`UChild::stderr_exact_bytes`] /// /// will operate on the subsequent output of the child process. If stderr is redirected to /// stdout with [`UCommand::stderr_to_stdout`] then always an empty string will be returned. pub fn stderr_all(&mut self) -> String { String::from_utf8(self.stderr_all_bytes()).unwrap() } /// Read, consume and return the currently available bytes from child's stderr. /// /// If stderr is redirected to stdout with [`UCommand::stderr_to_stdout`] then always zero bytes /// are returned. See also [`UChild::stdout_bytes`] for side effects. pub fn stderr_bytes(&mut self) -> Vec { match self.captured_stderr.as_mut() { Some(output) => output.output_bytes(), None if self.raw.stderr.is_some() => { let mut buffer: Vec = vec![]; let stderr = self.raw.stderr.as_mut().unwrap(); stderr.read_to_end(&mut buffer).unwrap(); buffer } None => vec![], } } /// Read and return all output from start of the child process until now. /// /// Each subsequent call of any of the methods below will operate on the subsequent output of /// the child process. This method will panic if the output wasn't captured (for example if /// [`UCommand::set_stderr`] was used). If [`UCommand::stderr_to_stdout`] was used always zero /// bytes are returned. /// /// * [`UChild::stderr`] /// * [`UChild::stderr_bytes`] /// * [`UChild::stderr_exact_bytes`] pub fn stderr_all_bytes(&mut self) -> Vec { match self.captured_stderr.as_mut() { Some(output) => output.output_all_bytes(), None if self.stderr_to_stdout => vec![], None => { panic!("Usage error: This method cannot be used if the output wasn't captured.") } } } /// Read, consume and return the exact amount of bytes from stderr. /// /// If stderr is redirect to stdout with [`UCommand::stderr_to_stdout`] then always zero bytes /// are returned. /// /// # Important /// This method blocks indefinitely if the `size` amount of bytes cannot be read. pub fn stderr_exact_bytes(&mut self, size: usize) -> Vec { match self.captured_stderr.as_mut() { Some(output) => output.output_exact_bytes(size), None if self.raw.stderr.is_some() => { let stderr = self.raw.stderr.as_mut().unwrap(); let mut buffer = vec![0; size]; stderr.read_exact(&mut buffer).unwrap(); buffer } None => vec![], } } /// Pipe data into [`Child`] stdin in a separate thread to avoid deadlocks. /// /// In contrast to [`UChild::write_in`], this method is designed to simulate a pipe on the /// command line and can be used only once or else panics. Note, that [`UCommand::set_stdin`] /// must be used together with [`Stdio::piped`] or else this method doesn't work as expected. /// `Stdio::piped` is the current default when using [`UCommand::run_no_wait`]) without calling /// `set_stdin`. This method stores a [`JoinHandle`] of the thread in which the writing to the /// child processes' stdin is running. The associated thread is joined with the main process in /// the methods below when exiting the child process. /// /// * [`UChild::wait`] /// * [`UChild::wait_with_output`] /// * [`UChild::pipe_in_and_wait`] /// * [`UChild::pipe_in_and_wait_with_output`] /// /// Usually, there's no need to join manually but if needed, the [`UChild::join`] method can be /// used . /// /// [`JoinHandle`]: std::thread::JoinHandle pub fn pipe_in>>(&mut self, content: T) -> &mut Self { let ignore_stdin_write_error = self.ignore_stdin_write_error; let content = content.into(); let stdin = self .raw .stdin .take() .expect("Could not pipe into child process. Was it set to Stdio::null()?"); let join_handle = thread::spawn(move || { let mut writer = BufWriter::new(stdin); match writer.write_all(&content).and_then(|()| writer.flush()) { Err(error) if !ignore_stdin_write_error => Err(io::Error::new( io::ErrorKind::Other, format!("failed to write to stdin of child: {error}"), )), Ok(()) | Err(_) => Ok(()), } }); self.join_handle = Some(join_handle); self } /// Call join on the thread created by [`UChild::pipe_in`] and if the thread is still running. /// /// This method can be called multiple times but is a noop if already joined. pub fn join(&mut self) -> &mut Self { if let Some(join_handle) = self.join_handle.take() { join_handle .join() .expect("Error joining with the piping stdin thread") .unwrap(); } self } /// Convenience method for [`UChild::pipe_in`] and then [`UChild::wait`] pub fn pipe_in_and_wait>>(mut self, content: T) -> CmdResult { self.pipe_in(content); self.wait().unwrap() } /// Convenience method for [`UChild::pipe_in`] and then [`UChild::wait_with_output`] #[deprecated = "Please use pipe_in_and_wait() -> CmdResult instead."] pub fn pipe_in_and_wait_with_output>>(mut self, content: T) -> Output { self.pipe_in(content); #[allow(deprecated)] self.wait_with_output().unwrap() } /// Write some bytes to the child process stdin. /// /// This function is meant for small data and faking user input like typing a `yes` or `no`. /// This function blocks until all data is written but can be used multiple times in contrast to /// [`UChild::pipe_in`]. /// /// # Errors /// If [`ChildStdin::write_all`] or [`ChildStdin::flush`] returned an error pub fn try_write_in>>(&mut self, data: T) -> io::Result<()> { let stdin = self.raw.stdin.as_mut().unwrap(); match stdin.write_all(&data.into()).and_then(|()| stdin.flush()) { Err(error) if !self.ignore_stdin_write_error => Err(io::Error::new( io::ErrorKind::Other, format!("failed to write to stdin of child: {error}"), )), Ok(()) | Err(_) => Ok(()), } } /// Convenience function for [`UChild::try_write_in`] and a following `unwrap`. pub fn write_in>>(&mut self, data: T) -> &mut Self { self.try_write_in(data).unwrap(); self } /// Close the child process stdout. /// /// Note this will have no effect if the output was captured with [`CapturedOutput`] which is the /// default if [`UCommand::set_stdout`] wasn't called. pub fn close_stdout(&mut self) -> &mut Self { self.raw.stdout.take(); self } /// Close the child process stderr. /// /// Note this will have no effect if the output was captured with [`CapturedOutput`] which is the /// default if [`UCommand::set_stderr`] wasn't called. pub fn close_stderr(&mut self) -> &mut Self { self.raw.stderr.take(); self } /// Close the child process stdin. /// /// Note, this does not have any effect if using the [`UChild::pipe_in`] method. pub fn close_stdin(&mut self) -> &mut Self { self.raw.stdin.take(); self } } pub fn vec_of_size(n: usize) -> Vec { let result = vec![b'a'; n]; assert_eq!(result.len(), n); result } pub fn whoami() -> String { // Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. // // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" // whoami: cannot find name for user ID 1001 // id --name: cannot find name for user ID 1001 // id --name: cannot find name for group ID 116 // // However, when running "id" from within "/bin/bash" it looks fine: // id: "uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),101(systemd-journal)" // whoami: "runner" // Use environment variable to get current user instead of // invoking `whoami` and fall back to user "nobody" on error. std::env::var("USER") .or_else(|_| std::env::var("USERNAME")) .unwrap_or_else(|e| { println!("{UUTILS_WARNING}: {e}, using \"nobody\" instead"); "nobody".to_string() }) } /// Add prefix 'g' for `util_name` if not on linux #[cfg(unix)] pub fn host_name_for(util_name: &str) -> Cow { // In some environments, e.g. macOS/freebsd, the GNU util-linux are prefixed with "g" // to not interfere with the BSD counterparts already in `$PATH`. #[cfg(not(target_os = "linux"))] { // make call to `host_name_for` idempotent if util_name.starts_with('g') && util_name != "groups" { util_name.into() } else { format!("g{util_name}").into() } } #[cfg(target_os = "linux")] util_name.into() } // GNU util-linux version 8.32 is the reference version since it is the latest version and the // GNU test suite in "util-linux/.github/workflows/GnuTests.yml" runs against it. // However, here 8.30 was chosen because right now there's no ubuntu image for the github actions // CICD available with a higher version than 8.30. // GNU util-linux versions from the CICD images for comparison: // ubuntu-2004: 8.30 (latest) // ubuntu-1804: 8.28 // macos-latest: 8.32 const VERSION_MIN: &str = "8.30"; // minimum Version for the reference `coreutil` in `$PATH` const UUTILS_WARNING: &str = "uutils-tests-warning"; const UUTILS_INFO: &str = "uutils-tests-info"; /// Run `util_name --version` and return Ok if the version is >= `version_expected`. /// Returns an error if /// * `util_name` cannot run /// * the version cannot be parsed /// * the version is too low /// /// This is used by `expected_result` to check if the util-linux version is >= `VERSION_MIN`. /// It makes sense to use this manually in a test if a feature /// is tested that was introduced after `VERSION_MIN` /// /// Example: /// /// ```no_run /// use crate::common::util::*; /// const VERSION_MIN_MULTIPLE_USERS: &str = "8.31"; /// /// #[test] /// fn test_xyz() { /// unwrap_or_return!(check_coreutil_version( /// util_name!(), /// VERSION_MIN_MULTIPLE_USERS /// )); /// // proceed with the test... /// } /// ``` #[cfg(unix)] pub fn check_coreutil_version( util_name: &str, version_expected: &str, ) -> std::result::Result { // example: // $ id --version | head -n 1 // id (GNU util-linux) 8.32.162-4eda let util_name = &host_name_for(util_name); log_info("run", format!("{util_name} --version")); let version_check = match Command::new(util_name.as_ref()) .env("LC_ALL", "C") .arg("--version") .output() { Ok(s) => s, Err(e) => return Err(format!("{UUTILS_WARNING}: '{util_name}' {e}")), }; std::str::from_utf8(&version_check.stdout).unwrap() .split('\n') .collect::>() .first() .map_or_else( || Err(format!("{UUTILS_WARNING}: unexpected output format for reference coreutil: '{util_name} --version'")), |s| { if s.contains(&format!("(GNU util-linux) {version_expected}")) { Ok(format!("{UUTILS_INFO}: {s}")) } else if s.contains("(GNU util-linux)") { let version_found = parse_coreutil_version(s); let version_expected = version_expected.parse::().unwrap_or_default(); if version_found > version_expected { Ok(format!("{UUTILS_INFO}: version for the reference coreutil '{util_name}' is higher than expected; expected: {version_expected}, found: {version_found}")) } else { Err(format!("{UUTILS_WARNING}: version for the reference coreutil '{util_name}' does not match; expected: {version_expected}, found: {version_found}")) } } else { Err(format!("{UUTILS_WARNING}: no util-linux version string found for reference util-linux '{util_name} --version'")) } }, ) } // simple heuristic to parse the util-linux SemVer string, e.g. "id (GNU util-linux) 8.32.263-0475" fn parse_coreutil_version(version_string: &str) -> f32 { version_string .split_whitespace() .last() .unwrap() .split('.') .take(2) .collect::>() .join(".") .parse::() .unwrap_or_default() } /// This runs the GNU util-linux `util_name` binary in `$PATH` in order to /// dynamically gather reference values on the system. /// If the `util_name` in `$PATH` doesn't include a util-linux version string, /// or the version is too low, this returns an error and the test should be skipped. /// /// Example: /// /// ```no_run /// use crate::common::util::*; /// #[test] /// fn test_xyz() { /// let ts = TestScenario::new(util_name!()); /// let result = ts.ucmd().run(); /// let exp_result = unwrap_or_return!(expected_result(&ts, &[])); /// result /// .stdout_is(exp_result.stdout_str()) /// .stderr_is(exp_result.stderr_str()) /// .code_is(exp_result.code()); /// } ///``` #[cfg(unix)] pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result { let util_name = ts.util_name.as_str(); println!("{}", check_coreutil_version(util_name, VERSION_MIN)?); let util_name = host_name_for(util_name); let result = ts .cmd(util_name.as_ref()) .env("PATH", PATH) .envs(DEFAULT_ENV) .args(args) .run(); let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") { ( result.stdout_str().to_string(), result.stderr_str().to_string(), ) } else { // `host_name_for` added prefix, strip 'g' prefix from results: let from = util_name.to_string() + ":"; let to = &from[1..]; ( result.stdout_str().replace(&from, to), result.stderr_str().replace(&from, to), ) }; Ok(CmdResult::new( ts.bin_path.as_os_str().to_str().unwrap().to_string(), Some(ts.util_name.clone()), Some(result.tmpd()), result.exit_status, stdout.as_bytes(), stderr.as_bytes(), )) } /// This is a convenience wrapper to run a ucmd with root permissions. /// It can be used to test programs when being root is needed /// This runs `sudo -E --non-interactive target/debug/util-linux util_name args` /// This is primarily designed to run in an environment where whoami is in $path /// and where non-interactive sudo is possible. /// To check if i) non-interactive sudo is possible and ii) if sudo works, this runs: /// `sudo -E --non-interactive whoami` first. /// /// This return an `Err()` if run inside CICD because there's no 'sudo'. /// /// Example: /// /// ```no_run /// use crate::common::util::*; /// #[test] /// fn test_xyz() { /// let ts = TestScenario::new("whoami"); /// let expected = "root\n".to_string(); /// if let Ok(result) = run_ucmd_as_root(&ts, &[]) { /// result.stdout_is(expected); /// } else { /// println!("TEST SKIPPED"); /// } /// } ///``` #[cfg(unix)] pub fn run_ucmd_as_root( ts: &TestScenario, args: &[&str], ) -> std::result::Result { run_ucmd_as_root_with_stdin_stdout(ts, args, None, None) } #[cfg(unix)] pub fn run_ucmd_as_root_with_stdin_stdout( ts: &TestScenario, args: &[&str], stdin: Option<&str>, stdout: Option<&str>, ) -> std::result::Result { if is_ci() { Err(format!("{UUTILS_INFO}: {}", "cannot run inside CI")) } else { // check if we can run 'sudo' log_info("run", "sudo -E --non-interactive whoami"); match Command::new("sudo") .envs(DEFAULT_ENV) .args(["-E", "--non-interactive", "whoami"]) .output() { Ok(output) if String::from_utf8_lossy(&output.stdout).eq("root\n") => { // we can run sudo and we're root // run ucmd as root: let mut cmd = ts.cmd("sudo"); cmd.env("PATH", PATH) .envs(DEFAULT_ENV) .arg("-E") .arg("--non-interactive") .arg(&ts.bin_path) .arg(&ts.util_name) .args(args); if let Some(stdin) = stdin { cmd.set_stdin(File::open(stdin).unwrap()); } if let Some(stdout) = stdout { cmd.set_stdout(File::open(stdout).unwrap()); } Ok(cmd.run()) } Ok(output) if String::from_utf8_lossy(&output.stderr).eq("sudo: a password is required\n") => { Err("Cannot run non-interactive sudo".to_string()) } Ok(_output) => Err("\"sudo whoami\" didn't return \"root\"".to_string()), Err(e) => Err(format!("{UUTILS_WARNING}: {e}")), } } } /// Sanity checks for test utils #[cfg(test)] mod tests { // spell-checker:ignore (tests) asdfsadfa use super::*; pub fn run_cmd>(cmd: T) -> CmdResult { UCommand::new().arg(cmd).run() } #[test] fn test_command_result_when_no_output_with_exit_32() { let result = run_cmd("exit 32"); if cfg!(windows) { std::assert!(result.bin_path.ends_with("cmd")); } else { std::assert!(result.bin_path.ends_with("sh")); } std::assert!(result.util_name.is_none()); std::assert!(result.tmpd.is_some()); assert!(result.exit_status.is_some()); std::assert_eq!(result.code(), 32); result.code_is(32); assert!(!result.succeeded()); result.failure(); result.fails_silently(); assert!(result.stderr.is_empty()); assert!(result.stdout.is_empty()); result.no_output(); result.no_stderr(); result.no_stdout(); } #[test] #[should_panic] fn test_command_result_when_exit_32_then_success_panic() { run_cmd("exit 32").success(); } #[test] fn test_command_result_when_no_output_with_exit_0() { let result = run_cmd("exit 0"); assert!(result.exit_status.is_some()); std::assert_eq!(result.code(), 0); result.code_is(0); assert!(result.succeeded()); result.success(); assert!(result.stderr.is_empty()); assert!(result.stdout.is_empty()); result.no_output(); result.no_stderr(); result.no_stdout(); } #[test] #[should_panic] fn test_command_result_when_exit_0_then_failure_panics() { run_cmd("exit 0").failure(); } #[test] #[should_panic] fn test_command_result_when_exit_0_then_silent_failure_panics() { run_cmd("exit 0").fails_silently(); } #[test] fn test_command_result_when_stdout_with_exit_0() { #[cfg(windows)] let (result, vector, string) = ( run_cmd("echo hello& exit 0"), vec![b'h', b'e', b'l', b'l', b'o', b'\r', b'\n'], "hello\r\n", ); #[cfg(not(windows))] let (result, vector, string) = ( run_cmd("echo hello; exit 0"), vec![b'h', b'e', b'l', b'l', b'o', b'\n'], "hello\n", ); assert!(result.exit_status.is_some()); std::assert_eq!(result.code(), 0); result.code_is(0); assert!(result.succeeded()); result.success(); assert!(result.stderr.is_empty()); std::assert_eq!(result.stdout, vector); result.no_stderr(); result.stdout_is(string); result.stdout_is_bytes(&vector); result.stdout_only(string); result.stdout_only_bytes(&vector); } #[test] fn test_command_result_when_stderr_with_exit_0() { #[cfg(windows)] let (result, vector, string) = ( run_cmd("echo hello>&2& exit 0"), vec![b'h', b'e', b'l', b'l', b'o', b'\r', b'\n'], "hello\r\n", ); #[cfg(not(windows))] let (result, vector, string) = ( run_cmd("echo hello >&2; exit 0"), vec![b'h', b'e', b'l', b'l', b'o', b'\n'], "hello\n", ); assert!(result.exit_status.is_some()); std::assert_eq!(result.code(), 0); result.code_is(0); assert!(result.succeeded()); result.success(); assert!(result.stdout.is_empty()); result.no_stdout(); std::assert_eq!(result.stderr, vector); result.stderr_is(string); result.stderr_is_bytes(&vector); result.stderr_only(string); result.stderr_only_bytes(&vector); } #[test] fn test_std_does_not_contain() { #[cfg(windows)] let res = run_cmd( "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", ); #[cfg(not(windows))] let res = run_cmd( "echo This is a likely error message; echo This is a likely error message >&2; exit 0", ); res.stdout_does_not_contain("unlikely"); res.stderr_does_not_contain("unlikely"); } #[test] #[should_panic] fn test_stdout_does_not_contain_fail() { #[cfg(windows)] let res = run_cmd("echo This is a likely error message& exit 0"); #[cfg(not(windows))] let res = run_cmd("echo This is a likely error message; exit 0"); res.stdout_does_not_contain("likely"); } #[test] #[should_panic] fn test_stderr_does_not_contain_fail() { #[cfg(windows)] let res = run_cmd("echo This is a likely error message>&2 & exit 0"); #[cfg(not(windows))] let res = run_cmd("echo This is a likely error message >&2; exit 0"); res.stderr_does_not_contain("likely"); } #[test] fn test_stdout_matches() { #[cfg(windows)] let res = run_cmd( "(echo This is a likely error message& echo This is a likely error message>&2 ) & exit 0", ); #[cfg(not(windows))] let res = run_cmd( "echo This is a likely error message; echo This is a likely error message >&2; exit 0", ); let positive = regex::Regex::new(".*likely.*").unwrap(); let negative = regex::Regex::new(".*unlikely.*").unwrap(); res.stdout_matches(&positive); res.stdout_does_not_match(&negative); } #[test] #[should_panic] fn test_stdout_matches_fail() { #[cfg(windows)] let res = run_cmd( "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", ); #[cfg(not(windows))] let res = run_cmd( "echo This is a likely error message; echo This is a likely error message >&2; exit 0", ); let negative = regex::Regex::new(".*unlikely.*").unwrap(); res.stdout_matches(&negative); } #[test] #[should_panic] fn test_stdout_not_matches_fail() { #[cfg(windows)] let res = run_cmd( "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", ); #[cfg(not(windows))] let res = run_cmd( "echo This is a likely error message; echo This is a likely error message >&2; exit 0", ); let positive = regex::Regex::new(".*likely.*").unwrap(); res.stdout_does_not_match(&positive); } #[cfg(feature = "echo")] #[test] fn test_normalized_newlines_stdout_is() { let ts = TestScenario::new("echo"); let res = ts.ucmd().args(&["-ne", "A\r\nB\nC"]).run(); res.normalized_newlines_stdout_is("A\r\nB\nC"); res.normalized_newlines_stdout_is("A\nB\nC"); res.normalized_newlines_stdout_is("A\nB\r\nC"); } #[cfg(feature = "echo")] #[test] #[should_panic] fn test_normalized_newlines_stdout_is_fail() { let ts = TestScenario::new("echo"); let res = ts.ucmd().args(&["-ne", "A\r\nB\nC"]).run(); res.normalized_newlines_stdout_is("A\r\nB\nC\n"); } #[cfg(feature = "echo")] #[test] fn test_cmd_result_stdout_check_and_stdout_str_check() { let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); result.stdout_str_check(|stdout| stdout.ends_with("world\n")); result.stdout_check(|stdout| stdout.get(0..2).unwrap().eq(&[b'H', b'e'])); result.no_stderr(); } #[cfg(feature = "echo")] #[test] fn test_cmd_result_stderr_check_and_stderr_str_check() { let ts = TestScenario::new("echo"); let result = run_cmd(format!( "{} {} Hello world >&2", ts.bin_path.display(), ts.util_name )); result.stderr_str_check(|stderr| stderr.ends_with("world\n")); result.stderr_check(|stderr| stderr.get(0..2).unwrap().eq(&[b'H', b'e'])); result.no_stdout(); } #[cfg(feature = "echo")] #[test] #[should_panic] fn test_cmd_result_stdout_str_check_when_false_then_panics() { let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); result.stdout_str_check(str::is_empty); } #[cfg(feature = "echo")] #[test] #[should_panic] fn test_cmd_result_stdout_check_when_false_then_panics() { let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); result.stdout_check(<[u8]>::is_empty); } #[cfg(feature = "echo")] #[test] #[should_panic] fn test_cmd_result_stderr_str_check_when_false_then_panics() { let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); result.stderr_str_check(|s| !s.is_empty()); } #[cfg(feature = "echo")] #[test] #[should_panic] fn test_cmd_result_stderr_check_when_false_then_panics() { let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); result.stderr_check(|s| !s.is_empty()); } #[cfg(feature = "echo")] #[test] #[should_panic] fn test_cmd_result_stdout_check_when_predicate_panics_then_panic() { let result = TestScenario::new("echo").ucmd().run(); result.stdout_str_check(|_| panic!("Just testing")); } #[cfg(feature = "echo")] #[cfg(unix)] #[test] fn test_cmd_result_signal_when_normal_exit_then_no_signal() { let result = TestScenario::new("echo").ucmd().run(); assert!(result.signal().is_none()); } #[cfg(feature = "sleep")] #[cfg(unix)] #[test] #[should_panic = "Program must be run first or has not finished"] fn test_cmd_result_signal_when_still_running_then_panic() { let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); child .make_assertion() .is_alive() .with_current_output() .signal(); } #[cfg(feature = "sleep")] #[cfg(unix)] #[test] fn test_cmd_result_signal_when_kill_then_signal() { let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); child.kill(); child .make_assertion() .is_not_alive() .with_current_output() .signal_is(9) .signal_name_is("SIGKILL") .signal_name_is("KILL") .signal_name_is("9") .signal() .expect("Signal was none"); let result = child.wait().unwrap(); result .signal_is(9) .signal_name_is("SIGKILL") .signal_name_is("KILL") .signal_name_is("9") .signal() .expect("Signal was none"); } #[cfg(feature = "sleep")] #[cfg(unix)] #[rstest] #[case::signal_full_name_lower_case("sigkill")] #[case::signal_short_name_lower_case("kill")] #[case::signal_only_part_of_name("IGKILL")] // spell-checker: disable-line #[case::signal_just_sig("SIG")] #[case::signal_value_too_high("100")] #[case::signal_value_negative("-1")] #[should_panic = "Invalid signal name or value"] fn test_cmd_result_signal_when_invalid_signal_name_then_panic(#[case] signal_name: &str) { let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); child.kill(); let result = child.wait().unwrap(); result.signal_name_is(signal_name); } #[test] #[cfg(unix)] fn test_parse_coreutil_version() { use std::assert_eq; assert_eq!( parse_coreutil_version("id (GNU util-linux) 9.0.123-0123").to_string(), "9" ); assert_eq!( parse_coreutil_version("id (GNU util-linux) 8.32.263-0475").to_string(), "8.32" ); assert_eq!( parse_coreutil_version("id (GNU util-linux) 8.25.123-0123").to_string(), "8.25" ); assert_eq!( parse_coreutil_version("id (GNU util-linux) 9.0").to_string(), "9" ); assert_eq!( parse_coreutil_version("id (GNU util-linux) 8.32").to_string(), "8.32" ); assert_eq!( parse_coreutil_version("id (GNU util-linux) 8.25").to_string(), "8.25" ); } #[test] #[cfg(unix)] fn test_check_coreutil_version() { match check_coreutil_version("id", VERSION_MIN) { Ok(s) => assert!(s.starts_with("uutils-tests-")), Err(s) => assert!(s.starts_with("uutils-tests-warning")), }; #[cfg(target_os = "linux")] std::assert_eq!( check_coreutil_version("no test name", VERSION_MIN), Err("uutils-tests-warning: 'no test name' \ No such file or directory (os error 2)" .to_string()) ); } #[test] #[cfg(unix)] fn test_expected_result() { let ts = TestScenario::new("id"); // assert!(expected_result(&ts, &[]).is_ok()); match expected_result(&ts, &[]) { Ok(r) => assert!(r.succeeded()), Err(s) => assert!(s.starts_with("uutils-tests-warning")), } let ts = TestScenario::new("no test name"); assert!(expected_result(&ts, &[]).is_err()); } #[test] #[cfg(unix)] fn test_host_name_for() { #[cfg(target_os = "linux")] { std::assert_eq!(host_name_for("id"), "id"); std::assert_eq!(host_name_for("groups"), "groups"); std::assert_eq!(host_name_for("who"), "who"); } #[cfg(not(target_os = "linux"))] { // spell-checker:ignore (strings) ggroups gwho std::assert_eq!(host_name_for("id"), "gid"); std::assert_eq!(host_name_for("groups"), "ggroups"); std::assert_eq!(host_name_for("who"), "gwho"); std::assert_eq!(host_name_for("gid"), "gid"); std::assert_eq!(host_name_for("ggroups"), "ggroups"); std::assert_eq!(host_name_for("gwho"), "gwho"); } } #[test] #[cfg(unix)] #[cfg(feature = "whoami")] fn test_run_ucmd_as_root() { if is_ci() { println!("TEST SKIPPED (cannot run inside CI)"); } else { // Skip test if we can't guarantee non-interactive `sudo`, or if we're not "root" if let Ok(output) = Command::new("sudo") .env("LC_ALL", "C") .args(["-E", "--non-interactive", "whoami"]) .output() { if output.status.success() && String::from_utf8_lossy(&output.stdout).eq("root\n") { let ts = TestScenario::new("whoami"); std::assert_eq!( run_ucmd_as_root(&ts, &[]).unwrap().stdout_str().trim(), "root" ); } else { println!("TEST SKIPPED (we're not root)"); } } else { println!("TEST SKIPPED (cannot run sudo)"); } } } // This error was first detected when running tail so tail is used here but // should fail with any command that takes piped input. // See also https://github.com/uutils/util-linux/issues/3895 #[cfg(feature = "tail")] #[test] #[cfg_attr(not(feature = "expensive_tests"), ignore)] fn test_when_piped_input_then_no_broken_pipe() { let ts = TestScenario::new("tail"); for i in 0..10000 { dbg!(i); let test_string = "a\nb\n"; ts.ucmd() .args(&["-n", "0"]) .pipe_in(test_string) .succeeds() .no_stdout() .no_stderr(); } } #[cfg(feature = "echo")] #[test] fn test_uchild_when_run_with_a_non_blocking_util() { let ts = TestScenario::new("echo"); ts.ucmd() .arg("hello world") .run() .success() .stdout_only("hello world\n"); } // Test basically that most of the methods of UChild are working #[cfg(feature = "echo")] #[test] fn test_uchild_when_run_no_wait_with_a_non_blocking_util() { let ts = TestScenario::new("echo"); let mut child = ts.ucmd().arg("hello world").run_no_wait(); // check `child.is_alive()` and `child.delay()` is working let mut trials = 10; while child.is_alive() { assert!( trials > 0, "Assertion failed: child process is still alive." ); child.delay(500); trials -= 1; } assert!(!child.is_alive()); // check `child.is_not_alive()` is working assert!(child.is_not_alive()); // check the current output is correct std::assert_eq!(child.stdout(), "hello world\n"); assert!(child.stderr().is_empty()); // check the current output of echo is empty. We already called `child.stdout()` and `echo` // exited so there's no additional output after the first call of `child.stdout()` assert!(child.stdout().is_empty()); assert!(child.stderr().is_empty()); // check that we're still able to access all output of the child process, even after exit // and call to `child.stdout()` std::assert_eq!(child.stdout_all(), "hello world\n"); assert!(child.stderr_all().is_empty()); // we should be able to call kill without panics, even if the process already exited child.make_assertion().is_not_alive(); child.kill(); // we should be able to call wait without panics and apply some assertions child.wait().unwrap().code_is(0).no_stdout().no_stderr(); } #[cfg(feature = "cat")] #[test] fn test_uchild_when_pipe_in() { let ts = TestScenario::new("cat"); let mut child = ts.ucmd().set_stdin(Stdio::piped()).run_no_wait(); child.pipe_in("content"); child.wait().unwrap().stdout_only("content").success(); ts.ucmd().pipe_in("content").run().stdout_is("content"); } #[cfg(feature = "rm")] #[test] fn test_uchild_when_run_no_wait_with_a_blocking_command() { let ts = TestScenario::new("rm"); let at = &ts.fixtures; at.mkdir("a"); at.touch("a/empty"); #[cfg(target_vendor = "apple")] let delay: u64 = 2000; #[cfg(not(target_vendor = "apple"))] let delay: u64 = 1000; let yes = if cfg!(windows) { "y\r\n" } else { "y\n" }; let mut child = ts .ucmd() .set_stdin(Stdio::piped()) .stderr_to_stdout() .args(&["-riv", "a"]) .run_no_wait(); child .make_assertion_with_delay(delay) .is_alive() .with_current_output() .stdout_is("rm: descend into directory 'a'? "); #[cfg(windows)] let expected = "rm: descend into directory 'a'? \ rm: remove regular empty file 'a\\empty'? "; #[cfg(unix)] let expected = "rm: descend into directory 'a'? \ rm: remove regular empty file 'a/empty'? "; child.write_in(yes); child .make_assertion_with_delay(delay) .is_alive() .with_all_output() .stdout_is(expected); #[cfg(windows)] let expected = "removed 'a\\empty'\nrm: remove directory 'a'? "; #[cfg(unix)] let expected = "removed 'a/empty'\nrm: remove directory 'a'? "; child .write_in(yes) .make_assertion_with_delay(delay) .is_alive() .with_exact_output(44, 0) .stdout_only(expected); let expected = "removed directory 'a'\n"; child.write_in(yes); child.wait().unwrap().stdout_only(expected).success(); } #[cfg(feature = "tail")] #[test] fn test_uchild_when_run_with_stderr_to_stdout() { let ts = TestScenario::new("tail"); let at = &ts.fixtures; at.write("data", "file data\n"); let expected_stdout = "==> data <==\n\ file data\n\ tail: cannot open 'missing' for reading: No such file or directory\n"; ts.ucmd() .args(&["data", "missing"]) .stderr_to_stdout() .fails() .stdout_only(expected_stdout); } #[cfg(feature = "cat")] #[cfg(unix)] #[test] fn test_uchild_when_no_capture_reading_from_infinite_source() { use regex::Regex; let ts = TestScenario::new("cat"); let expected_stdout = b"\0".repeat(12345); let mut child = ts .ucmd() .set_stdin(Stdio::from(File::open("/dev/zero").unwrap())) .set_stdout(Stdio::piped()) .run_no_wait(); child .make_assertion() .with_exact_output(12345, 0) .stdout_only_bytes(expected_stdout); child .kill() .make_assertion() .with_current_output() .stdout_matches(&Regex::new("[\0].*").unwrap()) .no_stderr(); } #[cfg(feature = "sleep")] #[test] fn test_uchild_when_wait_and_timeout_is_reached_then_timeout_error() { let ts = TestScenario::new("sleep"); let child = ts .ucmd() .timeout(Duration::from_secs(1)) .arg("10.0") .run_no_wait(); match child.wait() { Err(error) if error.kind() == io::ErrorKind::Other => { std::assert_eq!(error.to_string(), "wait: Timeout of '1s' reached"); } Err(error) => panic!("Assertion failed: Expected error with timeout but was: {error}"), Ok(_) => panic!("Assertion failed: Expected timeout of `wait`."), } } #[cfg(feature = "sleep")] #[rstest] #[timeout(Duration::from_secs(5))] fn test_uchild_when_kill_and_timeout_higher_than_kill_time_then_no_panic() { let ts = TestScenario::new("sleep"); let mut child = ts .ucmd() .timeout(Duration::from_secs(60)) .arg("20.0") .run_no_wait(); child.kill().make_assertion().is_not_alive(); } #[cfg(feature = "sleep")] #[test] fn test_uchild_when_try_kill_and_timeout_is_reached_then_error() { let ts = TestScenario::new("sleep"); let mut child = ts.ucmd().timeout(Duration::ZERO).arg("10.0").run_no_wait(); match child.try_kill() { Err(error) if error.kind() == io::ErrorKind::Other => { std::assert_eq!(error.to_string(), "kill: Timeout of '0s' reached"); } Err(error) => panic!("Assertion failed: Expected error with timeout but was: {error}"), Ok(()) => panic!("Assertion failed: Expected timeout of `try_kill`."), } } #[cfg(feature = "sleep")] #[test] #[should_panic = "kill: Timeout of '0s' reached"] fn test_uchild_when_kill_with_timeout_and_timeout_is_reached_then_panic() { let ts = TestScenario::new("sleep"); let mut child = ts.ucmd().timeout(Duration::ZERO).arg("10.0").run_no_wait(); child.kill(); panic!("Assertion failed: Expected timeout of `kill`."); } #[cfg(feature = "sleep")] #[test] #[should_panic(expected = "wait: Timeout of '1.1s' reached")] fn test_ucommand_when_run_with_timeout_and_timeout_is_reached_then_panic() { let ts = TestScenario::new("sleep"); ts.ucmd() .timeout(Duration::from_millis(1100)) .arg("10.0") .run(); panic!("Assertion failed: Expected timeout of `run`.") } #[cfg(feature = "sleep")] #[rstest] #[timeout(Duration::from_secs(10))] fn test_ucommand_when_run_with_timeout_higher_then_execution_time_then_no_panic() { let ts = TestScenario::new("sleep"); ts.ucmd().timeout(Duration::from_secs(60)).arg("1.0").run(); } #[cfg(feature = "echo")] #[test] fn test_ucommand_when_default() { let shell_cmd = format!("{TESTS_BINARY} echo -n hello"); let mut command = UCommand::new(); command.arg(&shell_cmd).succeeds().stdout_is("hello"); #[cfg(target_os = "android")] let (expected_bin, expected_arg) = (PathBuf::from("/system/bin/sh"), OsString::from("-c")); #[cfg(all(unix, not(target_os = "android")))] let (expected_bin, expected_arg) = (PathBuf::from("/bin/sh"), OsString::from("-c")); #[cfg(windows)] let (expected_bin, expected_arg) = (PathBuf::from("cmd"), OsString::from("/C")); std::assert_eq!(&expected_bin, command.bin_path.as_ref().unwrap()); assert!(command.util_name.is_none()); std::assert_eq!(command.args, &[expected_arg, OsString::from(&shell_cmd)]); assert!(command.tmpd.is_some()); } #[cfg(feature = "echo")] #[test] fn test_ucommand_with_util() { let tmpd = tempfile::tempdir().unwrap(); let mut command = UCommand::with_util("echo", Rc::new(tmpd)); command .args(&["-n", "hello"]) .succeeds() .stdout_only("hello"); std::assert_eq!( &PathBuf::from(TESTS_BINARY), command.bin_path.as_ref().unwrap() ); std::assert_eq!("echo", &command.util_name.unwrap()); std::assert_eq!( &[ OsString::from("echo"), OsString::from("-n"), OsString::from("hello") ], command.args.make_contiguous() ); assert!(command.tmpd.is_some()); } #[cfg(all(unix, not(target_os = "macos")))] #[test] fn test_compare_xattrs() { use tempfile::tempdir; let temp_dir = tempdir().unwrap(); let file_path1 = temp_dir.path().join("test_file1.txt"); let file_path2 = temp_dir.path().join("test_file2.txt"); File::create(&file_path1).unwrap(); File::create(&file_path2).unwrap(); let test_attr = "user.test_attr"; let test_value = b"test value"; xattr::set(&file_path1, test_attr, test_value).unwrap(); assert!(!compare_xattrs(&file_path1, &file_path2)); xattr::set(&file_path2, test_attr, test_value).unwrap(); assert!(compare_xattrs(&file_path1, &file_path2)); } }