From a5925b107d5d3ff1b8c357652910821d925d5722 Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Mon, 16 Oct 2017 23:20:55 +0200 Subject: [PATCH 1/2] Rewrite OutputAssertion to use closures --- src/assert.rs | 20 ++++++++++-------- src/output.rs | 57 ++++++++++++--------------------------------------- 2 files changed, 25 insertions(+), 52 deletions(-) diff --git a/src/assert.rs b/src/assert.rs index 74e45c0..398d586 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -8,7 +8,6 @@ use std::process::{Command, Stdio}; use std::vec::Vec; /// Assertions for a specific command. -#[derive(Debug)] pub struct Assert { cmd: Vec, env: Environment, @@ -371,7 +370,6 @@ impl Assert { } /// Assertions for command output. -#[derive(Debug)] pub struct OutputAssertionBuilder { assertion: Assert, kind: OutputKind, @@ -409,10 +407,13 @@ impl OutputAssertionBuilder { /// .unwrap(); /// ``` pub fn contains>(mut self, output: O) -> Assert { + let output_clone = output.into().clone(); + let expected_result = self.expected_result; + let predicate = move |x: &str| { + x.contains(&output_clone) == expected_result + }; self.assertion.expect_output.push(OutputAssertion { - expect: output.into(), - fuzzy: true, - expected_result: self.expected_result, + pred: Box::new(predicate), kind: self.kind, }); self.assertion @@ -430,10 +431,13 @@ impl OutputAssertionBuilder { /// .unwrap(); /// ``` pub fn is>(mut self, output: O) -> Assert { + let output_clone = output.into().clone(); + let expected_result = self.expected_result; + let predicate = move |x: &str| { + (&x.trim() == &output_clone.trim()) == expected_result + }; self.assertion.expect_output.push(OutputAssertion { - expect: output.into(), - fuzzy: false, - expected_result: self.expected_result, + pred: Box::new(predicate), kind: self.kind, }); self.assertion diff --git a/src/output.rs b/src/output.rs index d3e9d7b..f46ebac 100644 --- a/src/output.rs +++ b/src/output.rs @@ -4,58 +4,23 @@ use diff; use difference::Changeset; use std::process::Output; -#[derive(Debug, Clone)] +// TODO(dsprenkels) Should implement Debug. Because of the type of `pred`, we will have +// to do this by hand. pub struct OutputAssertion { - pub expect: String, - pub fuzzy: bool, - pub expected_result: bool, + pub pred: Box bool>, 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())); - } - } - - Ok(()) - } - - fn matches_exact(&self, got: &str) -> Result<()> { - let differences = Changeset::new(self.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(), - got.to_owned(), - nice_diff - )); - } else { - bail!(ErrorKind::OutputMatches(got.to_owned())); - } - } - - Ok(()) - } - pub fn execute(&self, output: &Output, cmd: &[String]) -> super::errors::Result<()> { + // TODO(dsprenkels) There is currently no error reporting. I still have to think + // of a solution that nicely handles all the predefined errors (`OutputDoesntContain` + // etc.) and also handles UserErrors. I may even consider using Any. let observed = String::from_utf8_lossy(self.kind.select(output)); - let result = if self.fuzzy { - self.matches_fuzzy(&observed) - } else { - self.matches_exact(&observed) + let result = match (self.pred)(&observed) { + true => Ok(()), + false => Err(ErrorKind::Unspecified(observed.into()).into()), }; result.map_err(|e| { super::errors::ErrorKind::OutputMismatch(cmd.to_vec(), e, self.kind) @@ -102,6 +67,10 @@ mod errors { description("Output was not as expected") display("expected to not match\noutput=```{}```", got) } + Unspecified(got: String) { + description("Unspecified error") + display("output=```{}```", got) + } } } } From bf2aaccd8ebb48f76b755b4be575f27b86bc02b0 Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Tue, 17 Oct 2017 11:28:29 +0200 Subject: [PATCH 2/2] Make predicate tests and move test to Rc --- src/assert.rs | 104 +++++++++++++++++++++++++++++++++++++++++--------- src/output.rs | 77 ++++++++++++++++++++++++++++--------- 2 files changed, 147 insertions(+), 34 deletions(-) diff --git a/src/assert.rs b/src/assert.rs index 398d586..b5ac698 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -1,13 +1,16 @@ 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. +#[derive(Debug)] pub struct Assert { cmd: Vec, env: Environment, @@ -370,6 +373,7 @@ impl Assert { } /// Assertions for command output. +#[derive(Debug)] pub struct OutputAssertionBuilder { assertion: Assert, kind: OutputKind, @@ -407,15 +411,15 @@ impl OutputAssertionBuilder { /// .unwrap(); /// ``` pub fn contains>(mut self, output: O) -> Assert { - let output_clone = output.into().clone(); + let expect = output.into(); let expected_result = self.expected_result; - let predicate = move |x: &str| { - x.contains(&output_clone) == expected_result - }; - self.assertion.expect_output.push(OutputAssertion { - pred: Box::new(predicate), - kind: self.kind, - }); + 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 } @@ -431,15 +435,15 @@ impl OutputAssertionBuilder { /// .unwrap(); /// ``` pub fn is>(mut self, output: O) -> Assert { - let output_clone = output.into().clone(); + let expect = output.into(); let expected_result = self.expected_result; - let predicate = move |x: &str| { - (&x.trim() == &output_clone.trim()) == expected_result - }; - self.assertion.expect_output.push(OutputAssertion { - pred: Box::new(predicate), - kind: self.kind, - }); + 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 } @@ -472,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 f46ebac..84e1a03 100644 --- a/src/output.rs +++ b/src/output.rs @@ -2,31 +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; -// TODO(dsprenkels) Should implement Debug. Because of the type of `pred`, we will have -// to do this by hand. +#[derive(Clone)] pub struct OutputAssertion { - pub pred: Box bool>, + pub test: Rc Result<()>>, pub kind: OutputKind, } impl OutputAssertion { pub fn execute(&self, output: &Output, cmd: &[String]) -> super::errors::Result<()> { - // TODO(dsprenkels) There is currently no error reporting. I still have to think - // of a solution that nicely handles all the predefined errors (`OutputDoesntContain` - // etc.) and also handles UserErrors. I may even consider using Any. 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(()) + } +} + +impl fmt::Debug for OutputAssertion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "OutputAssertion {{ test: [user supplied closure], kind: {:?} }}", + self.kind) + } +} - let result = match (self.pred)(&observed) { - true => Ok(()), - false => Err(ErrorKind::Unspecified(observed.into()).into()), - }; - result.map_err(|e| { - super::errors::ErrorKind::OutputMismatch(cmd.to_vec(), e, self.kind) - })?; +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(()) + 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; + + 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 { + bail!(ErrorKind::OutputMatches(got.to_owned())); + } + } + + 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()))), } } @@ -67,9 +110,9 @@ mod errors { description("Output was not as expected") display("expected to not match\noutput=```{}```", got) } - Unspecified(got: String) { - description("Unspecified error") - display("output=```{}```", got) + PredicateFails(got: String, err_str: Option) { + description("User-supplied predicate failed") + display("Error string: {:?}\noutput=```{}```", err_str, got) } } }