diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index fd1143f05..fc1665efc 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -14,7 +14,7 @@ fn test_utf8() { .args(&["-lwmcL"]) .pipe_in_fixture("UTF_8_test.txt") .run() - .stdout_is(" 0 0 0 0 0\n"); + .stdout_is(" 300 4969 22781 22213 79\n"); // GNU returns " 300 2086 22219 22781 79" // TODO: we should fix that to match GNU's behavior } diff --git a/tests/common/util.rs b/tests/common/util.rs index d33b1943d..2cee36267 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -53,9 +53,10 @@ pub fn is_wsl() -> bool { false } -fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> String { +/// 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(file_rel_path.as_ref().to_str().unwrap()) + 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) @@ -69,29 +70,95 @@ pub struct CmdResult { /// zero-exit from running the Command? /// see [`success`] pub success: bool, - /// captured utf-8 standard output after running the Command + /// captured standard output after running the Command pub stdout: String, - /// captured utf-8 standard error after running the Command + /// captured standard error after running the Command pub stderr: String, } impl CmdResult { + /// Returns a reference to the program's standard output as a slice of bytes + pub fn stdout(&self) -> &[u8] { + &self.stdout.as_bytes() + } + + /// Returns the program's standard output as a string slice + pub fn stdout_str(&self) -> &str { + &self.stdout + } + + /// Returns the program's standard output as a string + /// consumes self + pub fn stdout_move_str(self) -> String { + self.stdout + } + + /// Returns the program's standard output as a vec of bytes + /// consumes self + pub fn stdout_move_bytes(self) -> Vec { + Vec::from(self.stdout) + } + + /// Returns a reference to the program's standard error as a slice of bytes + pub fn stderr(&self) -> &[u8] { + &self.stderr.as_bytes() + } + + /// Returns the program's standard error as a string slice + pub fn stderr_str(&self) -> &str { + &self.stderr + } + + /// Returns the program's standard error as a string + /// consumes self + pub fn stderr_move_str(self) -> String { + self.stderr + } + + /// Returns the program's standard error as a vec of bytes + /// consumes self + pub fn stderr_move_bytes(self) -> Vec { + Vec::from(self.stderr) + } + + /// Returns the program's exit code + /// Panics if not run + pub fn code(&self) -> i32 { + self.code.expect("Program must be run first") + } + + /// 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.success + } + /// asserts that the command resulted in a success (zero) status code - pub fn success(&self) -> Box<&CmdResult> { + pub fn success(&self) -> &CmdResult { assert!(self.success); - Box::new(self) + self } /// asserts that the command resulted in a failure (non-zero) status code - pub fn failure(&self) -> Box<&CmdResult> { + pub fn failure(&self) -> &CmdResult { assert!(!self.success); - Box::new(self) + self } /// asserts that the command's exit code is the same as the given one - pub fn status_code(&self, code: i32) -> Box<&CmdResult> { + pub fn status_code(&self, code: i32) -> &CmdResult { assert!(self.code == Some(code)); - Box::new(self) + self } /// asserts that the command resulted in empty (zero-length) stderr stream output @@ -99,9 +166,9 @@ impl CmdResult { /// 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) + pub fn no_stderr(&self) -> &CmdResult { + assert!(self.stderr.is_empty()); + self } /// asserts that the command resulted in empty (zero-length) stderr stream output @@ -110,62 +177,102 @@ impl CmdResult { /// 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) + pub fn no_stdout(&self) -> &CmdResult { + assert!(self.stdout.is_empty()); + 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> { + pub fn stdout_is>(&self, msg: T) -> &CmdResult { assert_eq!(self.stdout, String::from(msg.as_ref())); - Box::new(self) + self + } + + /// asserts that the command resulted in stdout stream output, + /// whose bytes equal those of the passed in slice + pub fn stdout_is_bytes>(&self, msg: T) -> &CmdResult { + assert_eq!(self.stdout.as_bytes(), msg.as_ref()); + 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> { + pub fn stdout_is_fixture>(&self, file_rel_path: T) -> &CmdResult { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_is(contents) + self.stdout_is_bytes(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> { + pub fn stderr_is>(&self, msg: T) -> &CmdResult { assert_eq!( self.stderr.trim_end(), String::from(msg.as_ref()).trim_end() ); - Box::new(self) + self + } + + /// asserts that the command resulted in stderr stream output, + /// whose bytes equal those of the passed in slice + pub fn stderr_is_bytes>(&self, msg: T) -> &CmdResult { + assert_eq!(self.stderr.as_bytes(), msg.as_ref()); + 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> { + pub fn stdout_only>(&self, msg: T) -> &CmdResult { 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 + pub fn stdout_only_bytes>(&self, msg: T) -> &CmdResult { + self.no_stderr().stdout_is_bytes(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> { + pub fn stdout_only_fixture>(&self, file_rel_path: T) -> &CmdResult { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_only(contents) + self.stdout_only_bytes(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> { + pub fn stderr_only>(&self, msg: T) -> &CmdResult { self.no_stdout().stderr_is(msg) } - pub fn fails_silently(&self) -> Box<&CmdResult> { + /// 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 + pub fn stderr_only_bytes>(&self, msg: T) -> &CmdResult { + self.no_stderr().stderr_is_bytes(msg) + } + + pub fn fails_silently(&self) -> &CmdResult { assert!(!self.success); - assert_eq!(self.stderr, ""); - Box::new(self) + assert!(self.stderr.is_empty()); + self + } + + pub fn stdout_contains>(&self, cmp: T) -> &CmdResult { + assert!(self.stdout_str().contains(cmp.as_ref())); + self + } + + pub fn stderr_contains>(&self, cmp: &T) -> &CmdResult { + assert!(self.stderr_str().contains(cmp.as_ref())); + self } } @@ -255,11 +362,25 @@ impl AtPath { 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("open(write)", self.plus_as_string(name)); let _ = std::fs::write(self.plus(name), contents); } + pub fn write_bytes(&self, name: &str, contents: &[u8]) { + log_info("open(write)", 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("open(append)", self.plus_as_string(name)); let mut f = OpenOptions::new() @@ -270,6 +391,17 @@ impl AtPath { let _ = f.write(contents.as_bytes()); } + pub fn append_bytes(&self, name: &str, contents: &[u8]) { + log_info("open(append)", self.plus_as_string(name)); + let mut f = OpenOptions::new() + .write(true) + .append(true) + .open(self.plus(name)) + .unwrap(); + f.write_all(contents) + .unwrap_or_else(|e| panic!("Couldn't append to {}: {}", name, e)); + } + pub fn mkdir(&self, dir: &str) { log_info("mkdir", self.plus_as_string(dir)); fs::create_dir(&self.plus(dir)).unwrap(); @@ -521,19 +653,19 @@ impl UCommand { /// Add a parameter to the invocation. Path arguments are treated relative /// to the test environment directory. - pub fn arg>(&mut self, arg: S) -> Box<&mut UCommand> { + pub fn arg>(&mut self, arg: S) -> &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) + 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> { + pub fn args>(&mut self, args: &[S]) -> &mut UCommand { if self.has_run { panic!(MULTIPLE_STDIN_MEANINGLESS); } @@ -543,25 +675,25 @@ impl UCommand { } self.raw.args(args.as_ref()); - Box::new(self) + self } /// provides stdinput to feed in to the command when spawned - pub fn pipe_in>>(&mut self, input: T) -> Box<&mut UCommand> { + pub fn pipe_in>>(&mut self, input: T) -> &mut UCommand { if self.stdin.is_some() { panic!(MULTIPLE_STDIN_MEANINGLESS); } self.stdin = Some(input.into()); - Box::new(self) + 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> { + pub fn pipe_in_fixture>(&mut self, file_rel_path: S) -> &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> + pub fn env(&mut self, key: K, val: V) -> &mut UCommand where K: AsRef, V: AsRef, @@ -570,7 +702,7 @@ impl UCommand { panic!(ALREADY_RUN); } self.raw.env(key, val); - Box::new(self) + self } /// Spawns the command, feeds the stdin if any, and returns the