From 6c5df0cb8d491caed528bc4e72623a7edd100e01 Mon Sep 17 00:00:00 2001 From: wldmr Date: Sun, 15 Mar 2026 18:24:36 +0100 Subject: [PATCH 1/3] Support checking multiple expectation chains --- src/core.rs | 17 +++-- src/core/multiple_expectations.rs | 105 ++++++++++++++++++++++++++++++ src/lib.rs | 6 +- 3 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 src/core/multiple_expectations.rs diff --git a/src/core.rs b/src/core.rs index c7533b9..de87880 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 { @@ -139,7 +143,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", @@ -178,12 +182,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..04b00b0 --- /dev/null +++ b/src/core/multiple_expectations.rs @@ -0,0 +1,105 @@ +use super::ExpectationChain; + +/// Abstracts over different [`ExpectationChain`]s. +pub(crate) trait Conclude { + fn conclude(&mut self) -> Result<(), String>; +} + +impl<'a, T> Conclude for ExpectationChain<'a, T> { + fn conclude(&mut self) -> Result<(), String> { + ExpectationChain::conclude(self) + } +} + +/// Checks multiple expectations +#[must_use = "This doesn't do anything without calling a `conclude_*()` method"] +pub struct MultipleExpectations<'a> { + pub(crate) many: Vec>, + 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<'a>() -> MultipleExpectations<'a> { + MultipleExpectations { + many: Vec::new(), + panic_on_drop: true, + } +} + +impl<'a> MultipleExpectations<'a> { + /// 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<'a, 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(&mut self, chain: ExpectationChain<'a, T>) -> &mut Self { + self.many.push(Box::new(chain)); + 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; + let msg = self + .many + .iter_mut() + .filter_map(|it| it.conclude().err()) + .fold(String::new(), |mut acc, err| { + acc.push_str(&err); + acc + }); + if msg.trim().is_empty() { + Ok(()) + } else { + Err(msg) + } + } +} 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::*; } From df5ad28cb903d79cab3347b16489c70a4d044813 Mon Sep 17 00:00:00 2001 From: wldmr Date: Sun, 15 Mar 2026 18:44:19 +0100 Subject: [PATCH 2/3] Refactor: Iterate over one vec of checks vs two Saves us the parallel indexing and `.unwrap()`. --- src/core.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/core.rs b/src/core.rs index de87880..f53b236 100644 --- a/src/core.rs +++ b/src/core.rs @@ -92,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> { @@ -104,9 +107,8 @@ impl<'a, T> ExpectationChain<'a, T> { Self { expression, in_negated_mode: false, - negations: vec![], soft_mode: false, - expectations: vec![], + checks: vec![], } } @@ -122,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; @@ -152,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) From 7f0b8460fcfb678b380b0666ebb3982241e72c15 Mon Sep 17 00:00:00 2001 From: wldmr Date: Mon, 16 Mar 2026 18:16:06 +0100 Subject: [PATCH 3/3] MultipleExpectations: Evaluate errors eagerly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … to get around ownership issues. Turns out that by lugging around the lifetimes of the constituent `ExpectationChain`s we failed to compile when using the `.here()` method on non-`Copy` types, because those references don’t outlive the `.here()` call. (Who could have guessed? …) The solution: We conclude the expectation chain there and then, and we only keep the error messages around. Makes the code much simpler, too. --- src/core/multiple_expectations.rs | 41 +++++++++---------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/core/multiple_expectations.rs b/src/core/multiple_expectations.rs index 04b00b0..542898a 100644 --- a/src/core/multiple_expectations.rs +++ b/src/core/multiple_expectations.rs @@ -1,20 +1,9 @@ use super::ExpectationChain; -/// Abstracts over different [`ExpectationChain`]s. -pub(crate) trait Conclude { - fn conclude(&mut self) -> Result<(), String>; -} - -impl<'a, T> Conclude for ExpectationChain<'a, T> { - fn conclude(&mut self) -> Result<(), String> { - ExpectationChain::conclude(self) - } -} - /// Checks multiple expectations #[must_use = "This doesn't do anything without calling a `conclude_*()` method"] -pub struct MultipleExpectations<'a> { - pub(crate) many: Vec>, +pub struct MultipleExpectations { + pub(crate) errors: String, pub(crate) panic_on_drop: bool, } @@ -22,14 +11,14 @@ pub struct MultipleExpectations<'a> { /// /// Fluently build up expectations with [`.and()`][MultipleExpectations::and] or /// stepwise expectations with [`.now()`][MultipleExpectations::now]. -pub fn start_expectations<'a>() -> MultipleExpectations<'a> { +pub fn start_expectations() -> MultipleExpectations { MultipleExpectations { - many: Vec::new(), + errors: String::new(), panic_on_drop: true, } } -impl<'a> MultipleExpectations<'a> { +impl MultipleExpectations { /// Fluently adds an expectation. /// /// ```rust @@ -44,7 +33,7 @@ impl<'a> MultipleExpectations<'a> { /// .conclude_panic(); /// # } /// ``` - pub fn and(mut self, chain: ExpectationChain<'a, T>) -> Self { + pub fn and(mut self, chain: ExpectationChain) -> Self { self.here(chain); self } @@ -75,8 +64,10 @@ impl<'a> MultipleExpectations<'a> { /// checks.conclude_panic(); /// # } /// ``` - pub fn here(&mut self, chain: ExpectationChain<'a, T>) -> &mut Self { - self.many.push(Box::new(chain)); + pub fn here(&mut self, mut chain: ExpectationChain) -> &mut Self { + if let Err(err) = chain.conclude() { + self.errors.push_str(&err); + } self } @@ -88,18 +79,10 @@ impl<'a> MultipleExpectations<'a> { pub fn conclude_result(mut self) -> Result<(), String> { self.panic_on_drop = false; - let msg = self - .many - .iter_mut() - .filter_map(|it| it.conclude().err()) - .fold(String::new(), |mut acc, err| { - acc.push_str(&err); - acc - }); - if msg.trim().is_empty() { + if self.errors.trim().is_empty() { Ok(()) } else { - Err(msg) + Err(self.errors) } } }