From f7d67160439d7f4e3491800369331d1382486494 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 23 Mar 2018 21:41:59 -0600 Subject: [PATCH 1/4] feat(cmd): Augment process::Command This is an experiment in trying to use extension traits rather than wrapping `process::Command`. This both makes it more extensible (can interop with other crates) and able to be adapted to other "Command" crates like `duct`. `cli_test_dir` has something like `CommandStdInExt` called `CommandExt`. Differences include: - Scoped name since traits generally are pulled out of any namespace they are in. - Preserves the command and `stdin` to for richer error reporting. --- src/cmd.rs | 367 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + 2 files changed, 370 insertions(+) create mode 100644 src/cmd.rs diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 0000000..1e4e54f --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,367 @@ +use std::ffi; +use std::fmt; +use std::io::Write; +use std::io; +use std::process; +use std::str; + +use failure; + +/// Extend `Command` with helpers for running the current crate's binaries. +pub trait CommandCargoExt { + /// Create a `Command` to run the crate's main binary. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::main_binary() + /// .output() + /// .unwrap(); + /// ``` + fn main_binary() -> Self; + + /// Create a `Command` Run a specific binary of the current crate. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::cargo_binary("assert_fixture") + /// .output() + /// .unwrap(); + /// ``` + fn cargo_binary>(name: S) -> Self; +} + +impl CommandCargoExt for process::Command { + fn main_binary() -> Self { + let mut cmd = process::Command::new("carg"); + cmd.arg("run").arg("--quit").arg("--"); + cmd + } + + fn cargo_binary>(name: S) -> Self { + let mut cmd = process::Command::new("carg"); + cmd.arg("run") + .arg("--quit") + .arg("--bin") + .arg(name.as_ref()) + .arg("--"); + cmd + } +} + +/// Extend `Command` with a helper to pass a buffer to `stdin` +pub trait CommandStdInExt { + /// Write `buffer` to `stdin` when the command is run. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::new("cat") + /// .with_stdin("42") + /// .unwrap(); + /// ``` + fn with_stdin(self, buffer: S) -> StdInCommand + where + S: Into>; +} + +impl CommandStdInExt for process::Command { + fn with_stdin(self, buffer: S) -> StdInCommand + where + S: Into>, + { + StdInCommand { + cmd: self, + stdin: buffer.into(), + } + } +} + +/// `std::process::Command` with a `stdin` buffer. +pub struct StdInCommand { + cmd: process::Command, + stdin: Vec, +} + +impl StdInCommand { + /// Executes the command as a child process, waiting for it to finish and collecting all of its + /// output. + /// + /// By default, stdout and stderr are captured (and used to provide the resulting output). + /// Stdin is not inherited from the parent and any attempt by the child process to read from + /// the stdin stream will result in the stream immediately closing. + /// + /// *(mirrors `std::process::Command::output`** + pub fn output(&mut self) -> io::Result { + self.spawn()?.wait_with_output() + } + + /// Executes the command as a child process, returning a handle to it. + /// + /// By default, stdin, stdout and stderr are inherited from the parent. + /// + /// *(mirrors `std::process::Command::spawn`** + fn spawn(&mut self) -> io::Result { + // stdout/stderr should only be piped for `output` according to `process::Command::new`. + self.cmd.stdin(process::Stdio::piped()); + self.cmd.stdout(process::Stdio::piped()); + self.cmd.stderr(process::Stdio::piped()); + + let mut spawned = self.cmd.spawn()?; + + spawned + .stdin + .as_mut() + .expect("Couldn't get mut ref to command stdin") + .write_all(&self.stdin)?; + Ok(spawned) + } +} + +/// `std::process::Output` represented as a `Result`. +pub type OutputResult = Result; + +/// Extends `std::process::Output` with methods to to convert it to an `OutputResult`. +pub trait OutputOkExt +where + Self: ::std::marker::Sized, +{ + /// Convert an `std::process::Output` into an `OutputResult`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::new("echo") + /// .args(&["42"]) + /// .output() + /// .ok() + /// .unwrap(); + /// ``` + fn ok(self) -> OutputResult; + + /// Unwrap a `std::process::Output` but with a prettier message than `.ok().unwrap()`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::new("echo") + /// .args(&["42"]) + /// .output() + /// .unwrap(); + /// ``` + fn unwrap(self) { + if let Err(err) = self.ok() { + panic!("{}", err); + } + } +} + +impl OutputOkExt for process::Output { + /// Convert an `std::process::Output` into an `OutputResult`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::new("echo") + /// .args(&["42"]) + /// .output() + /// .ok() + /// .unwrap(); + /// ``` + fn ok(self) -> OutputResult { + if self.status.success() { + Ok(self) + } else { + let error = OutputError::new(self); + Err(error) + } + } +} + +impl<'c> OutputOkExt for &'c mut process::Command { + /// Convert an `std::process::Command` into an `OutputResult`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::new("echo") + /// .args(&["42"]) + /// .ok() + /// .unwrap(); + /// ``` + fn ok(self) -> OutputResult { + let output = self.output().map_err(|e| OutputError::with_cause(e))?; + if output.status.success() { + Ok(output) + } else { + let error = OutputError::new(output).set_cmd(format!("{:?}", self)); + Err(error) + } + } +} + +impl<'c> OutputOkExt for &'c mut StdInCommand { + /// Convert an `std::process::Command` into an `OutputResult`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::new("cat") + /// .with_stdin("42") + /// .ok() + /// .unwrap(); + /// ``` + fn ok(self) -> OutputResult { + let output = self.output().map_err(|e| OutputError::with_cause(e))?; + if output.status.success() { + Ok(output) + } else { + let error = OutputError::new(output) + .set_cmd(format!("{:?}", self.cmd)) + .set_stdin(self.stdin.clone()); + Err(error) + } + } +} + +#[derive(Fail, Debug)] +struct Output { + output: process::Output, +} + +impl fmt::Display for Output { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(code) = self.output.status.code() { + writeln!(f, "code={}", code)?; + } else { + writeln!(f, "code=")?; + } + if let Ok(stdout) = str::from_utf8(&self.output.stdout) { + writeln!(f, "stdout=```{}```", stdout)?; + } else { + writeln!(f, "stdout=```{:?}```", self.output.stdout)?; + } + if let Ok(stderr) = str::from_utf8(&self.output.stderr) { + writeln!(f, "stderr=```{}```", stderr)?; + } else { + writeln!(f, "stderr=```{:?}```", self.output.stderr)?; + } + + Ok(()) + } +} + +#[derive(Debug)] +enum OutputCause { + Expected(Output), + Unexpected(failure::Error), +} + +impl fmt::Display for OutputCause { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + OutputCause::Expected(ref e) => write!(f, "{}", e), + OutputCause::Unexpected(ref e) => write!(f, "{}", e), + } + } +} + +/// `std::process::Output` as a `Fail`. +#[derive(Fail, Debug)] +pub struct OutputError { + cmd: Option, + stdin: Option>, + cause: OutputCause, +} + +impl OutputError { + /// Convert `std::process::Output` into a `Fail`. + pub fn new(output: process::Output) -> Self { + Self { + cmd: None, + stdin: None, + cause: OutputCause::Expected(Output { output }), + } + } + + /// For errors that happen in creating a `std::process::Output`. + pub fn with_cause(cause: E) -> Self + where + E: Into, + { + Self { + cmd: None, + stdin: None, + cause: OutputCause::Unexpected(cause.into()), + } + } + + /// Add the command line for additional context. + pub fn set_cmd(mut self, cmd: String) -> Self { + self.cmd = Some(cmd); + self + } + + /// Add the `stdn` for additional context. + pub fn set_stdin(mut self, stdin: Vec) -> Self { + self.stdin = Some(stdin); + self + } + + /// Access the contained `std::process::Output`. + pub fn as_output(&self) -> Option<&process::Output> { + match self.cause { + OutputCause::Expected(ref e) => Some(&e.output), + OutputCause::Unexpected(_) => None, + } + } +} + +impl fmt::Display for OutputError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref cmd) = self.cmd { + writeln!(f, "command=`{}`", cmd)?; + } + if let Some(ref stdin) = self.stdin { + if let Ok(stdin) = str::from_utf8(&stdin) { + writeln!(f, "stdin=```{}```", stdin)?; + } else { + writeln!(f, "stdin=```{:?}```", stdin)?; + } + } + write!(f, "{}", self.cause) + } +} diff --git a/src/lib.rs b/src/lib.rs index 9b055b4..808e978 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -137,6 +137,9 @@ mod assert; mod diff; mod output; +/// `std::process::Command` extensions. +pub mod cmd; + pub use assert::Assert; pub use assert::OutputAssertionBuilder; /// Environment is a re-export of the Environment crate From 97673016cedbcd47b24a022f7925d0717ec88eed Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 27 Mar 2018 17:00:31 -0600 Subject: [PATCH 2/4] feat(tmpdir): Augment tempdir::TempDir This is an experiment in what kind of tempdir operations a holistic CLI testing framework might provide, following on the previous experiments with extension traits. The exact structure in this crate or across crates is TBD. This crate extends `TempDir` with the following - In TempDir or a child path, run a command. - On child path, touch a file. - On child path, write a binary blob or str to file. - Copy to a TempDir or a child path some files. Some other potential operations include - `write_yml(serde)` - `write_json(serde)` - `write_toml(serde)` In contrast, `cli_test_dir` can: - Run a single pre-defined program within the tempdir - Write binary files to tempdir - Offer a absolute path to a child file within the crate source (so its safe to pass to the program running in the tempdir). --- Cargo.toml | 6 ++ src/lib.rs | 5 + src/temp.rs | 269 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 src/temp.rs diff --git a/Cargo.toml b/Cargo.toml index 46aa92a..7428cc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,10 @@ build = "build.rs" [[bin]] name = "assert_fixture" +[features] +default = ["tempdir"] +tempdir = ["tempfile", "globwalk"] + [dependencies] colored = "1.5" difference = "2.0" @@ -22,6 +26,8 @@ failure = "0.1" failure_derive = "0.1" serde_json = "1.0" environment = "0.1" +tempfile = { version="3.0", optional=true } +globwalk = { version="0.1", optional=true } [build-dependencies] skeptic = "0.13" diff --git a/src/lib.rs b/src/lib.rs index 808e978..8d1b58d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,7 +124,9 @@ extern crate environment; extern crate failure; #[macro_use] extern crate failure_derive; +extern crate globwalk; extern crate serde_json; +extern crate tempfile; mod errors; pub use errors::AssertionError; @@ -139,6 +141,9 @@ mod output; /// `std::process::Command` extensions. pub mod cmd; +/// `tempfile::TempDir` extensions. +#[cfg(feature = "tempdir")] +pub mod temp; pub use assert::Assert; pub use assert::OutputAssertionBuilder; diff --git a/src/temp.rs b/src/temp.rs new file mode 100644 index 0000000..a831c9d --- /dev/null +++ b/src/temp.rs @@ -0,0 +1,269 @@ +use std::ffi; +use std::fs; +use std::io; +use std::io::Write; +use std::path; +use std::process; + +use globwalk; +use tempfile; +use failure; + +// Quick and dirty for doc tests; not meant for long term use. +pub use tempfile::TempDir; + +/// Extend `TempDir` to perform operations on relative paths within the temp directory via +/// `ChildPath`. +pub trait TempDirChildExt { + /// Create a path within the temp directory. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// println!("{:?}", temp.path()); + /// println!("{:?}", temp.child("foo/bar.txt").path()); + /// temp.close().unwrap(); + /// ``` + fn child

