diff --git a/Cargo.toml b/Cargo.toml index 5d53d06..4444670 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,16 @@ keywords = ["cli", "testing", "assert"] build = "build.rs" [dependencies] -colored = "1.4" +colored = "1.5" difference = "1.0" -error-chain = "0.10.0" -rustc-serialize = "0.3" +error-chain = "0.11" +serde_json = "1.0" [build-dependencies] -skeptic = "0.5" +skeptic = "0.13" [dev-dependencies] -skeptic = "0.5" +skeptic = "0.13" [badges] travis-ci = { repository = "killercup/assert_cli" } diff --git a/src/assert.rs b/src/assert.rs new file mode 100644 index 0000000..4a70290 --- /dev/null +++ b/src/assert.rs @@ -0,0 +1,431 @@ +use std::default; +use std::process::Command; +use std::path::PathBuf; + +use errors::*; +use output::{OutputAssertion, StdErr, StdOut}; + +/// Assertions for a specific command. +#[derive(Debug)] +pub struct Assert { + cmd: Vec, + current_dir: Option, + expect_success: Option, + expect_exit_code: Option, + expect_stdout: Option>, + expect_stderr: Option>, +} + +impl default::Default for Assert { + /// Construct an assert using `cargo run --` as command. + /// + /// Defaults to asserting _successful_ execution. + fn default() -> Self { + Assert { + cmd: vec!["cargo", "run", "--"] + .into_iter().map(String::from).collect(), + current_dir: None, + expect_success: Some(true), + expect_exit_code: None, + expect_stdout: None, + expect_stderr: None, + } + } +} + +impl Assert { + /// Run the crate's main binary. + /// + /// Defaults to asserting _successful_ execution. + pub fn main_binary() -> Self { + Assert::default() + } + + /// Run a specific binary of the current crate. + /// + /// Defaults to asserting _successful_ execution. + pub fn cargo_binary(name: &str) -> Self { + Assert { + cmd: vec!["cargo", "run", "--bin", name, "--"] + .into_iter().map(String::from).collect(), + ..Self::default() + } + } + + /// Run a custom command. + /// + /// Defaults to asserting _successful_ execution. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "1337"]) + /// .unwrap(); + /// ``` + pub fn command(cmd: &[&str]) -> Self { + Assert { + cmd: cmd.into_iter().cloned().map(String::from).collect(), + ..Self::default() + } + } + + /// Add arguments to the command. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo"]) + /// .with_args(&["42"]) + /// .prints("42") + /// .unwrap(); + /// ``` + pub fn with_args(mut self, args: &[&str]) -> Self { + self.cmd.extend(args.into_iter().cloned().map(String::from)); + self + } + + /// Sets the working directory for the command. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["wc", "lib.rs"]) + /// .current_dir(std::path::Path::new("src")) + /// .prints("lib.rs") + /// .execute() + /// .unwrap(); + /// ``` + pub fn current_dir>(mut self, dir: P) -> Self { + self.current_dir = Some(dir.into()); + self + } + + /// Small helper to make chains more readable. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "42"]) + /// .prints("42") + /// .unwrap(); + /// ``` + pub fn and(self) -> Self { + self + } + + /// Expect the command to be executed successfully. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "42"]) + /// .unwrap(); + /// ``` + pub fn succeeds(mut self) -> Self { + self.expect_exit_code = None; + self.expect_success = Some(true); + self + } + + /// Expect the command to fail. + /// + /// Note: This does not include shell failures like `command not found`. I.e. the + /// command must _run_ and fail for this assertion to pass. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) + /// .fails() + /// .and() + /// .prints_error("non-existing-file") + /// .unwrap(); + /// ``` + pub fn fails(mut self) -> Self { + self.expect_success = Some(false); + self + } + + /// Expect the command to fail and return a specific error code. + /// + /// # 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(); + /// ``` + pub fn fails_with(mut self, expect_exit_code: i32) -> Self { + self.expect_success = Some(false); + self.expect_exit_code = Some(expect_exit_code); + self + } + + /// Expect the command's output to **contain** `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "42"]) + /// .prints("42") + /// .unwrap(); + /// ``` + pub fn prints>(mut self, output: O) -> Self { + self.expect_stdout = Some(OutputAssertion { + expect: output.into(), + fuzzy: true, + 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`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) + /// .fails() + /// .and() + /// .prints_error("non-existing-file") + /// .unwrap(); + /// ``` + pub fn prints_error>(mut self, output: O) -> Self { + self.expect_stderr = Some(OutputAssertion { + expect: output.into(), + fuzzy: true, + expected_result: true, + kind: StdErr, + }); + self + } + + /// Expect the command to output **exactly** this `output` to stderr. + /// + /// # 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(); + /// ``` + 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 + } + + /// Expect the command's output to not **contain** `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "42"]) + /// .doesnt_print("73") + /// .execute() + /// .unwrap(); + /// ``` + 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 + } + + /// Expect the command to output to not be **exactly** this `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "42"]) + /// .doesnt_print_exactly("73") + /// .execute() + /// .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, + }); + self + } + + /// Expect the command's stderr output to not **contain** `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) + /// .fails() + /// .and() + /// .doesnt_print_error("content") + /// .execute() + /// .unwrap(); + /// ``` + pub fn doesnt_print_error>(mut self, output: O) -> Self { + self.expect_stderr = Some(OutputAssertion { + expect: output.into(), + fuzzy: true, + expected_result: false, + kind: StdErr, + }); + self + } + + /// Expect the command to output to not be **exactly** this `output` to stderr. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) + /// .fails_with(1) + /// .and() + /// .doesnt_print_error_exactly("content") + /// .execute() + /// .unwrap(); + /// ``` + pub fn doesnt_print_error_exactly>(mut self, output: O) -> Self { + self.expect_stderr = Some(OutputAssertion { + expect: output.into(), + fuzzy: false, + expected_result: false, + kind: StdErr, + }); + self + } + + /// Execute the command and check the assertions. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// let test = assert_cli::Assert::command(&["echo", "42"]) + /// .prints("42") + /// .execute(); + /// assert!(test.is_ok()); + /// ``` + 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(()) + } + + /// Execute the command, check the assertions, and panic when they fail. + /// + /// # Examples + /// + /// ```rust,should_panic="Assert CLI failure" + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "42"]) + /// .fails() + /// .unwrap(); // panics + /// ``` + pub fn unwrap(self) { + if let Err(err) = self.execute() { + panic!("{}", err); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 215c1fe..425ceae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,347 +106,16 @@ extern crate difference; #[macro_use] extern crate error_chain; -extern crate rustc_serialize; - -use std::process::Command; -use std::path::PathBuf; +extern crate serde_json; mod errors; -use errors::*; #[macro_use] mod macros; pub use macros::flatten_escaped_string; mod output; -use output::{OutputAssertion, StdErr, StdOut}; mod diff; -/// Assertions for a specific command. -#[derive(Debug)] -pub struct Assert { - cmd: Vec, - current_dir: Option, - expect_success: Option, - expect_exit_code: Option, - expect_stdout: Option>, - expect_stderr: Option>, -} - -impl std::default::Default for Assert { - /// Construct an assert using `cargo run --` as command. - /// - /// Defaults to asserting _successful_ execution. - fn default() -> Self { - Assert { - cmd: vec!["cargo", "run", "--"] - .into_iter().map(String::from).collect(), - current_dir: None, - expect_success: Some(true), - expect_exit_code: None, - expect_stdout: None, - expect_stderr: None, - } - } -} - -impl Assert { - /// Run the crate's main binary. - /// - /// Defaults to asserting _successful_ execution. - pub fn main_binary() -> Self { - Assert::default() - } - - /// Run a specific binary of the current crate. - /// - /// Defaults to asserting _successful_ execution. - pub fn cargo_binary(name: &str) -> Self { - Assert { - cmd: vec!["cargo", "run", "--bin", name, "--"] - .into_iter().map(String::from).collect(), - ..Self::default() - } - } - - /// Run a custom command. - /// - /// Defaults to asserting _successful_ execution. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["echo", "1337"]) - /// .unwrap(); - /// ``` - pub fn command(cmd: &[&str]) -> Self { - Assert { - cmd: cmd.into_iter().cloned().map(String::from).collect(), - ..Self::default() - } - } - - /// Add arguments to the command. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["echo"]) - /// .with_args(&["42"]) - /// .prints("42") - /// .unwrap(); - /// ``` - pub fn with_args(mut self, args: &[&str]) -> Self { - self.cmd.extend(args.into_iter().cloned().map(String::from)); - self - } - - /// Sets the working directory for the command. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["wc", "lib.rs"]) - /// .current_dir(std::path::Path::new("src")) - /// .prints("lib.rs") - /// .execute() - /// .unwrap(); - /// ``` - pub fn current_dir>(mut self, dir: P) -> Self { - self.current_dir = Some(dir.into()); - self - } - - /// Small helper to make chains more readable. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["echo", "42"]) - /// .prints("42") - /// .unwrap(); - /// ``` - pub fn and(self) -> Self { - self - } - - /// Expect the command to be executed successfully. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["echo", "42"]) - /// .unwrap(); - /// ``` - pub fn succeeds(mut self) -> Self { - self.expect_exit_code = None; - self.expect_success = Some(true); - self - } - - /// Expect the command to fail. - /// - /// Note: This does not include shell failures like `command not found`. I.e. the - /// command must _run_ and fail for this assertion to pass. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["cat", "non-existing-file"]) - /// .fails() - /// .and() - /// .prints_error("non-existing-file") - /// .unwrap(); - /// ``` - pub fn fails(mut self) -> Self { - self.expect_success = Some(false); - self - } - - /// Expect the command to fail and return a specific error code. - /// - /// # 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(); - /// ``` - pub fn fails_with(mut self, expect_exit_code: i32) -> Self { - self.expect_success = Some(false); - self.expect_exit_code = Some(expect_exit_code); - self - } - - /// Expect the command's output to **contain** `output`. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["echo", "42"]) - /// .prints("42") - /// .unwrap(); - /// ``` - pub fn prints>(mut self, output: O) -> Self { - self.expect_stdout = Some(OutputAssertion { - expect: output.into(), - fuzzy: 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, - kind: StdOut, - }); - self - } - - /// Expect the command's stderr output to **contain** `output`. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["cat", "non-existing-file"]) - /// .fails() - /// .and() - /// .prints_error("non-existing-file") - /// .unwrap(); - /// ``` - pub fn prints_error>(mut self, output: O) -> Self { - self.expect_stderr = Some(OutputAssertion { - expect: output.into(), - fuzzy: true, - kind: StdErr, - }); - self - } - - /// Expect the command to output **exactly** this `output` to stderr. - /// - /// # 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(); - /// ``` - pub fn prints_error_exactly>(mut self, output: O) -> Self { - self.expect_stderr = Some(OutputAssertion { - expect: output.into(), - fuzzy: false, - kind: StdErr, - }); - self - } - - /// Execute the command and check the assertions. - /// - /// # Examples - /// - /// ```rust - /// extern crate assert_cli; - /// - /// let test = assert_cli::Assert::command(&["echo", "42"]) - /// .prints("42") - /// .execute(); - /// assert!(test.is_ok()); - /// ``` - 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(()) - } - - /// Execute the command, check the assertions, and panic when they fail. - /// - /// # Examples - /// - /// ```rust,should_panic="Assert CLI failure" - /// extern crate assert_cli; - /// - /// assert_cli::Assert::command(&["echo", "42"]) - /// .fails() - /// .unwrap(); // panics - /// ``` - pub fn unwrap(self) { - if let Err(err) = self.execute() { - panic!("{}", err); - } - } -} +mod assert; +pub use assert::Assert; diff --git a/src/macros.rs b/src/macros.rs index 7377ce7..1c92529 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,5 +1,5 @@ use std::borrow::Cow; -use rustc_serialize::json::Json; +use serde_json; /// Easily construct an `Assert` with a custom command. /// @@ -50,10 +50,8 @@ macro_rules! assert_cmd { /// If `x` can not be decoded as `String`. #[doc(hidden)] fn deserialize_json_string(x: &str) -> String { - match Json::from_str(x).expect(&format!("Unable to deserialize `{:?}` as string.", x)) { - Json::String(deserialized) => deserialized, - _ => panic!("Unable to deserialize `{:?}` as string.", x), - } + serde_json::from_str(x) + .expect(&format!("Unable to deserialize `{:?}` as string.", x)) } /// Deserialize a JSON-encoded `String`. @@ -99,3 +97,32 @@ macro_rules! __assert_single_token_expression { // little helper (@DENY) => { }; } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn flatten_unquoted() + { + assert_eq!( + flatten_escaped_string("hello world"), + "hello world"); + } + + #[test] + fn flatten_quoted() + { + assert_eq!( + flatten_escaped_string(r#""hello world""#), + "hello world"); + } + + #[test] + fn flatten_escaped() + { + assert_eq!( + flatten_escaped_string(r#""hello world \u0042 A""#), + "hello world B A"); + } +} diff --git a/src/output.rs b/src/output.rs index 34c975c..b95fee2 100644 --- a/src/output.rs +++ b/src/output.rs @@ -11,13 +11,19 @@ use diff; pub struct OutputAssertion { pub expect: String, pub fuzzy: bool, + pub expected_result: bool, pub kind: T, } impl OutputAssertion { fn matches_fuzzy(&self, got: &str) -> Result<()> { - if !got.contains(&self.expect) { - bail!(ErrorKind::OutputMismatch(self.expect.clone(), got.into())); + let result = got.contains(&self.expect); + if result != self.expected_result { + if self.expected_result { + bail!(ErrorKind::OutputDoesntContain(self.expect.clone(), got.into())); + } else { + bail!(ErrorKind::OutputContains(self.expect.clone(), got.into())); + } } Ok(()) @@ -25,10 +31,15 @@ impl OutputAssertion { fn matches_exact(&self, got: &str) -> Result<()> { let differences = Changeset::new(self.expect.trim(), got.trim(), "\n"); - - if differences.distance > 0 { - let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::ExactOutputMismatch(nice_diff)); + let result = differences.distance == 0; + + if result != self.expected_result { + if self.expected_result { + let nice_diff = diff::render(&differences)?; + bail!(ErrorKind::OutputDoesntMatch(nice_diff)); + } else { + bail!(ErrorKind::OutputMatches(got.to_owned())); + } } Ok(()) @@ -88,14 +99,22 @@ mod errors { Fmt(::std::fmt::Error); } errors { - OutputMismatch(expected: String, got: String) { + OutputDoesntContain(expected: String, got: String) { description("Output was not as expected") display("expected to contain {:?}, got {:?}", expected, got) } - ExactOutputMismatch(diff: String) { + OutputContains(expected: String, got: String) { + description("Output was not as expected") + display("expected to not contain {:?}, got {:?}", expected, got) + } + OutputDoesntMatch(diff: String) { description("Output was not as expected") display("{}", diff) } + OutputMatches(got: String) { + description("Output was not as expected") + display("{}", got) + } } } }