diff --git a/Cargo.toml b/Cargo.toml index 7e9c121..894fcfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ difference = "2.0" error-chain = "0.11" serde_json = "1.0" environment = "0.1" +regex = "0.2.5" [build-dependencies] skeptic = "0.13" diff --git a/src/assert.rs b/src/assert.rs index bccc54e..6e10add 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -1,3 +1,5 @@ +extern crate regex; + use environment::Environment; use error_chain::ChainedError; use errors::*; @@ -342,9 +344,9 @@ impl Assert { None => command, }; - let mut spawned = command - .spawn() - .chain_err(|| ErrorKind::SpawnFailed(self.cmd.clone()))?; + let mut spawned = command.spawn().chain_err( + || ErrorKind::SpawnFailed(self.cmd.clone()), + )?; if let Some(ref contents) = self.stdin_contents { spawned @@ -360,7 +362,9 @@ impl Assert { let out = String::from_utf8_lossy(&output.stdout).to_string(); let err = String::from_utf8_lossy(&output.stderr).to_string(); let err: Error = ErrorKind::StatusMismatch(expect_success, out, err).into(); - bail!(err.chain_err(|| ErrorKind::AssertionFailed(self.cmd.clone()))); + bail!(err.chain_err( + || ErrorKind::AssertionFailed(self.cmd.clone()), + )); } } @@ -370,14 +374,17 @@ impl Assert { let err: Error = ErrorKind::ExitCodeMismatch(self.expect_exit_code, output.status.code(), out, err) .into(); - bail!(err.chain_err(|| ErrorKind::AssertionFailed(self.cmd.clone()))); + bail!(err.chain_err( + || ErrorKind::AssertionFailed(self.cmd.clone()), + )); } self.expect_output .iter() .map(|a| { - a.verify(&output) - .chain_err(|| ErrorKind::AssertionFailed(self.cmd.clone())) + a.verify(&output).chain_err(|| { + ErrorKind::AssertionFailed(self.cmd.clone()) + }) }) .collect::>>()?; @@ -445,6 +452,38 @@ impl OutputAssertionBuilder { self.assertion } + /// Expect the command to match **however many times** this `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// assert_cli::Assert::command(&["echo", "42"]) + /// .stdout().matches("[0-9]{2}") + /// .unwrap(); + /// ``` + pub fn matches(mut self, output: String) -> Assert { + let pred = OutputPredicate::new(self.kind, Output::matches(output)); + self.assertion.expect_output.push(pred); + self.assertion + } + + /// Expect the command to match `nmatches` times this `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// assert_cli::Assert::command(&["echo", "42"]) + /// .stdout().matches_ntimes("[0-9]{1}", 2) + /// .unwrap(); + /// ``` + pub fn matches_ntimes(mut self, output: String, nmatches: u32) -> Assert { + let pred = OutputPredicate::new(self.kind, Output::matches_ntimes(output, nmatches)); + self.assertion.expect_output.push(pred); + self.assertion + } + /// Expect the command's output to not **contain** `output`. /// /// # Examples diff --git a/src/output.rs b/src/output.rs index 3395f94..70ac75d 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,3 +1,5 @@ +extern crate regex; + use self::errors::*; pub use self::errors::{Error, ErrorKind}; use diff; @@ -63,7 +65,6 @@ impl IsPredicate { bail!(ErrorKind::BytesMatches(got.to_owned())); } } - Ok(()) } @@ -83,7 +84,6 @@ impl IsPredicate { bail!(ErrorKind::StrMatches(got.to_owned())); } } - Ok(()) } } @@ -174,11 +174,47 @@ impl fmt::Debug for FnPredicate { } } +#[derive(Debug, Clone)] +struct RegexPredicate { + regex: regex::Regex, + times: u32, +} + +impl RegexPredicate { + fn verify(&self, got: &[u8]) -> Result<()> { + let conversion = String::from_utf8_lossy(got); + let got = conversion.as_ref(); + if self.times == 0 { + let result = self.regex.is_match(got); + if !result { + bail!(ErrorKind::OutputDoesntMatchRegexExactTimes( + String::from(self.regex.as_str()), + got.into(), + 1, + 1 + )); + } + } else { + let regex_matches = self.regex.captures_iter(got).count(); + if regex_matches != (self.times as usize) { + bail!(ErrorKind::OutputDoesntMatchRegexExactTimes( + String::from(self.regex.as_str()), + got.into(), + self.times, + regex_matches, + )); + } + } + Ok(()) + } +} + #[derive(Debug, Clone)] enum ContentPredicate { Is(IsPredicate), Contains(ContainsPredicate), Fn(FnPredicate), + Regex(RegexPredicate), } impl ContentPredicate { @@ -187,6 +223,7 @@ impl ContentPredicate { ContentPredicate::Is(ref pred) => pred.verify(got), ContentPredicate::Contains(ref pred) => pred.verify(got), ContentPredicate::Fn(ref pred) => pred.verify(got), + ContentPredicate::Regex(ref pred) => pred.verify(got), } } } @@ -238,6 +275,46 @@ impl Output { Self::new(ContentPredicate::Is(pred)) } + /// Expect the command to **match** this `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo"]) + /// .with_args(&["42"]) + /// .stdout().matches("[0-9]{2}") + /// .unwrap(); + /// ``` + pub fn matches(output: String) -> Self { + let pred = RegexPredicate { + regex: regex::Regex::new(&output).unwrap(), + times: 0, + }; + Self::new(ContentPredicate::Regex(pred)) + } + + /// Expect the command to **match** this `output` exacly `nmatches` times. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command(&["echo"]) + /// .with_args(&["42"]) + /// .stdout().matches_ntimes("[0-9]{1}", 2) + /// .unwrap(); + /// ``` + pub fn matches_ntimes(output: String, nmatches: u32) -> Self { + let pred = RegexPredicate { + regex: regex::Regex::new(&output).unwrap(), + times: nmatches, + }; + Self::new(ContentPredicate::Regex(pred)) + } + /// Expect the command's output to not **contain** `output`. /// /// # Examples @@ -386,6 +463,16 @@ mod errors { description("Output predicate failed") display("{}\noutput=```{}```", msg, got) } +/* Adding a single error more makes this break, using the bottom one temporarily + OutputDoesntMatchRegex(regex: String, got: String) { + description("Expected to regex to match") + display("expected {}\n to match output=```{}```", regex, got) + } +*/ + OutputDoesntMatchRegexExactTimes(regex: String, got: String, expected_times: u32, got_times: usize) { + description("Expected to regex to match exact number of times") + display("expected {}\n to match output=```{}``` {} times instead of {} times", regex, got, expected_times, got_times) + } OutputMismatch(kind: super::OutputKind) { description("Output was not as expected") display( diff --git a/tests/cargo.rs b/tests/cargo.rs index f476ee1..16bef6f 100644 --- a/tests/cargo.rs +++ b/tests/cargo.rs @@ -1,4 +1,5 @@ extern crate assert_cli; +extern crate regex; #[test] fn main_binary() { @@ -21,3 +22,27 @@ fn cargo_binary() { .is("") .unwrap(); } + +#[test] +fn matches_with_regex() { + let re = regex::Regex::new("[0-9]{2}").unwrap(); + assert_cli::Assert::main_binary() + .with_env(assert_cli::Environment::inherit().insert("stdout", "42")) + .stdout() + .matches(re) + .stderr() + .is("") + .unwrap(); +} + +#[test] +fn matches_with_regex_ntimes() { + let re = regex::Regex::new("[0-9]{1}").unwrap(); + assert_cli::Assert::main_binary() + .with_env(assert_cli::Environment::inherit().insert("stdout", "42")) + .stdout() + .matches_ntimes(re, 2) + .stderr() + .is("") + .unwrap(); +}