Skip to content
This repository was archived by the owner on Dec 29, 2021. It is now read-only.

[WIP] Add support for arbitrary predicates #57

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 87 additions & 13 deletions src/assert.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -409,12 +411,15 @@ impl OutputAssertionBuilder {
/// .unwrap();
/// ```
pub fn contains<O: Into<String>>(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
}

Expand All @@ -430,12 +435,15 @@ impl OutputAssertionBuilder {
/// .unwrap();
/// ```
pub fn is<O: Into<String>>(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
}

Expand Down Expand Up @@ -468,6 +476,72 @@ impl OutputAssertionBuilder {
pub fn isnt<O: Into<String>>(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<F>(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<F>(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)]
Expand Down
98 changes: 55 additions & 43 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Fn(&str) -> 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()))),
}
}

Expand Down Expand Up @@ -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<String>) {
description("User-supplied predicate failed")
display("Error string: {:?}\noutput=```{}```", err_str, got)
}
}
}
}