diff --git a/README.md b/README.md index ae97974..796d975 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Here's a trivial example: extern crate assert_cli; fn main() { - assert_cli::Assert::command(&["echo", "42"]).prints("42").unwrap(); + assert_cli::Assert::command(&["echo", "42"]).stdout().contains("42").unwrap(); } ``` @@ -31,7 +31,7 @@ Or if you'd rather use the macro, to save you some writing: #[macro_use] extern crate assert_cli; fn main() { - assert_cmd!(echo "42").prints("42").unwrap(); + assert_cmd!(echo "42").stdout().contains("42").unwrap(); } ``` @@ -45,21 +45,21 @@ fn main() { let test = assert_cmd!(ls "foo-bar-foo") .fails() .and() - .prints_error("foo-bar-foo") + .stderr().contains("foo-bar-foo") .execute(); assert!(test.is_ok()); } ``` If you want to match the program's output _exactly_, you can use -`prints_exactly`: +`stdout().is`: ```rust,should_panic #[macro_use] extern crate assert_cli; fn main() { assert_cmd!(wc "README.md") - .prints_exactly("1337 README.md") + .stdout().is("1337 README.md") .unwrap(); } ``` diff --git a/src/assert.rs b/src/assert.rs index 4a70290..0732c35 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -1,9 +1,10 @@ use std::default; use std::process::Command; use std::path::PathBuf; +use std::vec::Vec; use errors::*; -use output::{OutputAssertion, StdErr, StdOut}; +use output::{OutputAssertion, OutputKind}; /// Assertions for a specific command. #[derive(Debug)] @@ -12,8 +13,7 @@ pub struct Assert { current_dir: Option, expect_success: Option, expect_exit_code: Option, - expect_stdout: Option>, - expect_stderr: Option>, + expect_output: Vec, } impl default::Default for Assert { @@ -27,8 +27,7 @@ impl default::Default for Assert { current_dir: None, expect_success: Some(true), expect_exit_code: None, - expect_stdout: None, - expect_stderr: None, + expect_output: vec![], } } } @@ -80,7 +79,7 @@ impl Assert { /// /// assert_cli::Assert::command(&["echo"]) /// .with_args(&["42"]) - /// .prints("42") + /// .stdout().contains("42") /// .unwrap(); /// ``` pub fn with_args(mut self, args: &[&str]) -> Self { @@ -97,7 +96,7 @@ impl Assert { /// /// assert_cli::Assert::command(&["wc", "lib.rs"]) /// .current_dir(std::path::Path::new("src")) - /// .prints("lib.rs") + /// .stdout().contains("lib.rs") /// .execute() /// .unwrap(); /// ``` @@ -114,7 +113,7 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .prints("42") + /// .stdout().contains("42") /// .unwrap(); /// ``` pub fn and(self) -> Self { @@ -150,7 +149,7 @@ impl Assert { /// assert_cli::Assert::command(&["cat", "non-existing-file"]) /// .fails() /// .and() - /// .prints_error("non-existing-file") + /// .stderr().contains("non-existing-file") /// .unwrap(); /// ``` pub fn fails(mut self) -> Self { @@ -168,7 +167,7 @@ impl Assert { /// assert_cli::Assert::command(&["cat", "non-existing-file"]) /// .fails_with(1) /// .and() - /// .prints_error_exactly("cat: non-existing-file: No such file or directory") + /// .stderr().is("cat: non-existing-file: No such file or directory") /// .unwrap(); /// ``` pub fn fails_with(mut self, expect_exit_code: i32) -> Self { @@ -177,7 +176,7 @@ impl Assert { self } - /// Expect the command's output to **contain** `output`. + /// Create an assertion for stdout's contents /// /// # Examples /// @@ -185,41 +184,18 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .prints("42") + /// .stdout().contains("42") /// .unwrap(); /// ``` - pub fn prints>(mut self, output: O) -> Self { - self.expect_stdout = Some(OutputAssertion { - expect: output.into(), - fuzzy: true, + pub fn stdout(self) -> OutputAssertionBuilder { + OutputAssertionBuilder { + assertion: self, + kind: OutputKind::StdOut, expected_result: true, - kind: StdOut, - }); - self - } - - /// Expect the command to output **exactly** this `output`. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["echo", "42"]) - /// .prints_exactly("42") - /// .unwrap(); - /// ``` - pub fn prints_exactly>(mut self, output: O) -> Self { - self.expect_stdout = Some(OutputAssertion { - expect: output.into(), - fuzzy: false, - expected_result: true, - kind: StdOut, - }); - self + } } - /// Expect the command's stderr output to **contain** `output`. + /// Create an assertion for stdout's contents /// /// # Examples /// @@ -227,67 +203,96 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["cat", "non-existing-file"]) - /// .fails() + /// .fails_with(1) /// .and() - /// .prints_error("non-existing-file") + /// .stderr().is("cat: non-existing-file: No such file or directory") /// .unwrap(); /// ``` - pub fn prints_error>(mut self, output: O) -> Self { - self.expect_stderr = Some(OutputAssertion { - expect: output.into(), - fuzzy: true, + pub fn stderr(self) -> OutputAssertionBuilder { + OutputAssertionBuilder { + assertion: self, + kind: OutputKind::StdErr, expected_result: true, - kind: StdErr, - }); - self + } } - /// Expect the command to output **exactly** this `output` to stderr. + /// Execute the command and check the assertions. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-existing-file"]) - /// .fails_with(1) - /// .and() - /// .prints_error_exactly("cat: non-existing-file: No such file or directory") - /// .unwrap(); + /// let test = assert_cli::Assert::command(&["echo", "42"]) + /// .stdout().contains("42") + /// .execute(); + /// assert!(test.is_ok()); /// ``` - pub fn prints_error_exactly>(mut self, output: O) -> Self { - self.expect_stderr = Some(OutputAssertion { - expect: output.into(), - fuzzy: false, - expected_result: true, - kind: StdErr, - }); - self + pub fn execute(self) -> Result<()> { + let cmd = &self.cmd[0]; + let args: Vec<_> = self.cmd.iter().skip(1).collect(); + let mut command = Command::new(cmd); + let command = command.args(&args); + let command = match self.current_dir { + Some(ref dir) => command.current_dir(dir), + None => command, + }; + let output = command.output()?; + + if let Some(expect_success) = self.expect_success { + if expect_success != output.status.success() { + bail!(ErrorKind::StatusMismatch( + self.cmd.clone(), + expect_success, + )); + } + } + + if self.expect_exit_code.is_some() && + self.expect_exit_code != output.status.code() { + bail!(ErrorKind::ExitCodeMismatch( + self.cmd.clone(), + self.expect_exit_code, + output.status.code(), + )); + } + + self.expect_output + .iter() + .map(|a| a.execute(&output, &self.cmd)) + .collect::>>()?; + + Ok(()) } - /// Expect the command's output to not **contain** `output`. + /// Execute the command, check the assertions, and panic when they fail. /// /// # Examples /// - /// ```rust + /// ```rust,should_panic="Assert CLI failure" /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .doesnt_print("73") - /// .execute() - /// .unwrap(); + /// .fails() + /// .unwrap(); // panics /// ``` - pub fn doesnt_print>(mut self, output: O) -> Self { - self.expect_stdout = Some(OutputAssertion { - expect: output.into(), - fuzzy: true, - expected_result: false, - kind: StdOut, - }); - self + pub fn unwrap(self) { + if let Err(err) = self.execute() { + panic!("{}", err); + } } +} - /// Expect the command to output to not be **exactly** this `output`. +/// Assertions for command output. +#[derive(Debug)] +pub struct OutputAssertionBuilder { + assertion: Assert, + kind: OutputKind, + expected_result: bool, +} + +impl OutputAssertionBuilder { + /// Negate the assertion predicate /// /// # Examples /// @@ -295,137 +300,83 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .doesnt_print_exactly("73") - /// .execute() + /// .stdout().not().contains("73") /// .unwrap(); /// ``` - pub fn doesnt_print_exactly>(mut self, output: O) -> Self { - self.expect_stdout = Some(OutputAssertion { - expect: output.into(), - fuzzy: false, - expected_result: false, - kind: StdOut, - }); + pub fn not(mut self) -> Self { + self.expected_result = ! self.expected_result; self } - /// Expect the command's stderr output to not **contain** `output`. + /// Expect the command's output to **contain** `output`. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-existing-file"]) - /// .fails() - /// .and() - /// .doesnt_print_error("content") - /// .execute() + /// assert_cli::Assert::command(&["echo", "42"]) + /// .stdout().contains("42") /// .unwrap(); /// ``` - pub fn doesnt_print_error>(mut self, output: O) -> Self { - self.expect_stderr = Some(OutputAssertion { + pub fn contains>(mut self, output: O) -> Assert { + self.assertion.expect_output.push(OutputAssertion { expect: output.into(), fuzzy: true, - expected_result: false, - kind: StdErr, + expected_result: self.expected_result, + kind: self.kind, }); - self + self.assertion } - /// Expect the command to output to not be **exactly** this `output` to stderr. + /// Expect the command to output **exactly** this `output`. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-existing-file"]) - /// .fails_with(1) - /// .and() - /// .doesnt_print_error_exactly("content") - /// .execute() + /// assert_cli::Assert::command(&["echo", "42"]) + /// .stdout().is("42") /// .unwrap(); /// ``` - pub fn doesnt_print_error_exactly>(mut self, output: O) -> Self { - self.expect_stderr = Some(OutputAssertion { + pub fn is>(mut self, output: O) -> Assert { + self.assertion.expect_output.push(OutputAssertion { expect: output.into(), fuzzy: false, - expected_result: false, - kind: StdErr, + expected_result: self.expected_result, + kind: self.kind, }); - self + self.assertion } - /// Execute the command and check the assertions. + /// Expect the command's output to not **contain** `output`. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// let test = assert_cli::Assert::command(&["echo", "42"]) - /// .prints("42") - /// .execute(); - /// assert!(test.is_ok()); + /// assert_cli::Assert::command(&["echo", "42"]) + /// .stdout().doesnt_contain("73") + /// .unwrap(); /// ``` - pub fn execute(self) -> Result<()> { - let cmd = &self.cmd[0]; - let args: Vec<_> = self.cmd.iter().skip(1).collect(); - let mut command = Command::new(cmd); - let command = command.args(&args); - let command = match self.current_dir { - Some(ref dir) => command.current_dir(dir), - None => command, - }; - let output = command.output()?; - - - if let Some(expect_success) = self.expect_success { - if expect_success != output.status.success() { - bail!(ErrorKind::StatusMismatch( - self.cmd.clone(), - expect_success, - )); - } - } - - if self.expect_exit_code.is_some() && - self.expect_exit_code != output.status.code() { - bail!(ErrorKind::ExitCodeMismatch( - self.cmd.clone(), - self.expect_exit_code, - output.status.code(), - )); - } - - if let Some(ref ouput_assertion) = self.expect_stdout { - ouput_assertion.execute(&output) - .map_err(|e| ErrorKind::StdoutMismatch(self.cmd.clone(), e))?; - } - - if let Some(ref ouput_assertion) = self.expect_stderr { - ouput_assertion.execute(&output) - .map_err(|e| ErrorKind::StderrMismatch(self.cmd.clone(), e))?; - } - - Ok(()) + pub fn doesnt_contain>(self, output: O) -> Assert { + self.not().contains(output) } - /// Execute the command, check the assertions, and panic when they fail. + /// Expect the command to output to not be **exactly** this `output`. /// /// # Examples /// - /// ```rust,should_panic="Assert CLI failure" + /// ```rust /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .fails() - /// .unwrap(); // panics + /// .stdout().isnt("73") + /// .unwrap(); /// ``` - pub fn unwrap(self) { - if let Err(err) = self.execute() { - panic!("{}", err); - } + pub fn isnt>(self, output: O) -> Assert { + self.not().is(output) } } diff --git a/src/lib.rs b/src/lib.rs index 425ceae..5d103da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ //! //! ```rust //! assert_cli::Assert::command(&["echo", "42"]) -//! .prints("42") +//! .stdout().contains("42") //! .unwrap(); //! ``` //! @@ -26,7 +26,7 @@ //! //! ```rust,should_panic //! assert_cli::Assert::command(&["echo", "42"]) -//! .prints_exactly("1337") +//! .stdout().is("1337") //! .unwrap(); //! ``` //! @@ -45,7 +45,7 @@ //! ```rust //! # #[macro_use] extern crate assert_cli; //! # fn main() { -//! assert_cmd!(echo "42").prints("42").unwrap(); +//! assert_cmd!(echo "42").stdout().contains("42").unwrap(); //! # } //! ``` //! @@ -67,8 +67,6 @@ //! # fn main() { //! assert_cmd!(cat "non-existing-file") //! .fails() -//! .and() -//! .prints_error("non-existing-file") //! .unwrap(); //! # } //! ``` @@ -78,10 +76,25 @@ //! - Use `fails_with` to assert a specific exit status. //! - There is also a `succeeds` method, but this is already the implicit default //! and can usually be omitted. -//! - We can inspect the output of **stderr** with `prints_error` and `prints_error_exactly`. //! - The `and` method has no effect, other than to make everything more readable. //! Feel free to use it. :-) //! +//! ## stdout / stderr +//! +//! You can add assertions on the content of **stdout** and **stderr**. They +//! can be mixed together or even multiple of one stream can be used. +//! +//! ```rust +//! # #[macro_use] extern crate assert_cli; +//! # fn main() { +//! assert_cmd!(echo "Hello world! The ansswer is 42.") +//! .stdout().contains("Hello world") +//! .stdout().contains("42") +//! .stderr().is("") +//! .unwrap(); +//! # } +//! ``` +//! //! ## Assert CLI Crates //! //! If you are testing a Rust binary crate, you can start with @@ -97,7 +110,7 @@ //! ```rust //! # #[macro_use] extern crate assert_cli; //! # fn main() { -//! let x = assert_cmd!(echo "1337").prints_exactly("42").execute(); +//! let x = assert_cmd!(echo "1337").stdout().is("42").execute(); //! assert!(x.is_err()); //! # } //! ``` @@ -119,3 +132,4 @@ mod diff; mod assert; pub use assert::Assert; +pub use assert::OutputAssertionBuilder; diff --git a/src/macros.rs b/src/macros.rs index 1c92529..1078032 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -22,7 +22,7 @@ use serde_json; /// # fn main() { /// assert_cmd!(echo "Launch sequence initiated.\nNo errors whatsoever!\n") /// .succeeds() -/// .prints("No errors whatsoever") +/// .stdout().contains("No errors whatsoever") /// .unwrap(); /// # } /// ``` diff --git a/src/output.rs b/src/output.rs index b95fee2..3376226 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,4 +1,3 @@ -use std::fmt; use std::process::Output; use difference::Changeset; @@ -8,14 +7,14 @@ pub use self::errors::{Error, ErrorKind}; use diff; #[derive(Debug, Clone)] -pub struct OutputAssertion { +pub struct OutputAssertion { pub expect: String, pub fuzzy: bool, pub expected_result: bool, - pub kind: T, + pub kind: OutputKind, } -impl OutputAssertion { +impl OutputAssertion { fn matches_fuzzy(&self, got: &str) -> Result<()> { let result = got.contains(&self.expect); if result != self.expected_result { @@ -45,51 +44,37 @@ impl OutputAssertion { Ok(()) } - pub fn execute(&self, output: &Output) -> Result<()> { + pub fn execute(&self, output: &Output, cmd: &[String]) -> super::errors::Result<()> { let observed = String::from_utf8_lossy(self.kind.select(output)); - if self.fuzzy { + let result = if self.fuzzy { self.matches_fuzzy(&observed) } else { self.matches_exact(&observed) - } + }; + result.map_err(|e| self.kind.map_err(e, cmd)) } } - -pub trait OutputType: fmt::Display { - fn select<'a>(&self, o: &'a Output) -> &'a [u8]; -} - - #[derive(Debug, Clone, Copy)] -pub struct StdOut; - -impl fmt::Display for StdOut { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "stdout") - } -} - -impl OutputType for StdOut { - fn select<'a>(&self, o: &'a Output) -> &'a [u8] { - &o.stdout - } +pub enum OutputKind { + StdOut, + StdErr, } - -#[derive(Debug, Clone, Copy)] -pub struct StdErr; - -impl fmt::Display for StdErr { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "stderr") +impl OutputKind { + pub fn select<'a>(self, o: &'a Output) -> &'a [u8] { + match self { + OutputKind::StdOut => &o.stdout, + OutputKind::StdErr => &o.stderr, + } } -} -impl OutputType for StdErr { - fn select<'a>(&self, o: &'a Output) -> &'a [u8] { - &o.stderr + pub fn map_err(self, e: Error, cmd: &[String]) -> super::errors::Error { + match self { + OutputKind::StdOut => super::errors::ErrorKind::StdoutMismatch(cmd.to_vec(), e).into(), + OutputKind::StdErr => super::errors::ErrorKind::StderrMismatch(cmd.to_vec(), e).into(), + } } }