(&self, path: P) -> ChildPath + where + P: AsRef; +} + +impl TempDirChildExt for tempfile::TempDir { + fn child

(&self, path: P) -> ChildPath + where + P: AsRef, + { + ChildPath::new(self.path().join(path.as_ref())) + } +} + +/// A path within a TempDir +pub struct ChildPath { + path: path::PathBuf, +} + +impl ChildPath { + /// Wrap a path for use with special built extension traits. + /// + /// See trait implementations or `TempDirChildExt` for more details. + pub fn new

(path: P) -> Self + where + P: Into, + { + Self { path: path.into() } + } + + /// Access the path. + pub fn path(&self) -> &path::Path { + &self.path + } +} + +/// Extend `TempDir` to run commands in it. +pub trait TempDirCommandExt { + /// Constructs a new Command for launching the program at path program, with the following + /// default configuration: + /// + /// - The current working directory is the temp dir + /// - No arguments to the program + /// - Inherit the current process's environment + /// - Inherit the current process's working directory + /// - Inherit stdin/stdout/stderr for spawn or status, but create pipes for output + /// - Builder methods are provided to change these defaults and otherwise configure the process. + /// + /// If program is not an absolute path, the PATH will be searched in an OS-defined way. + /// + /// The search path to be used may be controlled by setting the PATH environment variable on + /// the Command, but this has some implementation limitations on Windows (see + /// https://github.com/rust-lang/rust/issues/37519). + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.command("pwd").output().unwrap(); + /// temp.close().unwrap(); + /// ``` + fn command(&self, program: S) -> process::Command + where + S: AsRef; +} + +impl TempDirCommandExt for tempfile::TempDir { + fn command(&self, program: S) -> process::Command + where + S: AsRef, + { + let mut cmd = process::Command::new(program); + cmd.current_dir(self.path()); + cmd + } +} + +impl TempDirCommandExt for ChildPath { + fn command(&self, program: S) -> process::Command + where + S: AsRef, + { + let mut cmd = process::Command::new(program); + cmd.current_dir(self.path()); + cmd + } +} + +/// Extend `ChildPath` to create empty files. +pub trait ChildPathTouchExt { + /// Create an empty file at `ChildPath`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.child("foo.txt").touch().unwrap(); + /// temp.close().unwrap(); + /// ``` + fn touch(&self) -> io::Result<()>; +} + +impl ChildPathTouchExt for ChildPath { + fn touch(&self) -> io::Result<()> { + touch(self.path()) + } +} + +/// Extend `ChildPath` to write binary files. +pub trait ChildPathWriteBinExt { + /// Write a binary file at `ChildPath`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.child("foo.txt").write_binary(b"To be or not to be...").unwrap(); + /// temp.close().unwrap(); + /// ``` + fn write_binary(&self, data: &[u8]) -> io::Result<()>; +} + +impl ChildPathWriteBinExt for ChildPath { + fn write_binary(&self, data: &[u8]) -> io::Result<()> { + write_binary(self.path(), data) + } +} + +/// Extend `ChildPath` to write text files. +pub trait ChildPathWriteStrExt { + /// Write a text file at `ChildPath`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.child("foo.txt").write_str("To be or not to be...").unwrap(); + /// temp.close().unwrap(); + /// ``` + fn write_str(&self, data: &str) -> io::Result<()>; +} + +impl ChildPathWriteStrExt for ChildPath { + fn write_str(&self, data: &str) -> io::Result<()> { + write_str(self.path(), data) + } +} + +/// Extend `TempDir` to copy files into it. +pub trait TempDirCopyExt { + /// Copy files and directories into the current path from the `source` according to the glob + /// `patterns`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.copy_from(".", &["*.rs"]).unwrap(); + /// temp.close().unwrap(); + /// ``` + fn copy_from(&self, source: P, patterns: &[S]) -> Result<(), failure::Error> + where + P: AsRef, + S: AsRef; +} + +impl TempDirCopyExt for tempfile::TempDir { + fn copy_from(&self, source: P, patterns: &[S]) -> Result<(), failure::Error> + where + P: AsRef, + S: AsRef, + { + copy_from(self.path(), source.as_ref(), patterns) + } +} + +impl TempDirCopyExt for ChildPath { + fn copy_from(&self, source: P, patterns: &[S]) -> Result<(), failure::Error> + where + P: AsRef, + S: AsRef, + { + copy_from(self.path(), source.as_ref(), patterns) + } +} + +fn touch(path: &path::Path) -> io::Result<()> { + fs::File::create(path)?; + Ok(()) +} + +fn write_binary(path: &path::Path, data: &[u8]) -> io::Result<()> { + let mut file = fs::File::create(path)?; + file.write_all(data)?; + Ok(()) +} + +fn write_str(path: &path::Path, data: &str) -> io::Result<()> { + write_binary(path, data.as_bytes()) +} + +fn copy_from( + target: &path::Path, + source: &path::Path, + patterns: &[S], +) -> Result<(), failure::Error> +where + S: AsRef, +{ + for entry in globwalk::GlobWalker::from_patterns(patterns, source)?.follow_links(true) { + let entry = entry?; + let rel = entry + .path() + .strip_prefix(source) + .expect("entries to be under `source`"); + let target_path = target.join(rel); + if entry.file_type().is_dir() { + fs::create_dir_all(target_path)?; + } else if entry.file_type().is_file() { + fs::copy(entry.path(), target)?; + } + } + Ok(()) +} From 8b0f56c5207fbb235bd24184b16a3133acd0569b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 29 Mar 2018 11:35:35 -0600 Subject: [PATCH 3/4] feat(cmd): Add unwrap_err to commands Another common use case in basic assertions. --- src/cmd.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/cmd.rs b/src/cmd.rs index 1e4e54f..70ab570 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -170,9 +170,34 @@ where /// .output() /// .unwrap(); /// ``` - fn unwrap(self) { - if let Err(err) = self.ok() { - panic!("{}", err); + fn unwrap(self) -> process::Output { + match self.ok() { + Ok(output) => output, + Err(err) => panic!("{}", err), + } + } + + /// Unwrap a `std::process::Output` but with a prettier message than `.ok().unwrap()`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::new("non_existent_command") + /// .args(&["42"]) + /// .output() + /// .unwrap_err(); + /// ``` + fn unwrap_err(self) -> OutputError { + match self.ok() { + Ok(output) => panic!( + "Command completed successfully\nstdout=```{}```", + dump_buffer(&output.stdout) + ), + Err(err) => err, } } } @@ -227,6 +252,17 @@ impl<'c> OutputOkExt for &'c mut process::Command { Err(error) } } + + fn unwrap_err(self) -> OutputError { + match self.ok() { + Ok(output) => panic!( + "Completed successfully:\ncommand=`{:?}`\nstdout=```{}```", + self, + dump_buffer(&output.stdout) + ), + Err(err) => err, + } + } } impl<'c> OutputOkExt for &'c mut StdInCommand { @@ -255,6 +291,18 @@ impl<'c> OutputOkExt for &'c mut StdInCommand { Err(error) } } + + fn unwrap_err(self) -> OutputError { + match self.ok() { + Ok(output) => panic!( + "Completed successfully:\ncommand=`{:?}`\nstdin=```{}```\nstdout=```{}```", + self.cmd, + dump_buffer(&self.stdin), + dump_buffer(&output.stdout) + ), + Err(err) => err, + } + } } #[derive(Fail, Debug)] @@ -365,3 +413,11 @@ impl fmt::Display for OutputError { write!(f, "{}", self.cause) } } + +fn dump_buffer(buffer: &[u8]) -> String { + if let Ok(buffer) = str::from_utf8(&buffer) { + format!("{}", buffer) + } else { + format!("{:?}", buffer) + } +} From 300ece02e982cf4616bf7d073e9b34124f5fe8e4 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 30 Mar 2018 16:01:58 -0600 Subject: [PATCH 4/4] feat(cmd): First pass at assertions --- Cargo.toml | 1 + src/cmd.rs | 308 ++++++++++++++++++++++++++++++++++++++++++----------- src/lib.rs | 4 +- 3 files changed, 250 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7428cc4..b2e2ace 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ failure = "0.1" failure_derive = "0.1" serde_json = "1.0" environment = "0.1" +predicates = "0.3" tempfile = { version="3.0", optional=true } globwalk = { version="0.1", optional=true } diff --git a/src/cmd.rs b/src/cmd.rs index 70ab570..335524e 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -6,6 +6,7 @@ use std::process; use std::str; use failure; +pub use predicates; /// Extend `Command` with helpers for running the current crate's binaries. pub trait CommandCargoExt { @@ -150,7 +151,6 @@ where /// /// Command::new("echo") /// .args(&["42"]) - /// .output() /// .ok() /// .unwrap(); /// ``` @@ -167,7 +167,6 @@ where /// /// Command::new("echo") /// .args(&["42"]) - /// .output() /// .unwrap(); /// ``` fn unwrap(self) -> process::Output { @@ -188,7 +187,6 @@ where /// /// Command::new("non_existent_command") /// .args(&["42"]) - /// .output() /// .unwrap_err(); /// ``` fn unwrap_err(self) -> OutputError { @@ -203,21 +201,6 @@ where } impl OutputOkExt for process::Output { - /// Convert an `std::process::Output` into an `OutputResult`. - /// - /// # Examples - /// - /// ```rust,ignore - /// extern crate assert_cli; - /// use std::process::Command; - /// use assert_cli::cmd::*; - /// - /// Command::new("echo") - /// .args(&["42"]) - /// .output() - /// .ok() - /// .unwrap(); - /// ``` fn ok(self) -> OutputResult { if self.status.success() { Ok(self) @@ -229,20 +212,6 @@ impl OutputOkExt for process::Output { } impl<'c> OutputOkExt for &'c mut process::Command { - /// Convert an `std::process::Command` into an `OutputResult`. - /// - /// # Examples - /// - /// ```rust,ignore - /// extern crate assert_cli; - /// use std::process::Command; - /// use assert_cli::cmd::*; - /// - /// Command::new("echo") - /// .args(&["42"]) - /// .ok() - /// .unwrap(); - /// ``` fn ok(self) -> OutputResult { let output = self.output().map_err(|e| OutputError::with_cause(e))?; if output.status.success() { @@ -266,20 +235,6 @@ impl<'c> OutputOkExt for &'c mut process::Command { } impl<'c> OutputOkExt for &'c mut StdInCommand { - /// Convert an `std::process::Command` into an `OutputResult`. - /// - /// # Examples - /// - /// ```rust,ignore - /// extern crate assert_cli; - /// use std::process::Command; - /// use assert_cli::cmd::*; - /// - /// Command::new("cat") - /// .with_stdin("42") - /// .ok() - /// .unwrap(); - /// ``` fn ok(self) -> OutputResult { let output = self.output().map_err(|e| OutputError::with_cause(e))?; if output.status.success() { @@ -312,24 +267,28 @@ struct Output { impl fmt::Display for Output { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(code) = self.output.status.code() { - writeln!(f, "code={}", code)?; - } else { - writeln!(f, "code=")?; - } - if let Ok(stdout) = str::from_utf8(&self.output.stdout) { - writeln!(f, "stdout=```{}```", stdout)?; - } else { - writeln!(f, "stdout=```{:?}```", self.output.stdout)?; - } - if let Ok(stderr) = str::from_utf8(&self.output.stderr) { - writeln!(f, "stderr=```{}```", stderr)?; - } else { - writeln!(f, "stderr=```{:?}```", self.output.stderr)?; - } + output_fmt(&self.output, f) + } +} - Ok(()) +fn output_fmt(output: &process::Output, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(code) = output.status.code() { + writeln!(f, "code={}", code)?; + } else { + writeln!(f, "code=")?; + } + if let Ok(stdout) = str::from_utf8(&output.stdout) { + writeln!(f, "stdout=```{}```", stdout)?; + } else { + writeln!(f, "stdout=```{:?}```", output.stdout)?; + } + if let Ok(stderr) = str::from_utf8(&output.stderr) { + writeln!(f, "stderr=```{}```", stderr)?; + } else { + writeln!(f, "stderr=```{:?}```", output.stderr)?; } + + Ok(()) } #[derive(Debug)] @@ -414,6 +373,231 @@ impl fmt::Display for OutputError { } } +/// Extend `process::Output` with assertions. +/// +/// # Examples +/// +/// ```rust,ignore +/// extern crate assert_cli; +/// use std::process::Command; +/// use assert_cli::cmd::*; +/// +/// Command::main_binary() +/// .assert() +/// .success(); +/// ``` +pub trait OutputAssertExt { + /// Wrap with an interface for that provides assertions on the `process::Output`. + fn assert(self) -> Assert; +} + +impl OutputAssertExt for process::Output { + fn assert(self) -> Assert { + Assert::new(self) + } +} + +impl<'c> OutputAssertExt for &'c mut process::Command { + fn assert(self) -> Assert { + let output = self.output().unwrap(); + Assert::new(output).set_cmd(format!("{:?}", self)) + } +} + +impl<'c> OutputAssertExt for &'c mut StdInCommand { + fn assert(self) -> Assert { + let output = self.output().unwrap(); + Assert::new(output) + .set_cmd(format!("{:?}", self.cmd)) + .set_stdin(self.stdin.clone()) + } +} + +/// `process::Output` assertions. +#[derive(Debug)] +pub struct Assert { + output: process::Output, + cmd: Option, + stdin: Option>, +} + +impl Assert { + /// Convert `std::process::Output` into a `Fail`. + pub fn new(output: process::Output) -> Self { + Self { + output, + cmd: None, + stdin: None, + } + } + + /// Add the command line for additional context. + pub fn set_cmd(mut self, cmd: String) -> Self { + self.cmd = Some(cmd); + self + } + + /// Add the `stdn` for additional context. + pub fn set_stdin(mut self, stdin: Vec) -> Self { + self.stdin = Some(stdin); + self + } + + /// Access the contained `std::process::Output`. + pub fn get_output(&self) -> &process::Output { + &self.output + } + + // How does user interact with assertion API? + // - On Assert class, using error chaining + // - "Builder" or not? If yes, then do we extend Result? + // - How do we give a helpful unwrap? + // - Build up assertion data and "execute" it, like assert_cli used to? But that was mostly + // from building up before executing the command happened. Now we're doing it + // after-the-fact. + // - Immediately panic in each assertion? Let's give that a try. + + /// Ensure the command succeeded. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::main_binary() + /// .assert() + /// .success(); + /// ``` + pub fn success(self) -> Self { + if !self.output.status.success() { + panic!("Unexpected failure\n{}", self); + } + self + } + + /// Ensure the command failed. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::main_binary() + /// .env("exit", 1) + /// .assert() + /// .failure(); + /// ``` + pub fn failure(self) -> Self { + if self.output.status.success() { + panic!("Unexpected success\n{}", self); + } + self + } + + /// Ensure the command returned the expected code. + pub fn interrupted(self) -> Self { + if self.output.status.code().is_some() { + panic!("Unexpected completion\n{}", self); + } + self + } + + /// Ensure the command returned the expected code. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::main_binary() + /// .env("exit", "42") + /// .assert() + /// .code(predicates::predicate::eq(42)); + /// ``` + pub fn code(self, pred: &predicates::predicate::Predicate) -> Self { + let actual_code = self.output + .status + .code() + .unwrap_or_else(|| panic!("Command interrupted\n{}", self)); + if !pred.eval(&actual_code) { + panic!("Unexpected return code\n{}", self); + } + self + } + + /// Ensure the command wrote the expected data to `stdout`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::main_binary() + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stdout(predicates::predicate::eq(b"hello")); + /// ``` + pub fn stdout(self, pred: &predicates::predicate::Predicate>) -> Self { + { + let actual = &self.output.stdout; + if !pred.eval(actual) { + panic!("Unexpected stdout\n{}", self); + } + } + self + } + + /// Ensure the command wrote the expected data to `stderr`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use std::process::Command; + /// use assert_cli::cmd::*; + /// + /// Command::main_binary() + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stderr(predicates::predicate::eq(b"world")); + /// ``` + pub fn stderr(self, pred: &predicates::predicate::Predicate>) -> Self { + { + let actual = &self.output.stderr; + if !pred.eval(actual) { + panic!("Unexpected stderr\n{}", self); + } + } + self + } +} + +impl fmt::Display for Assert { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref cmd) = self.cmd { + writeln!(f, "command=`{}`", cmd)?; + } + if let Some(ref stdin) = self.stdin { + if let Ok(stdin) = str::from_utf8(&stdin) { + writeln!(f, "stdin=```{}```", stdin)?; + } else { + writeln!(f, "stdin=```{:?}```", stdin)?; + } + } + output_fmt(&self.output, f) + } +} + fn dump_buffer(buffer: &[u8]) -> String { if let Ok(buffer) = str::from_utf8(&buffer) { format!("{}", buffer) diff --git a/src/lib.rs b/src/lib.rs index 8d1b58d..1dcebc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,7 +126,9 @@ extern crate failure; extern crate failure_derive; extern crate globwalk; extern crate serde_json; -extern crate tempfile; +pub extern crate predicates; +#[cfg(feature = "tempdir")] +pub extern crate tempfile; mod errors; pub use errors::AssertionError;