#![allow(dead_code)] use std::env; use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{Read, Result, Write}; #[cfg(unix)] use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file}; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::rc::Rc; use std::str::from_utf8; use std::thread::sleep; use std::time::Duration; use tempfile::TempDir; #[cfg(windows)] static PROGNAME: &str = concat!(env!("CARGO_PKG_NAME"), ".exe"); #[cfg(not(windows))] static PROGNAME: &str = env!("CARGO_PKG_NAME"); 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."; /// Test if the program is running under CI pub fn is_ci() -> bool { std::env::var("CI") .unwrap_or(String::from("false")) .eq_ignore_ascii_case("true") } /// Test if the program is running under WSL // ref: @@ // ToDO: test on WSL2 which likely doesn't need special handling; plan change to `is_wsl_1()` if WSL2 is less needy pub fn is_wsl() -> bool { #[cfg(target_os = "linux")] { if let Ok(b) = std::fs::read("/proc/sys/kernel/osrelease") { if let Ok(s) = std::str::from_utf8(&b) { let a = s.to_ascii_lowercase(); return a.contains("microsoft") || a.contains("wsl"); } } } false } fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> String { let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); AtPath::new(tmpdir_path).read(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)] pub struct CmdResult { //tmpd is used for convenience functions for asserts against fixtures tmpd: Option>, /// exit status for command (if there is one) pub code: Option, /// zero-exit from running the Command? /// see [`success`] pub success: bool, /// captured utf-8 standard output after running the Command pub stdout: String, /// captured utf-8 standard error after running the Command pub stderr: String, } impl CmdResult { /// asserts that the command resulted in a success (zero) status code pub fn success(&self) -> Box<&CmdResult> { assert!(self.success); Box::new(self) } /// asserts that the command resulted in a failure (non-zero) status code pub fn failure(&self) -> Box<&CmdResult> { assert!(!self.success); Box::new(self) } /// asserts that the command's exit code is the same as the given one pub fn status_code(&self, code: i32) -> Box<&CmdResult> { assert!(self.code == Some(code)); Box::new(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 pub fn no_stderr(&self) -> Box<&CmdResult> { assert_eq!(self.stderr, ""); Box::new(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 pub fn no_stdout(&self) -> Box<&CmdResult> { assert_eq!(self.stdout, ""); Box::new(self) } /// 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 pub fn stdout_is>(&self, msg: T) -> Box<&CmdResult> { assert_eq!(self.stdout, String::from(msg.as_ref())); Box::new(self) } /// like stdout_is(...), but expects the contents of the file at the provided relative path pub fn stdout_is_fixture>(&self, file_rel_path: T) -> Box<&CmdResult> { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stdout_is(contents) } /// like stdout_is_fixture(...), but replaces the data in fixture file based on values provided in template_vars /// command output pub fn stdout_is_templated_fixture>(&self, file_rel_path: T, template_vars: Vec<(String, String)>) -> Box<&CmdResult> { let mut contents = read_scenario_fixture(&self.tmpd, file_rel_path); for kv in template_vars { contents = contents.replace(&kv.0, &kv.1); } self.stdout_is(contents) } /// asserts that the command resulted in stderr stream output that equals the /// passed in value, when both are trimmed of trailing whitespace /// stderr_only is a better choice unless stdout may or will be non-empty pub fn stderr_is>(&self, msg: T) -> Box<&CmdResult> { assert_eq!( self.stderr.trim_end(), String::from(msg.as_ref()).trim_end() ); Box::new(self) } /// asserts that /// 1. the command resulted in stdout stream output that equals the /// passed in value, when both are trimmed of trailing whitespace /// and 2. the command resulted in empty (zero-length) stderr stream output pub fn stdout_only>(&self, msg: T) -> Box<&CmdResult> { self.no_stderr().stdout_is(msg) } /// like stdout_only(...), but expects the contents of the file at the provided relative path pub fn stdout_only_fixture>(&self, file_rel_path: T) -> Box<&CmdResult> { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stdout_only(contents) } /// asserts that /// 1. the command resulted in stderr stream output that equals the /// passed in value, when both are trimmed of trailing whitespace /// and 2. the command resulted in empty (zero-length) stdout stream output pub fn stderr_only>(&self, msg: T) -> Box<&CmdResult> { self.no_stdout().stderr_is(msg) } pub fn fails_silently(&self) -> Box<&CmdResult> { assert!(!self.success); assert_eq!(self.stderr, ""); Box::new(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 { "/" } } /// 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) -> AtPath { AtPath { subdir: PathBuf::from(subdir), } } pub fn as_string(&self) -> String { self.subdir.to_str().unwrap().to_owned() } pub fn plus(&self, name: &str) -> PathBuf { let mut pathbuf = self.subdir.clone(); pathbuf.push(name); pathbuf } pub fn plus_as_string(&self, name: &str) -> String { String::from(self.plus(name).to_str().unwrap()) } 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 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(); let _ = f.read_to_string(&mut contents); contents } pub fn write(&self, name: &str, contents: &str) { log_info("open(write)", self.plus_as_string(name)); let _ = std::fs::write(self.plus(name), contents); } pub fn append(&self, name: &str, contents: &str) { log_info("open(append)", self.plus_as_string(name)); let mut f = OpenOptions::new() .write(true) .append(true) .open(self.plus(name)) .unwrap(); let _ = f.write(contents.as_bytes()); } pub fn mkdir(&self, dir: &str) { 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: &str) { log_info("touch", self.plus_as_string(file)); File::create(&self.plus(file)).unwrap(); } pub fn symlink_file(&self, src: &str, dst: &str) { log_info( "symlink", &format!("{},{}", self.plus_as_string(src), self.plus_as_string(dst)), ); symlink_file(&self.plus(src), &self.plus(dst)).unwrap(); } pub fn symlink_dir(&self, src: &str, dst: &str) { log_info( "symlink", &format!("{},{}", self.plus_as_string(src), self.plus_as_string(dst)), ); symlink_dir(&self.plus(src), &self.plus(dst)).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(_) => "".to_string(), } } 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: &str) -> bool { match fs::metadata(&self.plus(path)) { Ok(m) => m.is_file(), 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 s.starts_with(prefix) { String::from(&s[prefix.len()..]) } else { s } } } /// 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 { bin_path: PathBuf, util_name: String, pub fixtures: AtPath, tmpd: Rc, } impl TestScenario { pub fn new(util_name: &str) -> TestScenario { let tmpd = Rc::new(TempDir::new().unwrap()); let ts = TestScenario { bin_path: { // Instead of hardcoding the path relative to the current // directory, use Cargo's OUT_DIR to find path to executable. // This allows tests to be run using profiles other than debug. let target_dir = path_concat!(env!("OUT_DIR"), "..", "..", "..", PROGNAME); PathBuf::from(AtPath::new(Path::new(&target_dir)).root_dir_resolved()) }, util_name: String::from(util_name), fixtures: AtPath::new(tmpd.as_ref().path()), tmpd: 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); 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 { let mut cmd = self.cmd(&self.bin_path); cmd.arg(&self.util_name); cmd } /// Returns builder for invoking any system command. Paths given are treated /// relative to the environment's unique temporary test directory. pub fn cmd>(&self, bin: S) -> UCommand { UCommand::new_from_tmp(bin, self.tmpd.clone(), true) } // different names are used rather than an argument // because the need to keep the environment is exceedingly rare. pub fn ucmd_keepenv(&self) -> UCommand { let mut cmd = self.cmd_keepenv(&self.bin_path); cmd.arg(&self.util_name); cmd } pub fn cmd_keepenv>(&self, bin: S) -> UCommand { UCommand::new_from_tmp(bin, self.tmpd.clone(), false) } } /// A `UCommand` is a wrapper around an individual Command that provides several additional features /// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command /// and asserting on the results. /// 2. it tracks arguments provided so that in test cases which may provide variations of an arg in loops /// the test failure can display the exact call which preceded an assertion failure. /// 3. it provides convenience construction arguments to set the Command working directory and/or clear its environment. #[derive(Debug)] pub struct UCommand { pub raw: Command, comm_string: String, tmpd: Option>, has_run: bool, stdin: Option>, } impl UCommand { pub fn new, U: AsRef>(arg: T, curdir: U, env_clear: bool) -> UCommand { UCommand { tmpd: None, has_run: false, raw: { let mut cmd = Command::new(arg.as_ref()); cmd.current_dir(curdir.as_ref()); if env_clear { if cfg!(windows) { // %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" for (key, _) in env::vars_os() { if key.as_os_str() != "SYSTEMROOT" { cmd.env_remove(key); } } } else { cmd.env_clear(); } } cmd }, comm_string: String::from(arg.as_ref().to_str().unwrap()), stdin: None, } } pub fn new_from_tmp>(arg: T, tmpd: Rc, env_clear: bool) -> UCommand { let tmpd_path_buf = String::from(&(*tmpd.as_ref().path().to_str().unwrap())); let mut ucmd: UCommand = UCommand::new(arg.as_ref(), tmpd_path_buf, env_clear); ucmd.tmpd = Some(tmpd); ucmd } /// Add a parameter to the invocation. Path arguments are treated relative /// to the test environment directory. pub fn arg>(&mut self, arg: S) -> Box<&mut UCommand> { if self.has_run { panic!(ALREADY_RUN); } self.comm_string.push_str(" "); self.comm_string.push_str(arg.as_ref().to_str().unwrap()); self.raw.arg(arg.as_ref()); Box::new(self) } /// Add multiple parameters to the invocation. Path arguments are treated relative /// to the test environment directory. pub fn args>(&mut self, args: &[S]) -> Box<&mut UCommand> { if self.has_run { panic!(MULTIPLE_STDIN_MEANINGLESS); } for s in args { self.comm_string.push_str(" "); self.comm_string.push_str(s.as_ref().to_str().unwrap()); } self.raw.args(args.as_ref()); Box::new(self) } /// provides stdinput to feed in to the command when spawned pub fn pipe_in>>(&mut self, input: T) -> Box<&mut UCommand> { if self.stdin.is_some() { panic!(MULTIPLE_STDIN_MEANINGLESS); } self.stdin = Some(input.into()); Box::new(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) -> Box<&mut UCommand> { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.pipe_in(contents) } pub fn env(&mut self, key: K, val: V) -> Box<&mut UCommand> where K: AsRef, V: AsRef, { if self.has_run { panic!(ALREADY_RUN); } self.raw.env(key, val); Box::new(self) } /// Spawns the command, feeds the stdin if any, and returns the /// child process immediately. pub fn run_no_wait(&mut self) -> Child { if self.has_run { panic!(ALREADY_RUN); } self.has_run = true; log_info("run", &self.comm_string); let mut result = self .raw .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); if let Some(ref input) = self.stdin { result .stdin .take() .unwrap_or_else(|| panic!("Could not take child process stdin")) .write_all(input) .unwrap_or_else(|e| panic!("{}", e)); } result } /// 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 { let prog = self.run_no_wait().wait_with_output().unwrap(); CmdResult { tmpd: self.tmpd.clone(), code: prog.status.code(), success: prog.status.success(), stdout: from_utf8(&prog.stdout).unwrap().to_string(), stderr: from_utf8(&prog.stderr).unwrap().to_string(), } } /// 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. 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. 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!("{}/{}", tmpdir_path.to_str().unwrap(), file_rel_path) } } pub fn read_size(child: &mut Child, size: usize) -> String { let mut output = Vec::new(); output.resize(size, 0); sleep(Duration::from_secs(1)); child .stdout .as_mut() .unwrap() .read(output.as_mut_slice()) .unwrap(); String::from_utf8(output).unwrap() }