diff --git a/src/assert.rs b/src/assert.rs index 8d0ba0e..bccc54e 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -1,7 +1,7 @@ use environment::Environment; use error_chain::ChainedError; use errors::*; -use output::{OutputAssertion, OutputKind}; +use output::{Content, Output, OutputKind, OutputPredicate}; use std::default; use std::ffi::{OsStr, OsString}; use std::io::Write; @@ -18,8 +18,8 @@ pub struct Assert { current_dir: Option, expect_success: Option, expect_exit_code: Option, - expect_output: Vec, - stdin_contents: Option, + expect_output: Vec, + stdin_contents: Option>, } impl default::Default for Assert { @@ -118,8 +118,8 @@ impl Assert { /// .stdout().contains("42") /// .unwrap(); /// ``` - pub fn stdin(mut self, contents: &str) -> Self { - self.stdin_contents = Some(String::from(contents)); + pub fn stdin>>(mut self, contents: S) -> Self { + self.stdin_contents = Some(contents.into()); self } @@ -289,7 +289,6 @@ impl Assert { OutputAssertionBuilder { assertion: self, kind: OutputKind::StdOut, - expected_result: true, } } @@ -310,7 +309,6 @@ impl Assert { OutputAssertionBuilder { assertion: self, kind: OutputKind::StdErr, - expected_result: true, } } @@ -327,10 +325,10 @@ impl Assert { /// assert!(test.is_ok()); /// ``` pub fn execute(self) -> Result<()> { - let cmd = &self.cmd[0]; + let bin = &self.cmd[0]; let args: Vec<_> = self.cmd.iter().skip(1).collect(); - let mut command = Command::new(cmd); + let mut command = Command::new(bin); let command = command .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -353,7 +351,7 @@ impl Assert { .stdin .as_mut() .expect("Couldn't get mut ref to command stdin") - .write_all(contents.as_bytes())?; + .write_all(contents)?; } let output = spawned.wait_with_output()?; @@ -361,30 +359,26 @@ impl Assert { if expect_success != output.status.success() { let out = String::from_utf8_lossy(&output.stdout).to_string(); let err = String::from_utf8_lossy(&output.stderr).to_string(); - bail!(ErrorKind::StatusMismatch( - self.cmd.clone(), - expect_success, - out, - err, - )); + let err: Error = ErrorKind::StatusMismatch(expect_success, out, err).into(); + bail!(err.chain_err(|| ErrorKind::AssertionFailed(self.cmd.clone()))); } } if self.expect_exit_code.is_some() && self.expect_exit_code != output.status.code() { let out = String::from_utf8_lossy(&output.stdout).to_string(); let err = String::from_utf8_lossy(&output.stderr).to_string(); - bail!(ErrorKind::ExitCodeMismatch( - self.cmd.clone(), - self.expect_exit_code, - output.status.code(), - out, - err, - )); + let err: Error = + ErrorKind::ExitCodeMismatch(self.expect_exit_code, output.status.code(), out, err) + .into(); + bail!(err.chain_err(|| ErrorKind::AssertionFailed(self.cmd.clone()))); } self.expect_output .iter() - .map(|a| a.execute(&output, &self.cmd)) + .map(|a| { + a.verify(&output) + .chain_err(|| ErrorKind::AssertionFailed(self.cmd.clone())) + }) .collect::>>()?; Ok(()) @@ -414,11 +408,10 @@ impl Assert { pub struct OutputAssertionBuilder { assertion: Assert, kind: OutputKind, - expected_result: bool, } impl OutputAssertionBuilder { - /// Negate the assertion predicate + /// Expect the command's output to **contain** `output`. /// /// # Examples /// @@ -426,17 +419,16 @@ impl OutputAssertionBuilder { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .stdout().not().contains("73") + /// .stdout().contains("42") /// .unwrap(); /// ``` - // No clippy, we don't want to implement std::ops::Not :) - #[cfg_attr(feature = "cargo-clippy", allow(should_implement_trait))] - pub fn not(mut self) -> Self { - self.expected_result = !self.expected_result; - self + pub fn contains>(mut self, output: O) -> Assert { + let pred = OutputPredicate::new(self.kind, Output::contains(output)); + self.assertion.expect_output.push(pred); + self.assertion } - /// Expect the command's output to **contain** `output`. + /// Expect the command to output **exactly** this `output`. /// /// # Examples /// @@ -444,20 +436,16 @@ impl OutputAssertionBuilder { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .stdout().contains("42") + /// .stdout().is("42") /// .unwrap(); /// ``` - pub fn contains>(mut self, output: O) -> Assert { - self.assertion.expect_output.push(OutputAssertion { - expect: output.into(), - fuzzy: true, - expected_result: self.expected_result, - kind: self.kind, - }); + pub fn is>(mut self, output: O) -> Assert { + let pred = OutputPredicate::new(self.kind, Output::is(output)); + self.assertion.expect_output.push(pred); self.assertion } - /// Expect the command to output **exactly** this `output`. + /// Expect the command's output to not **contain** `output`. /// /// # Examples /// @@ -465,20 +453,16 @@ impl OutputAssertionBuilder { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .stdout().is("42") + /// .stdout().doesnt_contain("73") /// .unwrap(); /// ``` - pub fn is>(mut self, output: O) -> Assert { - self.assertion.expect_output.push(OutputAssertion { - expect: output.into(), - fuzzy: false, - expected_result: self.expected_result, - kind: self.kind, - }); + pub fn doesnt_contain>(mut self, output: O) -> Assert { + let pred = OutputPredicate::new(self.kind, Output::doesnt_contain(output)); + self.assertion.expect_output.push(pred); self.assertion } - /// Expect the command's output to not **contain** `output`. + /// Expect the command to output to not be **exactly** this `output`. /// /// # Examples /// @@ -486,26 +470,34 @@ impl OutputAssertionBuilder { /// extern crate assert_cli; /// /// assert_cli::Assert::command(&["echo", "42"]) - /// .stdout().doesnt_contain("73") + /// .stdout().isnt("73") /// .unwrap(); /// ``` - pub fn doesnt_contain>(self, output: O) -> Assert { - self.not().contains(output) + pub fn isnt>(mut self, output: O) -> Assert { + let pred = OutputPredicate::new(self.kind, Output::isnt(output)); + self.assertion.expect_output.push(pred); + self.assertion } - /// Expect the command to output to not be **exactly** this `output`. + /// Expect the command output to satisfy the given predicate. /// /// # Examples /// /// ```rust /// extern crate assert_cli; /// - /// assert_cli::Assert::command(&["echo", "42"]) - /// .stdout().isnt("73") + /// assert_cli::Assert::command(&["echo", "-n", "42"]) + /// .stdout().satisfies(|x| x.len() == 2, "bad length") /// .unwrap(); /// ``` - pub fn isnt>(self, output: O) -> Assert { - self.not().is(output) + pub fn satisfies(mut self, pred: F, msg: M) -> Assert + where + F: 'static + Fn(&str) -> bool, + M: Into, + { + let pred = OutputPredicate::new(self.kind, Output::satisfies(pred, msg)); + self.assertion.expect_output.push(pred); + self.assertion } } @@ -522,7 +514,11 @@ mod test { fn take_ownership() { let x = Environment::inherit(); - command().with_env(x.clone()).with_env(&x).with_env(x); + command() + .with_env(x.clone()) + .with_env(&x) + .with_env(x) + .unwrap(); } #[test] @@ -564,8 +560,7 @@ mod test { command() .with_env(y) .stdout() - .not() - .contains("key=value") + .doesnt_contain("key=value") .execute() .unwrap(); } diff --git a/src/errors.rs b/src/errors.rs index d809f91..8f3172e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,6 +11,9 @@ fn format_cmd(cmd: &[OsString]) -> String { } error_chain! { + links { + Output(output::Error, output::ErrorKind); + } foreign_links { Io(::std::io::Error); Fmt(::std::fmt::Error); @@ -24,12 +27,18 @@ error_chain! { format_cmd(cmd), ) } - StatusMismatch(cmd: Vec, expected: bool, out: String, err: String) { - description("Wrong status") + AssertionFailed(cmd: Vec) { + description("Assertion failed") display( - "{}: (command `{}` expected to {})\nstatus={}\nstdout=```{}```\nstderr=```{}```", + "{}: (command `{}` failed)", ERROR_PREFIX, format_cmd(cmd), + ) + } + StatusMismatch(expected: bool, out: String, err: String) { + description("Wrong status") + display( + "Expected to {}\nstatus={}\nstdout=```{}```\nstderr=```{}```", expected = if *expected { "succeed" } else { "fail" }, got = if *expected { "failed" } else { "succeeded" }, out = out, @@ -37,7 +46,6 @@ error_chain! { ) } ExitCodeMismatch( - cmd: Vec, expected: Option, got: Option, out: String, @@ -45,25 +53,15 @@ error_chain! { ) { description("Wrong exit code") display( - "{prefix}: (exit code of `{cmd}` expected to be `{expected:?}`)\n\ + "Expected exit code to be `{expected:?}`)\n\ exit code=`{code:?}`\n\ stdout=```{stdout}```\n\ stderr=```{stderr}```", - prefix=ERROR_PREFIX, - cmd=format_cmd(cmd), expected=expected, code=got, stdout=out, stderr=err, ) } - OutputMismatch(cmd: Vec, output_err: output::Error, kind: output::OutputKind) { - description("Output was not as expected") - display( - "{}: `{}` {:?} mismatch: {}", - ERROR_PREFIX, format_cmd(cmd), kind, output_err, - ) - } - } } diff --git a/src/lib.rs b/src/lib.rs index 8d56f1d..12def0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,11 +129,10 @@ mod errors; mod macros; pub use macros::flatten_escaped_string; -mod output; - +mod assert; mod diff; +mod output; -mod assert; pub use assert::Assert; pub use assert::OutputAssertionBuilder; /// Environment is a re-export of the Environment crate diff --git a/src/output.rs b/src/output.rs index 207cec6..3395f94 100644 --- a/src/output.rs +++ b/src/output.rs @@ -2,70 +2,314 @@ use self::errors::*; pub use self::errors::{Error, ErrorKind}; use diff; use difference::Changeset; -use std::ffi::OsString; -use std::process::Output; +use std::fmt; +use std::process; +use std::rc; -#[derive(Debug, Clone)] -pub struct OutputAssertion { - pub expect: String, - pub fuzzy: bool, + +#[derive(Clone, PartialEq, Eq)] +pub enum Content { + Str(String), + Bytes(Vec), +} + +impl fmt::Debug for Content { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Content::Str(ref data) => write!(f, "{}", data), + Content::Bytes(ref data) => write!(f, "{:?}", data), + } + } +} + +impl<'a> From<&'a str> for Content { + fn from(data: &'a str) -> Self { + Content::Str(data.into()) + } +} + +impl<'a> From<&'a [u8]> for Content { + fn from(data: &'a [u8]) -> Self { + Content::Bytes(data.into()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct IsPredicate { + pub expect: Content, pub expected_result: bool, - pub kind: OutputKind, } -impl OutputAssertion { - fn matches_fuzzy(&self, got: &str) -> Result<()> { - let result = got.contains(&self.expect); +impl IsPredicate { + pub fn verify(&self, got: &[u8]) -> Result<()> { + match self.expect { + Content::Str(ref expect) => { + self.verify_str(expect, String::from_utf8_lossy(got).as_ref()) + } + Content::Bytes(ref expect) => self.verify_bytes(expect, got), + } + } + + fn verify_bytes(&self, expect: &[u8], got: &[u8]) -> Result<()> { + let result = expect == got; + if result != self.expected_result { if self.expected_result { - bail!(ErrorKind::OutputDoesntContain( - self.expect.clone(), - got.into() + bail!(ErrorKind::BytesDoesntMatch( + expect.to_owned(), + got.to_owned(), )); } else { - bail!(ErrorKind::OutputContains(self.expect.clone(), got.into())); + bail!(ErrorKind::BytesMatches(got.to_owned())); } } Ok(()) } - fn matches_exact(&self, got: &str) -> Result<()> { - let differences = Changeset::new(self.expect.trim(), got.trim(), "\n"); + fn verify_str(&self, expect: &str, got: &str) -> Result<()> { + let differences = Changeset::new(expect.trim(), got.trim(), "\n"); let result = differences.distance == 0; if result != self.expected_result { if self.expected_result { let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::OutputDoesntMatch( - self.expect.clone(), + bail!(ErrorKind::StrDoesntMatch( + expect.to_owned(), got.to_owned(), nice_diff )); } else { - bail!(ErrorKind::OutputMatches(got.to_owned())); + bail!(ErrorKind::StrMatches(got.to_owned())); } } Ok(()) } +} - pub fn execute(&self, output: &Output, cmd: &[OsString]) -> super::errors::Result<()> { - let observed = String::from_utf8_lossy(self.kind.select(output)); +#[derive(Debug, Clone, PartialEq, Eq)] +struct ContainsPredicate { + pub expect: Content, + pub expected_result: bool, +} - let result = if self.fuzzy { - self.matches_fuzzy(&observed) - } else { - self.matches_exact(&observed) - }; - result.map_err(|e| { - super::errors::ErrorKind::OutputMismatch(cmd.to_vec(), e, self.kind) - })?; +fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option { + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +#[test] +fn test_find_subsequence() { + assert_eq!(find_subsequence(b"qwertyuiop", b"tyu"), Some(4)); + assert_eq!(find_subsequence(b"qwertyuiop", b"asd"), None); +} + +impl ContainsPredicate { + pub fn verify(&self, got: &[u8]) -> Result<()> { + match self.expect { + Content::Str(ref expect) => { + self.verify_str(expect, String::from_utf8_lossy(got).as_ref()) + } + Content::Bytes(ref expect) => self.verify_bytes(expect, got), + } + } + + pub fn verify_bytes(&self, expect: &[u8], got: &[u8]) -> Result<()> { + let result = find_subsequence(got, expect).is_some(); + if result != self.expected_result { + if self.expected_result { + bail!(ErrorKind::BytesDoesntContain( + expect.to_owned(), + got.to_owned() + )); + } else { + bail!(ErrorKind::BytesContains(expect.to_owned(), got.to_owned())); + } + } + + Ok(()) + } + + pub fn verify_str(&self, expect: &str, got: &str) -> Result<()> { + let result = got.contains(expect); + if result != self.expected_result { + if self.expected_result { + bail!(ErrorKind::StrDoesntContain( + expect.to_owned(), + got.to_owned() + )); + } else { + bail!(ErrorKind::StrContains(expect.to_owned(), got.to_owned())); + } + } + + Ok(()) + } +} + +#[derive(Clone)] +struct FnPredicate { + pub pred: rc::Rc bool>, + pub msg: String, +} + +impl FnPredicate { + pub fn verify(&self, got: &[u8]) -> Result<()> { + let got = String::from_utf8_lossy(got); + let pred = &self.pred; + if !pred(&got) { + let err: Error = ErrorKind::PredicateFailed(got.into_owned(), self.msg.clone()).into(); + bail!(err); + } Ok(()) } } +impl fmt::Debug for FnPredicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.msg) + } +} + +#[derive(Debug, Clone)] +enum ContentPredicate { + Is(IsPredicate), + Contains(ContainsPredicate), + Fn(FnPredicate), +} + +impl ContentPredicate { + pub fn verify(&self, got: &[u8]) -> Result<()> { + match *self { + ContentPredicate::Is(ref pred) => pred.verify(got), + ContentPredicate::Contains(ref pred) => pred.verify(got), + ContentPredicate::Fn(ref pred) => pred.verify(got), + } + } +} + +/// Assertions for command output. +#[derive(Debug, Clone)] +pub struct Output { + pred: ContentPredicate, +} + +impl Output { + /// Expect the command's output to **contain** `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo"]) + /// .with_args(&["42"]) + /// .stdout().contains("42") + /// .unwrap(); + /// ``` + pub fn contains>(output: O) -> Self { + let pred = ContainsPredicate { + expect: output.into(), + expected_result: true, + }; + Self::new(ContentPredicate::Contains(pred)) + } + + /// Expect the command to output **exactly** this `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo"]) + /// .with_args(&["42"]) + /// .stdout().is("42") + /// .unwrap(); + /// ``` + pub fn is>(output: O) -> Self { + let pred = IsPredicate { + expect: output.into(), + expected_result: true, + }; + Self::new(ContentPredicate::Is(pred)) + } + + /// Expect the command's output to not **contain** `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo"]) + /// .with_args(&["42"]) + /// .stdout().doesnt_contain("73") + /// .unwrap(); + /// ``` + pub fn doesnt_contain>(output: O) -> Self { + let pred = ContainsPredicate { + expect: output.into(), + expected_result: false, + }; + Self::new(ContentPredicate::Contains(pred)) + } + + /// Expect the command to output to not be **exactly** this `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo"]) + /// .with_args(&["42"]) + /// .stdout().isnt("73") + /// .unwrap(); + /// ``` + pub fn isnt>(output: O) -> Self { + let pred = IsPredicate { + expect: output.into(), + expected_result: false, + }; + Self::new(ContentPredicate::Is(pred)) + } + + /// Expect the command output to satisfy the given predicate. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "-n", "42"]) + /// .stdout().satisfies(|x| x.len() == 2, "bad length") + /// .unwrap(); + /// ``` + pub fn satisfies(pred: F, msg: M) -> Self + where + F: 'static + Fn(&str) -> bool, + M: Into, + { + let pred = FnPredicate { + pred: rc::Rc::new(pred), + msg: msg.into(), + }; + Self::new(ContentPredicate::Fn(pred)) + } + + fn new(pred: ContentPredicate) -> Self { + Self { pred } + } + + pub(crate) fn verify(&self, got: &[u8]) -> Result<()> { + self.pred.verify(got) + } +} + #[derive(Debug, Clone, Copy)] pub enum OutputKind { StdOut, @@ -73,7 +317,7 @@ pub enum OutputKind { } impl OutputKind { - pub fn select(self, o: &Output) -> &[u8] { + pub fn select(self, o: &process::Output) -> &[u8] { match self { OutputKind::StdOut => &o.stdout, OutputKind::StdErr => &o.stderr, @@ -81,28 +325,74 @@ impl OutputKind { } } +#[derive(Debug, Clone)] +pub struct OutputPredicate { + kind: OutputKind, + pred: Output, +} + +impl OutputPredicate { + pub fn new(kind: OutputKind, pred: Output) -> Self { + Self { kind, pred } + } + + pub(crate) fn verify(&self, got: &process::Output) -> Result<()> { + let got = self.kind.select(got); + self.pred + .verify(got) + .chain_err(|| ErrorKind::OutputMismatch(self.kind)) + } +} + mod errors { error_chain! { foreign_links { Fmt(::std::fmt::Error); } errors { - OutputDoesntContain(expected: String, got: String) { + StrDoesntContain(expected: String, got: String) { description("Output was not as expected") display("expected to contain {:?}\noutput=```{}```", expected, got) } - OutputContains(expected: String, got: String) { + BytesDoesntContain(expected: Vec, got: Vec) { + description("Output was not as expected") + display("expected to contain {:?}\noutput=```{:?}```", expected, got) + } + StrContains(expected: String, got: String) { description("Output was not as expected") display("expected to not contain {:?}\noutput=```{}```", expected, got) } - OutputDoesntMatch(expected: String, got: String, diff: String) { + BytesContains(expected: Vec, got: Vec) { + description("Output was not as expected") + display("expected to not contain {:?}\noutput=```{:?}```", expected, got) + } + StrDoesntMatch(expected: String, got: String, diff: String) { description("Output was not as expected") display("diff:\n{}", diff) } - OutputMatches(got: String) { + BytesDoesntMatch(expected: Vec, got: Vec) { + description("Output was not as expected") + display("expected=```{:?}```\noutput=```{:?}```", expected, got) + } + StrMatches(got: String) { description("Output was not as expected") display("expected to not match\noutput=```{}```", got) } + BytesMatches(got: Vec) { + description("Output was not as expected") + display("expected to not match\noutput=```{:?}```", got) + } + PredicateFailed(got: String, msg: String) { + description("Output predicate failed") + display("{}\noutput=```{}```", msg, got) + } + OutputMismatch(kind: super::OutputKind) { + description("Output was not as expected") + display( + "Unexpected {:?}", + kind + ) + } } } }