From 6406d8054ec9f3586c8d400a3c1ac4ba03a56f88 Mon Sep 17 00:00:00 2001 From: Miguel Mendes <1120588@isep.ipp.pt> Date: Thu, 25 Jan 2018 00:11:57 +0000 Subject: [PATCH 1/4] Implemented regex matching. Gonna test this before refactoring. --- Cargo.toml | 1 + src/assert.rs | 36 ++++++++++++++++++---- src/output.rs | 83 +++++++++++++++++++++++++++++++++++--------------- tests/cargo.rs | 13 ++++++++ 4 files changed, 103 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d1198dd..05f6f60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ difference = "1.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 8d0ba0e..40116bc 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -1,7 +1,9 @@ +extern crate regex; + use environment::Environment; use error_chain::ChainedError; use errors::*; -use output::{OutputAssertion, OutputKind}; +use output::{OutputAssertion, OutputKind, ExpectType}; use std::default; use std::ffi::{OsStr, OsString}; use std::io::Write; @@ -344,9 +346,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 @@ -449,7 +451,7 @@ impl OutputAssertionBuilder { /// ``` pub fn contains>(mut self, output: O) -> Assert { self.assertion.expect_output.push(OutputAssertion { - expect: output.into(), + expect: ExpectType::STRING(output.into()), fuzzy: true, expected_result: self.expected_result, kind: self.kind, @@ -470,7 +472,29 @@ impl OutputAssertionBuilder { /// ``` pub fn is>(mut self, output: O) -> Assert { self.assertion.expect_output.push(OutputAssertion { - expect: output.into(), + expect: ExpectType::STRING(output.into()), + fuzzy: false, + expected_result: self.expected_result, + kind: self.kind, + }); + self.assertion + } + + /// Expect the command to output **exactly** this `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// extern crate regex; + /// let re = regex::Regex::new("[0-9]{2}").unwrap(); + /// assert_cli::Assert::command(&["echo", "42"]) + /// .stdout().matches(re) + /// .unwrap(); + /// ``` + pub fn matches(mut self, output: regex::Regex) -> Assert { + self.assertion.expect_output.push(OutputAssertion { + expect: ExpectType::REGEX(output), fuzzy: false, expected_result: self.expected_result, kind: self.kind, diff --git a/src/output.rs b/src/output.rs index 207cec6..a573279 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; @@ -5,9 +7,16 @@ use difference::Changeset; use std::ffi::OsString; use std::process::Output; +#[derive(Debug, Clone)] +pub enum ExpectType { + STRING(String), + REGEX(regex::Regex), + //perdicate? +} + #[derive(Debug, Clone)] pub struct OutputAssertion { - pub expect: String, + pub expect: ExpectType, pub fuzzy: bool, pub expected_result: bool, pub kind: OutputKind, @@ -15,38 +24,60 @@ pub struct OutputAssertion { 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())); + match self.expect { + ExpectType::STRING(ref self_str) => { + let result = got.contains(self_str); + if result != self.expected_result { + if self.expected_result { + bail!(ErrorKind::OutputDoesntContain(self_str.clone(), got.into())); + } else { + bail!(ErrorKind::OutputContains(self_str.clone(), got.into())); + } + } + } + // This brach here makes no sense unless we support matching multiple times + ExpectType::REGEX(ref self_regex) => { + let result = self_regex.is_match(got); + if result != self.expected_result { + bail!(ErrorKind::OutputDoesntMatchRegex( + String::from(self_regex.as_str()), + 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; + match self.expect { + ExpectType::STRING(ref self_str) => { + let differences = Changeset::new(self_str.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())); + if result != self.expected_result { + if self.expected_result { + let nice_diff = diff::render(&differences)?; + bail!(ErrorKind::OutputDoesntMatch( + self_str.clone(), + got.to_owned(), + nice_diff, + )); + } else { + bail!(ErrorKind::OutputMatches(got.to_owned())); + } + } + } + ExpectType::REGEX(ref self_regex) => { + let result = self_regex.is_match(got); + if result != self.expected_result { + bail!(ErrorKind::OutputDoesntMatchRegex( + String::from(self_regex.as_str()), + got.into(), + )); + } } } - Ok(()) } @@ -103,6 +134,10 @@ mod errors { description("Output was not as expected") display("expected to not match\noutput=```{}```", got) } + OutputDoesntMatchRegex(regex: String, got: String) { + description("Regex did not match") + display("expected {} to match\noutput=```{}```", regex, got) + } } } } diff --git a/tests/cargo.rs b/tests/cargo.rs index f476ee1..0da9d57 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,15 @@ 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(); +} From e70bdac42875dc2da05c1551fb5d7a8ae73d3fbd Mon Sep 17 00:00:00 2001 From: Miguel Mendes <1120588@isep.ipp.pt> Date: Thu, 25 Jan 2018 20:25:04 +0000 Subject: [PATCH 2/4] fuzzy and nonfuzzy regex matches. --- src/assert.rs | 26 ++++++++++++++++++++++++-- src/output.rs | 19 ++++++++++++------- tests/cargo.rs | 12 ++++++++++++ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/assert.rs b/src/assert.rs index 40116bc..06f3b06 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -480,7 +480,7 @@ impl OutputAssertionBuilder { self.assertion } - /// Expect the command to output **exactly** this `output`. + /// Expect the command to match **however many times** this `output`. /// /// # Examples /// @@ -494,7 +494,29 @@ impl OutputAssertionBuilder { /// ``` pub fn matches(mut self, output: regex::Regex) -> Assert { self.assertion.expect_output.push(OutputAssertion { - expect: ExpectType::REGEX(output), + expect: ExpectType::REGEX(output, 0), + fuzzy: true, + expected_result: self.expected_result, + kind: self.kind, + }); + self.assertion + } + + /// Expect the command to match `nmatches` times this `output`. + /// + /// # Examples + /// + /// ```rust + /// extern crate assert_cli; + /// extern crate regex; + /// let re = regex::Regex::new("[0-9]{1}").unwrap(); + /// assert_cli::Assert::command(&["echo", "42"]) + /// .stdout().matches_ntimes(re, 2) + /// .unwrap(); + /// ``` + pub fn matches_ntimes(mut self, output: regex::Regex, nmatches: u32) -> Assert { + self.assertion.expect_output.push(OutputAssertion { + expect: ExpectType::REGEX(output, nmatches), fuzzy: false, expected_result: self.expected_result, kind: self.kind, diff --git a/src/output.rs b/src/output.rs index a573279..4233a62 100644 --- a/src/output.rs +++ b/src/output.rs @@ -10,7 +10,7 @@ use std::process::Output; #[derive(Debug, Clone)] pub enum ExpectType { STRING(String), - REGEX(regex::Regex), + REGEX(regex::Regex, u32), //perdicate? } @@ -35,13 +35,14 @@ impl OutputAssertion { } } } - // This brach here makes no sense unless we support matching multiple times - ExpectType::REGEX(ref self_regex) => { - let result = self_regex.is_match(got); - if result != self.expected_result { - bail!(ErrorKind::OutputDoesntMatchRegex( + ExpectType::REGEX(ref self_regex, nmatches) => { + let regex_matches = self_regex.captures_iter(got).count(); + if regex_matches != (nmatches as usize) { + bail!(ErrorKind::OutputDoesntMatchRegexExactTimes( String::from(self_regex.as_str()), got.into(), + nmatches, + regex_matches, )); } } @@ -68,7 +69,7 @@ impl OutputAssertion { } } } - ExpectType::REGEX(ref self_regex) => { + ExpectType::REGEX(ref self_regex, _) => { let result = self_regex.is_match(got); if result != self.expected_result { bail!(ErrorKind::OutputDoesntMatchRegex( @@ -138,6 +139,10 @@ mod errors { description("Regex did not match") display("expected {} to match\noutput=```{}```", regex, got) } + OutputDoesntMatchRegexExactTimes(regex: String, got: String, expected_times: u32, got_times: usize) { + description("Regex did not match exact number of times") + display("expected {} to match {} {} times\noutput=```{}```", regex, got, expected_times, got_times) + } } } } diff --git a/tests/cargo.rs b/tests/cargo.rs index 0da9d57..16bef6f 100644 --- a/tests/cargo.rs +++ b/tests/cargo.rs @@ -34,3 +34,15 @@ fn matches_with_regex() { .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(); +} From e69509508fb02ecbc1db19ae2ffbe0c0e327acc6 Mon Sep 17 00:00:00 2001 From: Miguel Mendes <1120588@isep.ipp.pt> Date: Thu, 25 Jan 2018 20:34:35 +0000 Subject: [PATCH 3/4] fixed fuzzy matching. --- src/output.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/output.rs b/src/output.rs index 4233a62..af0a591 100644 --- a/src/output.rs +++ b/src/output.rs @@ -35,14 +35,12 @@ impl OutputAssertion { } } } - ExpectType::REGEX(ref self_regex, nmatches) => { - let regex_matches = self_regex.captures_iter(got).count(); - if regex_matches != (nmatches as usize) { - bail!(ErrorKind::OutputDoesntMatchRegexExactTimes( + ExpectType::REGEX(ref self_regex, _) => { + let result = self_regex.is_match(got); + if result != self.expected_result { + bail!(ErrorKind::OutputDoesntMatchRegex( String::from(self_regex.as_str()), got.into(), - nmatches, - regex_matches, )); } } @@ -69,12 +67,14 @@ impl OutputAssertion { } } } - ExpectType::REGEX(ref self_regex, _) => { - let result = self_regex.is_match(got); - if result != self.expected_result { - bail!(ErrorKind::OutputDoesntMatchRegex( + ExpectType::REGEX(ref self_regex, nmatches) => { + let regex_matches = self_regex.captures_iter(got).count(); + if regex_matches != (nmatches as usize) { + bail!(ErrorKind::OutputDoesntMatchRegexExactTimes( String::from(self_regex.as_str()), got.into(), + nmatches, + regex_matches, )); } } From 0d589c206f82e5cc74db61ec13f51003f5ad9e1d Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 5 Feb 2018 19:36:31 +0000 Subject: [PATCH 4/4] Implemented Regex parsing, 1st pass. --- src/assert.rs | 45 +++++++++++++++++++++++--- src/output.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/assert.rs b/src/assert.rs index da40884..6e10add 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -362,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()), + )); } } @@ -372,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::>>()?; @@ -447,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 ef9328a..70ac75d 100644 --- a/src/output.rs +++ b/src/output.rs @@ -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(