diff --git a/src/core.rs b/src/core.rs index c7533b9..f53b236 100644 --- a/src/core.rs +++ b/src/core.rs @@ -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 { @@ -88,11 +92,14 @@ pub struct ExpectationChain<'a, T> { in_negated_mode: bool, - negations: Vec, - soft_mode: bool, - expectations: Vec + 'a>>, + checks: Vec>, +} + +struct Check<'a, T> { + expectation: Box + 'a>, + is_negated: bool, } impl<'a, T> ExpectationChain<'a, T> { @@ -100,9 +107,8 @@ impl<'a, T> ExpectationChain<'a, T> { Self { expression, in_negated_mode: false, - negations: vec![], soft_mode: false, - expectations: vec![], + checks: vec![], } } @@ -118,9 +124,10 @@ impl<'a, T> ExpectationChain<'a, T> { } pub fn expecting(mut self, expectation: impl Expectation + '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; @@ -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", @@ -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) @@ -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 { diff --git a/src/core/multiple_expectations.rs b/src/core/multiple_expectations.rs new file mode 100644 index 0000000..542898a --- /dev/null +++ b/src/core/multiple_expectations.rs @@ -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(mut self, chain: ExpectationChain) -> 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(&mut self, mut chain: ExpectationChain) -> &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) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 57732dd..8afe9e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::*; }