From 5681d0b6b9371ed2ffd8b1100a5479ebc9301f16 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Tue, 21 Mar 2017 20:43:39 +0100 Subject: [PATCH 01/10] cargo.toml keywords --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4dda779..398a65a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ homepage = "https://github.com/killercup/assert_cli" documentation = "http://killercup.github.io/assert_cli/" readme = "README.md" categories = ["development-tools::testing"] -keywords = ["cli", "testing"] +keywords = ["cli", "testing", "assert"] build = "build.rs" [dependencies] From ed19a66c9600706a478393917f7a7da598979303 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Tue, 21 Mar 2017 20:55:13 +0100 Subject: [PATCH 02/10] README.md - make the header less cluttered - add generic docs badge - remove coveralls badge (its not up-to-date) - print() -> prints() --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 76cef87..f6edb97 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ # Assert CLI -> Test CLI Applications. +> **Test CLI Applications** - This crate checks the output of a child process is as expected. -Currently, this crate only includes basic functionality to check the output of a child process -is as expected. - -[![Build Status](https://travis-ci.org/killercup/assert_cli.svg)](https://travis-ci.org/killercup/assert_cli) [![Coverage Status](https://coveralls.io/repos/killercup/assert_cli/badge.svg?branch=master&service=github)](https://coveralls.io/github/killercup/assert_cli?branch=master) - -**[Documentation](http://killercup.github.io/assert_cli/)** +[![Build Status](https://travis-ci.org/killercup/assert_cli.svg)](https://travis-ci.org/killercup/assert_cli) [![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] ## Install @@ -54,8 +49,8 @@ fn main() { } ``` -If you want to check for the program's output, you can use `print` or -`print_exactly`: +If you want to match the program's output _exactly_, you can use +`prints_exactly`: ```rust,should_panic="Assert CLI failure" #[macro_use] extern crate assert_cli; @@ -67,13 +62,16 @@ fn main() { } ``` -this will show a nice, colorful diff in your terminal, like this: +... which has the benefit to show a nice, colorful diff in your terminal, +like this: ```diff -1337 +92 ``` +More detailed information is available in the [documentation]. :-) + ## License Licensed under either of @@ -89,3 +87,5 @@ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[Documentation]: http://killercup.github.io/assert_cli/ From cc2d7be18ac8e12ae0bdff84a3f5d711a2d35af2 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Tue, 21 Mar 2017 21:12:27 +0100 Subject: [PATCH 03/10] documentation tweaks - note: std has `.` at the end of one-line doc-strings. --- src/lib.rs | 80 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b319cca..0ae100c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,14 +2,21 @@ //! //! This crate's goal is to provide you some very easy tools to test your CLI //! applications. It can currently execute child processes and validate their -//! exit status as well as stdout output against your assertions. +//! exit status as well as stdout and stderr output against your assertions. //! -//! ## Examples +//! Include the crate like //! -//! Here's a trivial example: +//! ```rust +//! #[macro_use] // <-- import the convenience macro (optional) +//! extern crate assert_cli; +//! # fn main() { } +//! ``` //! -//! ```rust extern crate assert_cli; +//! ## Basic Examples +//! +//! Here's a trivial example: //! +//! ```rust //! assert_cli::Assert::command(&["echo", "42"]) //! .succeeds() //! .and().prints("42") @@ -20,7 +27,7 @@ //! //! ```rust,should_panic //! assert_cli::Assert::command(&["echo", "42"]) -//! .prints("1337") +//! .prints_exactly("1337") //! .unwrap(); //! ``` //! @@ -31,29 +38,33 @@ //! +42 //! ``` //! +//! ## Assert CLI Crates +//! //! If you are testing a Rust binary crate, you can start with //! `Assert::main_binary()` to use `cargo run` as command. Or, if you want to //! run a specific binary (if you have more than one), use //! `Assert::cargo_binary`. //! -//! Alternatively, you can use the `assert_cmd!` macro to construct the command: +//! ## `assert_cmd!` Macro //! -//! ```rust -//! #[macro_use] extern crate assert_cli; +//! Alternatively, you can use the `assert_cmd!` macro to construct the command more conveniently: //! +//! ```rust +//! # #[macro_use] extern crate assert_cli; //! # fn main() { //! assert_cmd!(echo 42).succeeds().prints("42").unwrap(); //! # } //! ``` //! -//! (Make sure to include the crate as `#[macro_use] extern crate assert_cli;`!) +//! Don't forget to import the crate with `#[macro_use]`. ;-) +//! +//! ## Don't Panic! //! //! If you don't want it to panic when the assertions are not met, simply call //! `.execute` instead of `.unwrap` to get a `Result`: //! //! ```rust -//! #[macro_use] extern crate assert_cli; -//! +//! # #[macro_use] extern crate assert_cli; //! # fn main() { //! let x = assert_cmd!(echo 1337).prints_exactly("42").execute(); //! assert!(x.is_err()); @@ -74,7 +85,7 @@ use errors::*; mod diff; -/// Assertions for a specific command +/// Assertions for a specific command. #[derive(Debug)] pub struct Assert { cmd: Vec, @@ -88,6 +99,8 @@ pub struct Assert { 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", "--"] @@ -103,12 +116,16 @@ impl std::default::Default for Assert { } impl Assert { - /// Use the crate's main binary as command + /// Run the crate's main binary. + /// + /// Defaults to asserting _successful_ execution. pub fn main_binary() -> Self { Assert::default() } - /// Use the crate's main binary as command + /// 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, "--"] @@ -117,7 +134,9 @@ impl Assert { } } - /// Use custom command + /// Run a custom command. + /// + /// Defaults to asserting _successful_ execution. /// /// # Examples /// @@ -135,7 +154,7 @@ impl Assert { } } - /// Add arguments to the command + /// Add arguments to the command. /// /// # Examples /// @@ -153,7 +172,7 @@ impl Assert { self } - /// Small helper to make chains more readable + /// Small helper to make chains more readable. /// /// # Examples /// @@ -168,7 +187,9 @@ impl Assert { self } - /// Expect the command to be executed successfully + /// Expect the command to be executed successfully. + /// + /// Note: This is already set by default, so you only need this for explicitness. /// /// # Examples /// @@ -184,7 +205,10 @@ impl Assert { self } - /// Expect the command to fail + /// 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 /// @@ -200,7 +224,7 @@ impl Assert { self } - /// Expect the command to fail and return a specific error code + /// Expect the command to fail and return a specific error code. /// /// # Examples /// @@ -217,7 +241,7 @@ impl Assert { self } - /// Expect the command's output to contain `output` + /// Expect the command's output to contain `output`. /// /// # Examples /// @@ -234,7 +258,7 @@ impl Assert { self } - /// Expect the command to output exactly this `output` + /// Expect the command to output exactly this `output`. /// /// # Examples /// @@ -251,7 +275,7 @@ impl Assert { self } - /// Expect the command's stderr output to contain `output` + /// Expect the command's stderr output to contain `output`. /// /// # Examples /// @@ -269,7 +293,7 @@ impl Assert { self } - /// Expect the command to output exactly this `output` to stderr + /// Expect the command to output exactly this `output` to stderr. /// /// # Examples /// @@ -287,7 +311,7 @@ impl Assert { self } - /// Execute the command and check the assertions + /// Execute the command and check the assertions. /// /// # Examples /// @@ -362,7 +386,7 @@ impl Assert { Ok(()) } - /// Execute the command, check the assertions, and panic when they fail + /// Execute the command, check the assertions, and panic when they fail. /// /// # Examples /// @@ -380,7 +404,7 @@ impl Assert { } } -/// Easily construct an `Assert` with a custom command +/// Easily construct an `Assert` with a custom command. /// /// Make sure to include the crate as `#[macro_use] extern crate assert_cli;` if /// you want to use this macro. @@ -394,7 +418,7 @@ impl Assert { /// No errors whatsoever /// ``` /// -/// you would call it like this: +/// ..., you would call it like this: /// /// ```rust /// #[macro_use] extern crate assert_cli; From cdf0d1eacd4b4cc560f000c1bea7c1e58b520c73 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Tue, 21 Mar 2017 20:47:50 +0100 Subject: [PATCH 04/10] error formatting - try to mimic `assert_eq!` - close #14: try to avoid multiline panics --- src/errors.rs | 46 +++++++++++++++++++++++++++++++++------------- src/lib.rs | 8 +++++--- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index b2751f5..2c0b44a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,5 @@ +static ERROR_PREFIX: &'static str = "CLI assertion failed"; + error_chain! { foreign_links { Io(::std::io::Error); @@ -7,42 +9,60 @@ error_chain! { StatusMismatch(cmd: Vec, expected: bool) { description("Wrong status") display( - "Command {:?} {got} but expected it to {expected}", + "{}: `(command `{}` expected to {})` (command {})", + ERROR_PREFIX, cmd.join(" "), - got = if *expected { "failed" } else { "succeed" }, - expected = if *expected { "succeed" } else { "failed" }, + expected = if *expected { "succeed" } else { "fail" }, + got = if *expected { "failed" } else { "succeeded" }, ) } ExitCodeMismatch(cmd: Vec, expected: Option, got: Option) { description("Wrong exit code") display( - "Command {:?} exited with code {:?} but expected it to be {:?}", - cmd.join(" "), got, expected, + "{}: `(exit code of `{}` expected to be `{:?}`)` (exit code was: `{:?}`)", + ERROR_PREFIX, + cmd.join(" "), + expected, + got, ) } - OutputMismatch(expected: String, got: String) { + OutputMismatch(cmd: Vec, expected: String, got: String) { description("Output was not as expected") display( - "Expected output to contain\n{}\nbut could not find it in\n{}", + "{}: `(output of `{}` expected to contain `{:?}`)` (output was: `{:?}`)", + ERROR_PREFIX, + cmd.join(" "), expected, got, ) } - ExactOutputMismatch(diff: String) { + ExactOutputMismatch(cmd: Vec, diff: String) { description("Output was not as expected") - display("{}", diff) + display( + "{}: `(output of `{}` was not as expected)`\n{}\n", + ERROR_PREFIX, + cmd.join(" "), + diff.trim() + ) } - ErrorOutputMismatch(expected: String, got: String) { + ErrorOutputMismatch(cmd: Vec, expected: String, got: String) { description("Stderr output was not as expected") display( - "Expected stderr output to contain\n{}\nbut could not find it in\n{}", + "{}: `(stderr output of `{}` expected to contain `{:?}`)` (stderr was: `{:?}`)", + ERROR_PREFIX, + cmd.join(" "), expected, got, ) } - ExactErrorOutputMismatch(diff: String) { + ExactErrorOutputMismatch(cmd: Vec, diff: String) { description("Stderr output was not as expected") - display("{}", diff) + display( + "{}: `(stderr output of `{}` was not as expected)`\n{}\n", + ERROR_PREFIX, + cmd.join(" "), + diff.trim() + ) } } } diff --git a/src/lib.rs b/src/lib.rs index 0ae100c..da3a38e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -351,6 +351,7 @@ impl Assert { match (self.expect_output, self.fuzzy_output) { (Some(ref expected_output), true) if !stdout.contains(expected_output) => { bail!(ErrorKind::OutputMismatch( + self.cmd.clone(), expected_output.clone(), stdout.into(), )); @@ -359,7 +360,7 @@ impl Assert { let differences = Changeset::new(expected_output.trim(), stdout.trim(), "\n"); if differences.distance > 0 { let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::ExactOutputMismatch(nice_diff)); + bail!(ErrorKind::ExactOutputMismatch(self.cmd.clone(), nice_diff)); } }, _ => {}, @@ -369,6 +370,7 @@ impl Assert { match (self.expect_error_output, self.fuzzy_error_output) { (Some(ref expected_output), true) if !stderr.contains(expected_output) => { bail!(ErrorKind::ErrorOutputMismatch( + self.cmd.clone(), expected_output.clone(), stderr.into(), )); @@ -377,7 +379,7 @@ impl Assert { let differences = Changeset::new(expected_output.trim(), stderr.trim(), "\n"); if differences.distance > 0 { let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::ExactErrorOutputMismatch(nice_diff)); + bail!(ErrorKind::ExactErrorOutputMismatch(self.cmd.clone(),nice_diff)); } }, _ => {}, @@ -399,7 +401,7 @@ impl Assert { /// ``` pub fn unwrap(self) { if let Err(err) = self.execute() { - panic!("Assert CLI failure:\n{}", err); + panic!("{}", err); } } } From 0ee479c650abb3f6540884b0f11a6ec8b6b15590 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Tue, 21 Mar 2017 21:01:49 +0100 Subject: [PATCH 05/10] refactor output assertion .. just a suggestion, since stdout and stderr are somewhat redundant. --- src/lib.rs | 66 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index da3a38e..42bf865 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,10 +91,14 @@ pub struct Assert { cmd: Vec, expect_success: bool, expect_exit_code: Option, - expect_output: Option, - fuzzy_output: bool, - expect_error_output: Option, - fuzzy_error_output: bool, + expect_stdout: Option, + expect_stderr: Option, +} + +#[derive(Debug)] +struct OutputAssertion { + expect: String, + fuzzy: bool, } impl std::default::Default for Assert { @@ -107,10 +111,8 @@ impl std::default::Default for Assert { .into_iter().map(String::from).collect(), expect_success: true, expect_exit_code: None, - expect_output: None, - fuzzy_output: false, - expect_error_output: None, - fuzzy_error_output: false, + expect_stdout: None, + expect_stderr: None, } } } @@ -253,8 +255,10 @@ impl Assert { /// .unwrap(); /// ``` pub fn prints>(mut self, output: O) -> Self { - self.expect_output = Some(output.into()); - self.fuzzy_output = true; + self.expect_stdout = Some(OutputAssertion { + expect: output.into(), + fuzzy: true, + }); self } @@ -270,8 +274,10 @@ impl Assert { /// .unwrap(); /// ``` pub fn prints_exactly>(mut self, output: O) -> Self { - self.expect_output = Some(output.into()); - self.fuzzy_output = false; + self.expect_stdout = Some(OutputAssertion { + expect: output.into(), + fuzzy: false, + }); self } @@ -288,8 +294,10 @@ impl Assert { /// .unwrap(); /// ``` pub fn prints_error>(mut self, output: O) -> Self { - self.expect_error_output = Some(output.into()); - self.fuzzy_error_output = true; + self.expect_stderr = Some(OutputAssertion { + expect: output.into(), + fuzzy: true, + }); self } @@ -306,8 +314,10 @@ impl Assert { /// .unwrap(); /// ``` pub fn prints_error_exactly>(mut self, output: O) -> Self { - self.expect_error_output = Some(output.into()); - self.fuzzy_error_output = false; + self.expect_stderr = Some(OutputAssertion { + expect: output.into(), + fuzzy: false, + }); self } @@ -348,15 +358,21 @@ impl Assert { } let stdout = String::from_utf8_lossy(&output.stdout); - match (self.expect_output, self.fuzzy_output) { - (Some(ref expected_output), true) if !stdout.contains(expected_output) => { + match self.expect_stdout { + Some(OutputAssertion { + expect: ref expected_output, + fuzzy: true, + }) if !stdout.contains(expected_output) => { bail!(ErrorKind::OutputMismatch( self.cmd.clone(), expected_output.clone(), stdout.into(), )); }, - (Some(ref expected_output), false) => { + Some(OutputAssertion { + expect: ref expected_output, + fuzzy: false, + }) => { let differences = Changeset::new(expected_output.trim(), stdout.trim(), "\n"); if differences.distance > 0 { let nice_diff = diff::render(&differences)?; @@ -367,15 +383,21 @@ impl Assert { } let stderr = String::from_utf8_lossy(&output.stderr); - match (self.expect_error_output, self.fuzzy_error_output) { - (Some(ref expected_output), true) if !stderr.contains(expected_output) => { + match self.expect_stderr { + Some(OutputAssertion { + expect: ref expected_output, + fuzzy: true, + }) if !stderr.contains(expected_output) => { bail!(ErrorKind::ErrorOutputMismatch( self.cmd.clone(), expected_output.clone(), stderr.into(), )); }, - (Some(ref expected_output), false) => { + Some(OutputAssertion { + expect: ref expected_output, + fuzzy: false, + }) => { let differences = Changeset::new(expected_output.trim(), stderr.trim(), "\n"); if differences.distance > 0 { let nice_diff = diff::render(&differences)?; From fcd20f163981a6b61e91ed6e3f74f8633d1a16b4 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Tue, 21 Mar 2017 22:39:06 +0100 Subject: [PATCH 06/10] refactor output assertion II --- src/errors.rs | 29 ++++-------------- src/lib.rs | 84 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 2c0b44a..e965eb5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -26,40 +26,23 @@ error_chain! { got, ) } - OutputMismatch(cmd: Vec, expected: String, got: String) { + OutputMismatch(output_name: String, cmd: Vec, expected: String, got: String) { description("Output was not as expected") display( - "{}: `(output of `{}` expected to contain `{:?}`)` (output was: `{:?}`)", + "{}: `({} of `{}` expected to contain `{:?}`)` (output was: `{:?}`)", ERROR_PREFIX, + output_name, cmd.join(" "), expected, got, ) } - ExactOutputMismatch(cmd: Vec, diff: String) { + ExactOutputMismatch(output_name: String, cmd: Vec, diff: String) { description("Output was not as expected") display( - "{}: `(output of `{}` was not as expected)`\n{}\n", - ERROR_PREFIX, - cmd.join(" "), - diff.trim() - ) - } - ErrorOutputMismatch(cmd: Vec, expected: String, got: String) { - description("Stderr output was not as expected") - display( - "{}: `(stderr output of `{}` expected to contain `{:?}`)` (stderr was: `{:?}`)", - ERROR_PREFIX, - cmd.join(" "), - expected, - got, - ) - } - ExactErrorOutputMismatch(cmd: Vec, diff: String) { - description("Stderr output was not as expected") - display( - "{}: `(stderr output of `{}` was not as expected)`\n{}\n", + "{}: `({} of `{}` was not as expected)`\n{}\n", ERROR_PREFIX, + output_name, cmd.join(" "), diff.trim() ) diff --git a/src/lib.rs b/src/lib.rs index 42bf865..780900d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,8 @@ extern crate difference; #[macro_use] extern crate error_chain; -use std::process::Command; +use std::process::{Command, Output}; +use std::fmt; use difference::Changeset; @@ -101,6 +102,30 @@ struct OutputAssertion { fuzzy: bool, } +#[derive(Debug, Copy, Clone)] +enum OutputType { + StdOut, + StdErr, +} + +impl OutputType { + fn select<'a>(&self, o: &'a Output) -> &'a [u8] { + match *self { + OutputType::StdOut => &o.stdout, + OutputType::StdErr => &o.stderr, + } + } +} + +impl fmt::Display for OutputType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + OutputType::StdOut => write!(f, "stdout"), + OutputType::StdErr => write!(f, "stderr"), + } + } +} + impl std::default::Default for Assert { /// Construct an assert using `cargo run --` as command. /// @@ -357,57 +382,52 @@ impl Assert { )); } - let stdout = String::from_utf8_lossy(&output.stdout); - match self.expect_stdout { + self.assert_output(OutputType::StdOut, &output)?; + self.assert_output(OutputType::StdErr, &output)?; + + Ok(()) + } + + /// Perform the appropriate output assertion. + fn assert_output(&self, output_type: OutputType, output: &Output) -> Result<()> { + let observed = String::from_utf8_lossy(output_type.select(output)); + match *self.expect_output(output_type) { Some(OutputAssertion { expect: ref expected_output, fuzzy: true, - }) if !stdout.contains(expected_output) => { + }) if !observed.contains(expected_output) => { bail!(ErrorKind::OutputMismatch( + output_type.to_string(), self.cmd.clone(), expected_output.clone(), - stdout.into(), + observed.into(), )); }, Some(OutputAssertion { expect: ref expected_output, fuzzy: false, }) => { - let differences = Changeset::new(expected_output.trim(), stdout.trim(), "\n"); + let differences = Changeset::new(expected_output.trim(), observed.trim(), "\n"); if differences.distance > 0 { let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::ExactOutputMismatch(self.cmd.clone(), nice_diff)); + bail!(ErrorKind::ExactOutputMismatch( + output_type.to_string(), + self.cmd.clone(), + nice_diff + )); } }, _ => {}, } + Ok(()) + } - let stderr = String::from_utf8_lossy(&output.stderr); - match self.expect_stderr { - Some(OutputAssertion { - expect: ref expected_output, - fuzzy: true, - }) if !stderr.contains(expected_output) => { - bail!(ErrorKind::ErrorOutputMismatch( - self.cmd.clone(), - expected_output.clone(), - stderr.into(), - )); - }, - Some(OutputAssertion { - expect: ref expected_output, - fuzzy: false, - }) => { - let differences = Changeset::new(expected_output.trim(), stderr.trim(), "\n"); - if differences.distance > 0 { - let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::ExactErrorOutputMismatch(self.cmd.clone(),nice_diff)); - } - }, - _ => {}, + /// Return a reference to the appropriate output assertion. + fn expect_output(&self, output_type: OutputType) -> &Option { + match output_type { + OutputType::StdOut => &self.expect_stdout, + OutputType::StdErr => &self.expect_stderr, } - - Ok(()) } /// Execute the command, check the assertions, and panic when they fail. From 1f29f4f25978bedf0bf476f5a27bedb0ea6a83e5 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Tue, 21 Mar 2017 22:41:17 +0100 Subject: [PATCH 07/10] allow matching without asserting the exit status --- src/lib.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 780900d..1c2a0ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,7 +90,7 @@ mod diff; #[derive(Debug)] pub struct Assert { cmd: Vec, - expect_success: bool, + expect_success: Option, expect_exit_code: Option, expect_stdout: Option, expect_stderr: Option, @@ -134,7 +134,7 @@ impl std::default::Default for Assert { Assert { cmd: vec!["cargo", "run", "--"] .into_iter().map(String::from).collect(), - expect_success: true, + expect_success: None, expect_exit_code: None, expect_stdout: None, expect_stderr: None, @@ -216,8 +216,6 @@ impl Assert { /// Expect the command to be executed successfully. /// - /// Note: This is already set by default, so you only need this for explicitness. - /// /// # Examples /// /// ```rust @@ -228,7 +226,8 @@ impl Assert { /// .unwrap(); /// ``` pub fn succeeds(mut self) -> Self { - self.expect_success = true; + self.expect_exit_code = None; + self.expect_success = Some(true); self } @@ -247,7 +246,7 @@ impl Assert { /// .unwrap(); /// ``` pub fn fails(mut self) -> Self { - self.expect_success = false; + self.expect_success = Some(false); self } @@ -263,7 +262,7 @@ impl Assert { /// .unwrap(); /// ``` pub fn fails_with(mut self, expect_exit_code: i32) -> Self { - self.expect_success = false; + self.expect_success = Some(false); self.expect_exit_code = Some(expect_exit_code); self } @@ -366,11 +365,13 @@ impl Assert { let output = command.output()?; - if self.expect_success != output.status.success() { - bail!(ErrorKind::StatusMismatch( - self.cmd.clone(), - self.expect_success.clone(), - )); + 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() && From f45db8563926e2077dc3b642b55feb5ce59e8acb Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Wed, 22 Mar 2017 21:23:39 +0100 Subject: [PATCH 08/10] refactor `assert_cmd!` macro - fix #22 - fix #15 --- Cargo.toml | 1 + README.md | 6 ++-- src/lib.rs | 42 +++------------------- src/macros.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 41 deletions(-) create mode 100644 src/macros.rs diff --git a/Cargo.toml b/Cargo.toml index 398a65a..865b246 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ build = "build.rs" colored = "1.4" difference = "1.0" error-chain = "0.10.0" +rustc-serialize = "0.3" [build-dependencies] skeptic = "0.5" diff --git a/README.md b/README.md index f6edb97..e3f787e 100644 --- a/README.md +++ b/README.md @@ -42,17 +42,17 @@ And here is one that will fail (which also shows `execute` which returns a #[macro_use] extern crate assert_cli; fn main() { - let test = assert_cmd!(grep amet Cargo.toml) + let test = assert_cmd!(grep amet "Cargo.toml") .fails_with(1) .execute(); - assert!(test.is_err()); + assert!(test.is_ok()); } ``` If you want to match the program's output _exactly_, you can use `prints_exactly`: -```rust,should_panic="Assert CLI failure" +```rust,should_panic #[macro_use] extern crate assert_cli; fn main() { diff --git a/src/lib.rs b/src/lib.rs index 1c2a0ab..f858b87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,7 @@ extern crate difference; #[macro_use] extern crate error_chain; +extern crate rustc_serialize; use std::process::{Command, Output}; use std::fmt; @@ -84,6 +85,9 @@ use difference::Changeset; mod errors; use errors::*; +#[macro_use] mod macros; +pub use macros::flatten_escaped_string; + mod diff; /// Assertions for a specific command. @@ -448,41 +452,3 @@ impl Assert { } } } - -/// Easily construct an `Assert` with a custom command. -/// -/// Make sure to include the crate as `#[macro_use] extern crate assert_cli;` if -/// you want to use this macro. -/// -/// # Examples -/// -/// To test that our very complex cli applications succeeds and prints some -/// text to stdout that contains -/// -/// ```plain -/// No errors whatsoever -/// ``` -/// -/// ..., you would call it like this: -/// -/// ```rust -/// #[macro_use] extern crate assert_cli; -/// # fn main() { -/// assert_cmd!(echo "Launch sequence initiated.\nNo errors whatsoever!\n") -/// .succeeds() -/// .prints("No errors whatsoever") -/// .unwrap(); -/// # } -/// ``` -/// -/// The macro will try to convert its arguments as strings, but is limited by -/// Rust's default tokenizer, e.g., you always need to quote CLI arguments -/// like `"--verbose"`. -#[macro_export] -macro_rules! assert_cmd { - ($($x:tt)+) => {{ - $crate::Assert::command( - &[$(stringify!($x)),*] - ) - }} -} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..e5e1145 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,99 @@ +use std::borrow::Cow; +use rustc_serialize::json::Json; + +/// Easily construct an `Assert` with a custom command. +/// +/// Make sure to include the crate as `#[macro_use] extern crate assert_cli;` if +/// you want to use this macro. +/// +/// # Examples +/// +/// To test that our very complex cli applications succeeds and prints some +/// text to stdout that contains +/// +/// ```plain +/// No errors whatsoever +/// ``` +/// +/// ..., you would call it like this: +/// +/// ```rust +/// #[macro_use] extern crate assert_cli; +/// # fn main() { +/// assert_cmd!(echo "Launch sequence initiated.\nNo errors whatsoever!\n") +/// .succeeds() +/// .prints("No errors whatsoever") +/// .unwrap(); +/// # } +/// ``` +/// +/// The macro will try to convert its arguments as strings, but is limited by +/// Rust's default tokenizer, e.g., you always need to quote CLI arguments +/// like `"--verbose"`. +#[macro_export] +macro_rules! assert_cmd { + ($($x:tt)+) => {{ + $(__assert_single_token_expression!(@CHECK $x);)* + + $crate::Assert::command( + &[$( + $crate::flatten_escaped_string(stringify!($x)).as_ref() + ),*] + ) + }} +} + +/// Deserialize a JSON-encoded `String`. +/// +/// # Panics +/// +/// 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), + } +} + +/// Deserialize a JSON-encoded `String`. +/// +/// # Panics +/// +/// If `x` can not be decoded as `String`. +#[doc(hidden)] +pub fn flatten_escaped_string(x: &str) -> Cow { + if x.starts_with('"') && x.ends_with('"') { + Cow::Owned(deserialize_json_string(x)) + } else { + Cow::Borrowed(x) + } +} + +/// Inspect a single token and decide if it is safe to `stringify!`, without loosing +/// information about whitespaces, to address https://github.com/killercup/assert_cli/issues/22. +/// +/// Call like `__assert_single_token_expression!(@CHECK x)`, where `x` can be any token to check. +/// +/// This macro will only accept single tokens, which parse as expressions, e.g. +/// - strings "foo", r#"foo" +/// - idents `foo`, `foo42` +/// - numbers `42` +/// - chars `'a'` +/// +/// Delimited token trees `{...}` and the like are rejected. Everything thats not an expression +/// will also be rejected. +#[doc(hidden)] +#[macro_export] +macro_rules! __assert_single_token_expression { + // deny `{...}` + (@CHECK {$( $x:tt )*}) => { assert_cmd!(@DENY {$( $x )*}) }; + // deny `(...)` + (@CHECK ($( $x:tt )*)) => { assert_cmd!(@DENY {$( $x )*}) }; + // deny `[...]` + (@CHECK [$( $x:tt )*]) => { assert_cmd!(@DENY {$( $x )*}) }; + // only allow tokens that parse as expression + (@CHECK $x:expr) => { }; + // little helper + (@DENY) => { }; +} From 8a01125acd4a6d0bac75adac1d7be37c27ef2acf Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Wed, 22 Mar 2017 23:55:45 +0100 Subject: [PATCH 09/10] expect success by default + cleanup --- src/errors.rs | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index e965eb5..e4b7ae8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,4 +1,4 @@ -static ERROR_PREFIX: &'static str = "CLI assertion failed"; +const ERROR_PREFIX: &'static str = "CLI assertion failed"; error_chain! { foreign_links { diff --git a/src/lib.rs b/src/lib.rs index f858b87..98f4787 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,7 +138,7 @@ impl std::default::Default for Assert { Assert { cmd: vec!["cargo", "run", "--"] .into_iter().map(String::from).collect(), - expect_success: None, + expect_success: Some(true), expect_exit_code: None, expect_stdout: None, expect_stderr: None, From e307b80f9c1bd48fd991a5443b394f2bb2d0ca20 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Wed, 22 Mar 2017 23:56:52 +0100 Subject: [PATCH 10/10] some doc changes - explain asserting the exit codes - better reflect the fact that `success` is expected by default - tip/hint about the macro limitation --- README.md | 16 +++++++--- src/lib.rs | 90 +++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e3f787e..ae97974 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,13 @@ fn main() { } ``` -Or if you'd rather use the macro: +Or if you'd rather use the macro, to save you some writing: ```rust #[macro_use] extern crate assert_cli; fn main() { - assert_cmd!(echo 42).succeeds().and().prints("42").unwrap(); + assert_cmd!(echo "42").prints("42").unwrap(); } ``` @@ -42,8 +42,10 @@ And here is one that will fail (which also shows `execute` which returns a #[macro_use] extern crate assert_cli; fn main() { - let test = assert_cmd!(grep amet "Cargo.toml") - .fails_with(1) + let test = assert_cmd!(ls "foo-bar-foo") + .fails() + .and() + .prints_error("foo-bar-foo") .execute(); assert!(test.is_ok()); } @@ -56,7 +58,7 @@ If you want to match the program's output _exactly_, you can use #[macro_use] extern crate assert_cli; fn main() { - assert_cmd!("wc" "README.md") + assert_cmd!(wc "README.md") .prints_exactly("1337 README.md") .unwrap(); } @@ -70,6 +72,10 @@ like this: +92 ``` +**Tip**: Enclose arguments in the `assert_cmd!` macro in quotes `"`, + if there are special characters, which the macro doesn't accept, e.g. + `assert_cmd!(cat "foo.txt")`. + More detailed information is available in the [documentation]. :-) ## License diff --git a/src/lib.rs b/src/lib.rs index 98f4787..80e28bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,8 +18,7 @@ //! //! ```rust //! assert_cli::Assert::command(&["echo", "42"]) -//! .succeeds() -//! .and().prints("42") +//! .prints("42") //! .unwrap(); //! ``` //! @@ -38,25 +37,57 @@ //! +42 //! ``` //! -//! ## Assert CLI Crates +//! ## `assert_cmd!` Macro //! -//! If you are testing a Rust binary crate, you can start with -//! `Assert::main_binary()` to use `cargo run` as command. Or, if you want to -//! run a specific binary (if you have more than one), use -//! `Assert::cargo_binary`. +//! Alternatively, you can use the `assert_cmd!` macro to construct the command more conveniently, +//! but please carefully read the limitations below, or this may seriously go wrong. //! -//! ## `assert_cmd!` Macro +//! ```rust +//! # #[macro_use] extern crate assert_cli; +//! # fn main() { +//! assert_cmd!(echo "42").prints("42").unwrap(); +//! # } +//! ``` +//! +//! **Tips** +//! +//! - Don't forget to import the crate with `#[macro_use]`. ;-) +//! - Enclose arguments in the `assert_cmd!` macro in quotes `"`, +//! if there are special characters, which the macro doesn't accept, e.g. +//! `assert_cmd!(cat "foo.txt")`. +//! +//! ## Exit Status //! -//! Alternatively, you can use the `assert_cmd!` macro to construct the command more conveniently: +//! All assertion default to checking that the command exited with success. +//! +//! However, when you expect a command to fail, you can express it like this: //! //! ```rust //! # #[macro_use] extern crate assert_cli; //! # fn main() { -//! assert_cmd!(echo 42).succeeds().prints("42").unwrap(); +//! assert_cmd!(cat "non-existing-file") +//! .fails() +//! .and() +//! .prints_error("non-existing-file") +//! .unwrap(); //! # } //! ``` //! -//! Don't forget to import the crate with `#[macro_use]`. ;-) +//! Some notes on this: +//! +//! - 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. :-) +//! +//! ## Assert CLI Crates +//! +//! If you are testing a Rust binary crate, you can start with +//! `Assert::main_binary()` to use `cargo run` as command. Or, if you want to +//! run a specific binary (if you have more than one), use +//! `Assert::cargo_binary`. //! //! ## Don't Panic! //! @@ -66,7 +97,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").prints_exactly("42").execute(); //! assert!(x.is_err()); //! # } //! ``` @@ -175,7 +206,6 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "1337"]) - /// .succeeds() /// .unwrap(); /// ``` pub fn command(cmd: &[&str]) -> Self { @@ -194,7 +224,6 @@ impl Assert { /// /// assert_cli::Assert::command(&["echo"]) /// .with_args(&["42"]) - /// .succeeds() /// .prints("42") /// .unwrap(); /// ``` @@ -211,7 +240,7 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .succeeds().and().prints("42") + /// .prints("42") /// .unwrap(); /// ``` pub fn and(self) -> Self { @@ -226,7 +255,6 @@ impl Assert { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .succeeds() /// .unwrap(); /// ``` pub fn succeeds(mut self) -> Self { @@ -245,8 +273,10 @@ impl Assert { /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-exisiting-file"]) + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) /// .fails() + /// .and() + /// .prints_error("non-existing-file") /// .unwrap(); /// ``` pub fn fails(mut self) -> Self { @@ -261,8 +291,10 @@ impl Assert { /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-exisiting-file"]) + /// 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 { @@ -271,7 +303,7 @@ impl Assert { self } - /// Expect the command's output to contain `output`. + /// Expect the command's output to **contain** `output`. /// /// # Examples /// @@ -290,7 +322,7 @@ impl Assert { self } - /// Expect the command to output exactly this `output`. + /// Expect the command to output **exactly** this `output`. /// /// # Examples /// @@ -309,16 +341,17 @@ impl Assert { self } - /// Expect the command's stderr output to contain `output`. + /// Expect the command's stderr output to **contain** `output`. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-exisiting-file"]) + /// assert_cli::Assert::command(&["cat", "non-existing-file"]) /// .fails() - /// .prints_error("non-exisiting-file") + /// .and() + /// .prints_error("non-existing-file") /// .unwrap(); /// ``` pub fn prints_error>(mut self, output: O) -> Self { @@ -329,16 +362,17 @@ impl Assert { self } - /// Expect the command to output exactly this `output` to stderr. + /// Expect the command to output **exactly** this `output` to stderr. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["cat", "non-exisiting-file"]) - /// .fails() - /// .prints_error_exactly("cat: non-exisiting-file: No such file or directory") + /// 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 { @@ -357,7 +391,7 @@ impl Assert { /// extern crate assert_cli; /// /// let test = assert_cli::Assert::command(&["echo", "42"]) - /// .succeeds() + /// .prints("42") /// .execute(); /// assert!(test.is_ok()); /// ```