diff --git a/src/lib.rs b/src/lib.rs index 80e28bc..4f01721 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,10 +108,7 @@ extern crate difference; #[macro_use] extern crate error_chain; extern crate rustc_serialize; -use std::process::{Command, Output}; -use std::fmt; - -use difference::Changeset; +use std::process::Command; mod errors; use errors::*; @@ -119,6 +116,12 @@ use errors::*; #[macro_use] mod macros; pub use macros::flatten_escaped_string; +mod output; +use output::{OutputAssertion, StdErr, StdOut}; + +mod parse_cmd; +use parse_cmd::ToCmd; + mod diff; /// Assertions for a specific command. @@ -127,38 +130,8 @@ pub struct Assert { cmd: Vec, expect_success: Option, expect_exit_code: Option, - expect_stdout: Option, - expect_stderr: Option, -} - -#[derive(Debug)] -struct OutputAssertion { - expect: String, - fuzzy: bool, -} - -#[derive(Debug, Copy, Clone)] -enum OutputType { - StdOut, - StdErr, -} - -impl OutputType { - fn select<'a>(&self, o: &'a Output) -> &'a [u8] { - match *self { - OutputType::StdOut => &o.stdout, - OutputType::StdErr => &o.stderr, - } - } -} - -impl fmt::Display for OutputType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - OutputType::StdOut => write!(f, "stdout"), - OutputType::StdErr => write!(f, "stderr"), - } - } + expect_stdout: Option>, + expect_stderr: Option>, } impl std::default::Default for Assert { @@ -208,9 +181,16 @@ impl Assert { /// assert_cli::Assert::command(&["echo", "1337"]) /// .unwrap(); /// ``` - pub fn command(cmd: &[&str]) -> Self { + /// + /// ```rust + /// extern crate assert_cli; + /// + /// assert_cli::Assert::command("echo 1337") + /// .unwrap(); + /// ``` + pub fn command<'a, T: ToCmd<'a> + ?Sized>(cmd: &'a T) -> Self { Assert { - cmd: cmd.into_iter().cloned().map(String::from).collect(), + cmd: cmd.to_cmd(), ..Self::default() } } @@ -318,6 +298,7 @@ impl Assert { self.expect_stdout = Some(OutputAssertion { expect: output.into(), fuzzy: true, + kind: StdOut, }); self } @@ -337,6 +318,7 @@ impl Assert { self.expect_stdout = Some(OutputAssertion { expect: output.into(), fuzzy: false, + kind: StdOut, }); self } @@ -358,6 +340,7 @@ impl Assert { self.expect_stderr = Some(OutputAssertion { expect: output.into(), fuzzy: true, + kind: StdErr, }); self } @@ -379,6 +362,7 @@ impl Assert { self.expect_stderr = Some(OutputAssertion { expect: output.into(), fuzzy: false, + kind: StdErr, }); self } @@ -421,52 +405,15 @@ impl Assert { )); } - self.assert_output(OutputType::StdOut, &output)?; - self.assert_output(OutputType::StdErr, &output)?; - - Ok(()) - } - - /// Perform the appropriate output assertion. - fn assert_output(&self, output_type: OutputType, output: &Output) -> Result<()> { - let observed = String::from_utf8_lossy(output_type.select(output)); - match *self.expect_output(output_type) { - Some(OutputAssertion { - expect: ref expected_output, - fuzzy: true, - }) if !observed.contains(expected_output) => { - bail!(ErrorKind::OutputMismatch( - output_type.to_string(), - self.cmd.clone(), - expected_output.clone(), - observed.into(), - )); - }, - Some(OutputAssertion { - expect: ref expected_output, - fuzzy: false, - }) => { - let differences = Changeset::new(expected_output.trim(), observed.trim(), "\n"); - if differences.distance > 0 { - let nice_diff = diff::render(&differences)?; - bail!(ErrorKind::ExactOutputMismatch( - output_type.to_string(), - self.cmd.clone(), - nice_diff - )); - } - }, - _ => {}, + if let Some(ouput_assertion) = self.expect_stdout { + ouput_assertion.execute(&output)?; } - Ok(()) - } - /// Return a reference to the appropriate output assertion. - fn expect_output(&self, output_type: OutputType) -> &Option { - match output_type { - OutputType::StdOut => &self.expect_stdout, - OutputType::StdErr => &self.expect_stderr, + if let Some(ouput_assertion) = self.expect_stderr { + ouput_assertion.execute(&output)?; } + + Ok(()) } /// Execute the command, check the assertions, and panic when they fail. diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..3c3267a --- /dev/null +++ b/src/output.rs @@ -0,0 +1,91 @@ +use std::fmt; +use std::process::Output; + +use difference::Changeset; + +use errors::*; +use diff; + +#[derive(Debug, Clone)] +pub struct OutputAssertion { + pub expect: String, + pub fuzzy: bool, + pub kind: T, +} + +impl OutputAssertion { + fn matches_fuzzy(&self, got: &str) -> Result<()> { + if !got.contains(&self.expect) { + bail!(ErrorKind::OutputMismatch( + self.kind.to_string(), + vec!["Foo".to_string()], + self.expect.clone(), + got.into(), + )); + } + + Ok(()) + } + + fn matches_exact(&self, got: &str) -> Result<()> { + let differences = Changeset::new(self.expect.trim(), got.trim(), "\n"); + + if differences.distance > 0 { + let nice_diff = diff::render(&differences)?; + bail!(ErrorKind::ExactOutputMismatch( + self.kind.to_string(), + vec!["Foo".to_string()], + nice_diff + )); + } + + Ok(()) + } + + pub fn execute(&self, output: &Output) -> Result<()> { + let observed = String::from_utf8_lossy(self.kind.select(output)); + + if self.fuzzy { + self.matches_fuzzy(&observed) + } else { + self.matches_exact(&observed) + } + } +} + + +pub trait OutputType: fmt::Display { + fn select<'a>(&self, o: &'a Output) -> &'a [u8]; +} + + +#[derive(Debug, Clone, Copy)] +pub struct StdOut; + +impl fmt::Display for StdOut { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "stdout") + } +} + +impl OutputType for StdOut { + fn select<'a>(&self, o: &'a Output) -> &'a [u8] { + &o.stdout + } +} + + +#[derive(Debug, Clone, Copy)] +pub struct StdErr; + +impl fmt::Display for StdErr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "stderr") + } +} + +impl OutputType for StdErr { + fn select<'a>(&self, o: &'a Output) -> &'a [u8] { + &o.stderr + } +} diff --git a/src/parse_cmd.rs b/src/parse_cmd.rs new file mode 100644 index 0000000..98e578f --- /dev/null +++ b/src/parse_cmd.rs @@ -0,0 +1,98 @@ +pub trait ToCmd<'a> { + fn to_cmd(&'a self) -> Vec; +} + +impl<'a> ToCmd<'a> for str { + fn to_cmd(&'a self) -> Vec { + let mut args = Vec::new(); + let mut current_arg = String::new(); + let mut in_quote = Vec::new(); + + for c in self.chars() { + if in_quote.is_empty() && c.is_whitespace() { + args.push(current_arg); + current_arg = String::new(); + continue; + } + + current_arg.push(c); + + if c == '"' || c == '\'' { + if in_quote.last() == Some(&c) { + in_quote.pop(); + } else { + in_quote.push(c); + } + } + } + + if !current_arg.is_empty() { + args.push(current_arg); + } + + args + } +} + +impl<'a, 'b, T> ToCmd<'a> for T where + &'a T: AsRef<[&'b str]>, + T: 'a, +{ + fn to_cmd(&'a self) -> Vec { + self.as_ref().into_iter().map(|x| x.to_string()).collect() + } +} + +#[cfg(test)] +mod test { + use super::ToCmd; + + #[test] + fn slices() { + assert_eq!( + ToCmd::to_cmd(&["echo", "42"]), + vec!["echo", "42"] + ); + } + + #[test] + fn simple() { + assert_eq!( + "echo 42".to_cmd(), + vec!["echo", "42"] + ); + assert_eq!( + r#"echo "42""#.to_cmd(), + vec!["echo", "\"42\""] + ); + assert_eq!( + r#"echo '42'"#.to_cmd(), + vec!["echo", "\'42\'"] + ); + assert_eq!( + r#"echo '42 is the answer'"#.to_cmd(), + vec!["echo", "\'42 is the answer\'"] + ); + } + + #[test] + fn real_world() { + assert_eq!( + r#"cargo run --bin whatever -- --input="Lorem ipsum" -f"#.to_cmd(), + vec!["cargo", "run", "--bin", "whatever", "--", "--input=\"Lorem ipsum\"", "-f"] + ); + } + + #[test] + fn nested_quotes() { + assert_eq!( + r#"echo "lorem ipsum 'dolor' sit amet""#.to_cmd(), + vec!["echo", "\"lorem ipsum 'dolor' sit amet\""] + ); + + assert_eq!( + r#"echo "lorem ipsum ('dolor "doloris" septetur') sit amet""#.to_cmd(), + vec!["echo", "\"lorem ipsum ('dolor \"doloris\" septetur') sit amet\""] + ); + } +}