diff --git a/src/assert.rs b/src/assert.rs index 74e45c0..b5ac698 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -1,10 +1,12 @@ use environment::Environment; use errors::*; -use output::{OutputAssertion, OutputKind}; +use output::{self, OutputAssertion, OutputKind}; use std::default; use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; +use std::rc::Rc; +use std::result::Result as stdResult; use std::vec::Vec; /// Assertions for a specific command. @@ -409,12 +411,15 @@ impl OutputAssertionBuilder { /// .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, - }); + let expect = output.into(); + let expected_result = self.expected_result; + let test = move |got: &str| output::matches_fuzzy(got, &expect, expected_result); + self.assertion + .expect_output + .push(OutputAssertion { + test: Rc::new(test), + kind: self.kind, + }); self.assertion } @@ -430,12 +435,15 @@ impl OutputAssertionBuilder { /// .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, - }); + let expect = output.into(); + let expected_result = self.expected_result; + let test = move |got: &str| output::matches_exact(got, &expect, expected_result); + self.assertion + .expect_output + .push(OutputAssertion { + test: Rc::new(test), + kind: self.kind, + }); self.assertion } @@ -468,6 +476,72 @@ impl OutputAssertionBuilder { pub fn isnt>(self, output: O) -> Assert { self.not().is(output) } + + /// Expect the command to satisfy the predicate defined by `pred`. + /// `pred` should be a function taking a `&str` and returning `true` + /// if the assertion was correct, or `false` if the assertion should + /// fail. When it fails, it will fail with a `PredicateFails` error. + /// This error will contain no additional data. If this is required + /// then you may want to use `satisfies_ok` instead. + /// + /// # Examples + /// ```rust + /// extern crate assert_cli; + /// + /// // Test for a specific output length + /// assert_cli::Assert::command(&["echo", "-n", "42"]) + /// .stdout().satisfies(|x| x.len() == 2) + /// .unwrap(); + /// + /// // Test a more complex predicate + /// assert_cli::Assert::command(&["echo", "-n", "Hello World!"]) + /// .stdout().satisfies(|x| x.starts_with("Hello") && x.ends_with("World!")) + /// .unwrap(); + /// ``` + pub fn satisfies(mut self, pred: F) -> Assert + where F: 'static + Fn(&str) -> bool + { + let test = move |got: &str| output::matches_pred(got, &pred); + self.assertion + .expect_output + .push(OutputAssertion { + test: Rc::new(test), + kind: self.kind, + }); + self.assertion + } + + /// Expect the command to satisfy the predicate defined by `pred_ok`. + /// Unlike `satisfies`, this function takes a predicate function which + /// gets the command's output as an argument and returns a + /// `Result<(), String>` struct, such that you can specify a custom + /// error for when the assertion fails. + /// + /// # Examples + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo", "-n", "42"]) + /// .stdout().satisfies_ok(|x| { + /// match x.len() { + /// 2 => Ok(()), + /// n => Err(format!("Bad output length: {}", n)), + /// } + /// }) + /// .unwrap(); + /// ``` + pub fn satisfies_ok(mut self, pred_ok: F) -> Assert + where F: 'static + Fn(&str) -> stdResult<(), String> + { + let test = move |got: &str| output::matches_pred_ok(got, &pred_ok); + self.assertion + .expect_output + .push(OutputAssertion { + test: Rc::new(test), + kind: self.kind, + }); + self.assertion + } } #[cfg(test)] diff --git a/src/output.rs b/src/output.rs index d3e9d7b..84e1a03 100644 --- a/src/output.rs +++ b/src/output.rs @@ -2,66 +2,74 @@ use self::errors::*; pub use self::errors::{Error, ErrorKind}; use diff; use difference::Changeset; +use std::fmt; use std::process::Output; +use std::rc::Rc; +use std::result::Result as StdResult; -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct OutputAssertion { - pub expect: String, - pub fuzzy: bool, - pub expected_result: bool, + pub test: Rc Result<()>>, pub kind: OutputKind, } impl OutputAssertion { - fn matches_fuzzy(&self, got: &str) -> Result<()> { - 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())); - } - } - + pub fn execute(&self, output: &Output, cmd: &[String]) -> super::errors::Result<()> { + let observed = String::from_utf8_lossy(self.kind.select(output)); + let result = (self.test)(&observed); + result.map_err(|e| super::errors::ErrorKind::OutputMismatch(cmd.to_vec(), e, self.kind))?; Ok(()) } +} - fn matches_exact(&self, got: &str) -> Result<()> { - let differences = Changeset::new(self.expect.trim(), got.trim(), "\n"); - let result = differences.distance == 0; +impl fmt::Debug for OutputAssertion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "OutputAssertion {{ test: [user supplied closure], kind: {:?} }}", + self.kind) + } +} - if result != self.expected_result { - if self.expected_result { - let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::OutputDoesntMatch( - self.expect.clone(), - got.to_owned(), - nice_diff - )); - } else { - bail!(ErrorKind::OutputMatches(got.to_owned())); - } +pub fn matches_fuzzy(got: &str, expect: &str, expected_result: bool) -> Result<()> { + let result = got.contains(expect); + if result != expected_result { + if expected_result { + bail!(ErrorKind::OutputDoesntContain(expect.into(), got.into())); + } else { + bail!(ErrorKind::OutputContains(expect.into(), got.into())); } - - Ok(()) } - pub fn execute(&self, output: &Output, cmd: &[String]) -> super::errors::Result<()> { - let observed = String::from_utf8_lossy(self.kind.select(output)); + Ok(()) +} + +pub fn matches_exact(got: &str, expect: &str, expected_result: bool) -> Result<()> { + let differences = Changeset::new(expect.trim(), got.trim(), "\n"); + let result = differences.distance == 0; - let result = if self.fuzzy { - self.matches_fuzzy(&observed) + if result != expected_result { + if expected_result { + let nice_diff = diff::render(&differences)?; + bail!(ErrorKind::OutputDoesntMatch(expect.to_owned(), got.to_owned(), nice_diff)); } else { - self.matches_exact(&observed) - }; - result.map_err(|e| { - super::errors::ErrorKind::OutputMismatch(cmd.to_vec(), e, self.kind) - })?; + bail!(ErrorKind::OutputMatches(got.to_owned())); + } + } - Ok(()) + Ok(()) +} + +pub fn matches_pred(got: &str, pred: &Fn(&str) -> bool) -> Result<()> { + match pred(got) { + true => Ok(()), + false => bail!(ErrorKind::PredicateFails(got.to_owned(), None)), + } +} + +pub fn matches_pred_ok(got: &str, pred_ok: &Fn(&str) -> StdResult<(), String>) -> Result<()> { + match pred_ok(got) { + Ok(()) => Ok(()), + Err(s) => bail!(ErrorKind::PredicateFails(got.to_owned(), Some(s.to_owned()))), } } @@ -102,6 +110,10 @@ mod errors { description("Output was not as expected") display("expected to not match\noutput=```{}```", got) } + PredicateFails(got: String, err_str: Option) { + description("User-supplied predicate failed") + display("Error string: {:?}\noutput=```{}```", err_str, got) + } } } }