Skip to content
Draft
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
50 changes: 29 additions & 21 deletions src/core.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::fmt::Debug;

mod multiple_expectations;

pub use multiple_expectations::{start_expectations, MultipleExpectations};

/// Starts a new expectation chain for the supplied expression.
#[macro_export]
macro_rules! expect {
Expand Down Expand Up @@ -88,21 +92,23 @@ pub struct ExpectationChain<'a, T> {

in_negated_mode: bool,

negations: Vec<bool>,

soft_mode: bool,

expectations: Vec<Box<dyn Expectation<T> + 'a>>,
checks: Vec<Check<'a, T>>,
}

struct Check<'a, T> {
expectation: Box<dyn Expectation<T> + 'a>,
is_negated: bool,
}

impl<'a, T> ExpectationChain<'a, T> {
pub fn from_expression(expression: ExpressionUnderTest<'a, T>) -> Self {
Self {
expression,
in_negated_mode: false,
negations: vec![],
soft_mode: false,
expectations: vec![],
checks: vec![],
}
}

Expand All @@ -118,9 +124,10 @@ impl<'a, T> ExpectationChain<'a, T> {
}

pub fn expecting(mut self, expectation: impl Expectation<T> + 'a) -> Self {
self.expectations.push(Box::new(expectation));

self.negations.push(self.in_negated_mode);
self.checks.push(Check {
expectation: Box::new(expectation),
is_negated: self.in_negated_mode,
});

self.in_negated_mode = false;

Expand All @@ -139,7 +146,7 @@ impl<'a, T> ExpectationChain<'a, T> {
self
}

pub fn conclude_result(self) -> Result<(), String> {
fn conclude(&mut self) -> Result<(), String> {
let location = self.expression.location;
let mut message = format!(
"{}:{}:{}\nwhen testing expression\n\n",
Expand All @@ -148,16 +155,14 @@ impl<'a, T> ExpectationChain<'a, T> {
message.push_str(&format!(" {}\n\n", self.expression.tested_expression));

let mut had_failure = false;
for i in 0..self.expectations.len() {
let expectation = self.expectations.get(i).unwrap();
let is_negated = self.negations.get(i).unwrap();

if !(is_negated ^ expectation.test(self.expression.actual)) {
for check in self.checks.drain(..) {
if !(check.is_negated ^ check.expectation.test(self.expression.actual)) {
had_failure = true;

let failure_message =
expectation.message(self.expression.tested_expression, self.expression.actual);
let failure_message = if *is_negated {
let failure_message = check
.expectation
.message(self.expression.tested_expression, self.expression.actual);
let failure_message = if check.is_negated {
indented(" ", &format!("NOT {}", failure_message))
} else {
indented(" ", &failure_message)
Expand All @@ -178,12 +183,15 @@ impl<'a, T> ExpectationChain<'a, T> {
}
}

pub fn conclude_panic(self) {
if let Err(message) = self.conclude_result() {
eprintln!("{}", message);
panic!()
pub fn conclude_panic(mut self) {
if let Err(message) = self.conclude() {
panic!("{}", message);
}
}

pub fn conclude_result(mut self) -> Result<(), String> {
self.conclude()
}
}

fn indented(indentation: &str, s: &str) -> String {
Expand Down
88 changes: 88 additions & 0 deletions src/core/multiple_expectations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use super::ExpectationChain;

/// Checks multiple expectations
#[must_use = "This doesn't do anything without calling a `conclude_*()` method"]
pub struct MultipleExpectations {
pub(crate) errors: String,
pub(crate) panic_on_drop: bool,
}

/// If your test needs to check more than one expectation.
///
/// Fluently build up expectations with [`.and()`][MultipleExpectations::and] or
/// stepwise expectations with [`.now()`][MultipleExpectations::now].
pub fn start_expectations() -> MultipleExpectations {
MultipleExpectations {
errors: String::new(),
panic_on_drop: true,
}
}

impl MultipleExpectations {
/// Fluently adds an expectation.
///
/// ```rust
/// # use rassert::prelude::*;
/// # fn main() {
/// let numbers = vec![1, 2, 3];
/// start_expectations()
/// .and(expect!(&numbers).to_have_length(3))
/// .and(expect!(&numbers[0]).to_be(&1))
/// .and(expect!(&numbers[1]).to("be even", |it| it % 2 == 0))
/// .and(expect!(&numbers[2]).to_be(&3))
/// .conclude_panic();
/// # }
/// ```
pub fn and<T>(mut self, chain: ExpectationChain<T>) -> Self {
self.here(chain);
self
}

/// Mutably adds an expectation.
///
/// Use this style if you want to check intermediate results, but keep the
/// “important” check at the end, so that it is nice and isolated.
///
/// ``` rust
/// # use rassert::prelude::*;
/// # fn main() {
/// let mut checks = start_expectations();
///
/// let is_even = |n: &u8| (*n) % 2 == 0;
///
/// let a = [1, 2].iter().sum();
/// checks.here(expect!(&a).not().to("be even", is_even));
///
/// let b = [3, 6].iter().sum();
/// checks.here(expect!(&b).not().to("be even", is_even));
///
/// // This this the important one!
/// let sum_of_two_odd_numbers = a + b;
/// checks.here(expect!(&sum_of_two_odd_numbers)
/// .to("be even", is_even));
///
/// checks.conclude_panic();
/// # }
/// ```
pub fn here<T>(&mut self, mut chain: ExpectationChain<T>) -> &mut Self {
if let Err(err) = chain.conclude() {
self.errors.push_str(&err);
}
self
}

pub fn conclude_panic(self) {
if let Err(err) = self.conclude_result() {
panic!("{}", err);
}
}

pub fn conclude_result(mut self) -> Result<(), String> {
self.panic_on_drop = false;
if self.errors.trim().is_empty() {
Ok(())
} else {
Err(self.errors)
}
}
}
6 changes: 5 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
mod core;
mod expectations;

pub use crate::core::{Expectation, ExpectationChain, ExpressionUnderTest, SourceLocation};
pub use crate::core::{
start_expectations, Expectation, ExpectationChain, ExpressionUnderTest, MultipleExpectations,
SourceLocation,
};

pub mod prelude {
pub use super::expect;
pub use super::expect_matches;
pub use super::start_expectations;

pub use super::expectations::*;
}