diff --git a/README.md b/README.md index e22a4c4..f4be4d3 100644 --- a/README.md +++ b/README.md @@ -419,9 +419,11 @@ for all iterators. | contains_only_once | verify that an iterator/collection contains only the specified values in any order and each of them only once | | single_element | verify that an iterator/collection contains exaclty one element and return a `Spec` for that one element | | filtered_on | filter the elements of an iterator/collection on a condition and return a `Spec` that contains the filtered elements | -| any_satisfies | verify that any element of an iterator/collection satisfies a predicate | +| any_satisfies | verify that at least one element of an iterator/collection satisfies a predicate | | all_satisfy | verify that all elements of an iterator/collection satisfy a predicate | | none_satisfies | verify that none of the elements of an iterator/collection satisfies a predicate | +| each_element | verify that all elements of an iterator/collection satisfy the given assertions | +| any_element | verify that at least one element of an iterator/collection satisfies the given assertions | for iterators that yield items in a well-defined order. diff --git a/src/lib.rs b/src/lib.rs index 4bdbb57..3bbcf99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,6 +138,10 @@ //! e.is_greater_than(1) //! .is_at_most(10) //! ); +//! +//! assert_that!(numbers).any_element(|e| +//! e.is_equal_to(4) +//! ); //! ``` //! //! See [`Spec::each_element()`] for more details. diff --git a/src/spec/mod.rs b/src/spec/mod.rs index 2b40bb0..9cd55b8 100644 --- a/src/spec/mod.rs +++ b/src/spec/mod.rs @@ -1055,8 +1055,9 @@ impl Spec<'_, S, CollectFailures> { } impl<'a, I, R> Spec<'a, I, R> { - /// Iterates over the elements of a collection or iterator and executes the - /// given assertions for each of those elements. + /// Iterates over the elements of a collection or an iterator and executes + /// the given assertions for each of those elements. If all elements are + /// asserted successfully, the whole assertion succeeds. /// /// It iterates over all elements of the collection or iterator and collects /// the failure messages for those elements where the assertion fails. In @@ -1093,15 +1094,16 @@ impl<'a, I, R> Spec<'a, I, R> { /// but was: 8 /// expected: <= 7 /// - /// expected numbers [4] item to be at most 7 + /// expected numbers [4] to be at most 7 /// but was: 10 /// expected: <= 7 /// ``` #[allow(clippy::return_self_not_must_use)] + #[track_caller] pub fn each_element(mut self, assert: A) -> Spec<'a, (), R> where I: IntoIterator, - for<'c> A: Fn(Spec<'c, T, CollectFailures>) -> Spec<'c, B, CollectFailures>, + A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>, { let default_expression = &Expression::default(); let root_expression = self.expression.as_ref().unwrap_or(default_expression); @@ -1135,6 +1137,91 @@ impl<'a, I, R> Spec<'a, I, R> { failing_strategy: self.failing_strategy, } } + + /// Iterates over the elements of a collection or an iterator and executes + /// the given assertions for each of those elements. If the assertion of any + /// element is successful, the iteration stops and the whole assertion + /// succeeds. + /// + /// If the assertion fails for all elements, the failures of the assertion + /// for all elements are collected. + /// + /// The failure messages contain the position of the element within the + /// collection or iterator. The position is 0-based. So a failure message + /// for the first element contains `[0]`, the second `[1]`, and so on. + /// + /// # Example + /// + /// The following assertion: + /// + /// ```should_panic + /// use asserting::prelude::*; + /// + /// let digit_names = ["one", "two", "three"]; + /// + /// assert_that!(digit_names).any_element(|e| + /// e.contains('x') + /// ); + /// ``` + /// + /// will print: + /// + /// ```console + /// expected digit_names [0] to contain 'x' + /// but was: "one" + /// expected: 'x' + /// + /// expected digit_names [1] to contain 'x' + /// but was: "two" + /// expected: 'x' + /// + /// expected digit_names [2] to contain 'x' + /// but was: "three" + /// expected: 'x' + /// ``` + #[track_caller] + pub fn any_element(mut self, assert: A) -> Spec<'a, (), R> + where + I: IntoIterator, + A: Fn(Spec<'a, T, CollectFailures>) -> Spec<'a, B, CollectFailures>, + { + let default_expression = &Expression::default(); + let root_expression = self.expression.as_ref().unwrap_or(default_expression); + let mut any_success = false; + let mut position = -1; + for item in self.subject { + position += 1; + let element_spec = Spec { + subject: item, + expression: Some(format!("{root_expression} [{position}]").into()), + description: None, + location: self.location, + failures: vec![], + diff_format: self.diff_format.clone(), + failing_strategy: CollectFailures, + }; + let failures = assert(element_spec).failures; + if failures.is_empty() { + any_success = true; + break; + } + self.failures.extend(failures); + } + if !any_success + && any::type_name_of_val(&self.failing_strategy) == any::type_name::() + { + PanicOnFail.do_fail_with(&self.failures); + } + Spec { + subject: (), + expression: self.expression, + description: self.description, + location: self.location, + failures: self.failures, + diff_format: self.diff_format, + failing_strategy: self.failing_strategy, + } + } } /// An error describing a failed assertion. diff --git a/src/spec/tests.rs b/src/spec/tests.rs index e75a1e9..54ddc64 100644 --- a/src/spec/tests.rs +++ b/src/spec/tests.rs @@ -3,6 +3,7 @@ use crate::spec::{AssertFailure, Expression, OwnedLocation}; use crate::std::{ format, string::{String, ToString}, + vec, }; #[test] @@ -229,8 +230,14 @@ fn soft_assertions_panic_once_with_multiple_failure_messages() { .soft_panic(); } +#[derive(Debug)] +struct TestPerson { + name: String, + age: u8, +} + #[test] -fn assert_each_element_of_an_iterator() { +fn assert_each_element_of_an_iterator_of_integer() { let subject = [2, 4, 6, 8, 10]; assert_that(subject) @@ -239,14 +246,50 @@ fn assert_each_element_of_an_iterator() { } #[test] -fn assert_each_element_of_a_borrowed_iterator() { - let subject = [2, 4, 6, 8, 10]; +fn assert_each_element_of_an_iterator_of_person() { + let subject = vec![ + TestPerson { + name: "John".into(), + age: 42, + }, + TestPerson { + name: "Jane".into(), + age: 20, + }, + ]; + + assert_that(subject) + .is_not_empty() + .each_element(|person| person.extracting(|p| p.age).is_at_most(42)); +} + +#[test] +fn assert_each_element_of_a_borrowed_iterator_of_integer() { + let subject = vec![2, 4, 6, 8, 10]; assert_that(&subject) .is_not_empty() .each_element(|e| e.is_positive().is_at_most(&20)); } +#[test] +fn assert_each_element_of_a_borrowed_iterator_of_person() { + let subject = vec![ + TestPerson { + name: "John".into(), + age: 42, + }, + TestPerson { + name: "Jane".into(), + age: 20, + }, + ]; + + assert_that(&subject) + .is_not_empty() + .each_element(|person| person.extracting(|p| &p.name).starts_with('J')); +} + #[test] #[should_panic = "expected numbers [1] to be not equal to 4\n but was: 4\n expected: not 4\n"] fn assert_each_element_of_an_iterator_panics_if_one_assertion_fails() { @@ -286,6 +329,96 @@ fn verify_assert_each_element_of_an_iterator_fails() { ); } +#[test] +fn assert_any_element_of_an_iterator_of_str() { + let subject = ["one", "two", "three", "four", "five"]; + + assert_that(subject) + .is_not_empty() + .any_element(|e| e.contains("ee")); +} + +#[test] +fn assert_any_element_of_an_iterator_of_person() { + let subject = vec![ + TestPerson { + name: "John".into(), + age: 42, + }, + TestPerson { + name: "Jane".into(), + age: 20, + }, + ]; + + assert_that(subject) + .is_not_empty() + .any_element(|person| person.extracting(|p| p.age).is_at_most(20)); +} + +#[test] +fn assert_any_element_of_a_borrowed_iterator_of_str() { + let subject = vec!["one", "two", "three", "four", "five"]; + + assert_that(&subject) + .is_not_empty() + .any_element(|e| e.starts_with("fi")); +} + +#[test] +fn assert_any_element_of_a_borrowed_iterator_of_person() { + let subject = vec![ + TestPerson { + name: "John".into(), + age: 42, + }, + TestPerson { + name: "Jane".into(), + age: 20, + }, + ]; + + assert_that(&subject) + .is_not_empty() + .any_element(|person| person.extracting(|p| &p.name).ends_with('n')); +} + +#[test] +fn verify_assert_any_element_of_an_iterator_fails() { + let subject = ["one", "two", "three", "four", "five"]; + + let failures = verify_that(subject) + .named("words") + .any_element(|e| e.starts_with("fu")) + .display_failures(); + + assert_eq!( + failures, + &[ + r#"expected words [0] to start with "fu" + but was: "one" + expected: "fu" +"#, + r#"expected words [1] to start with "fu" + but was: "two" + expected: "fu" +"#, + r#"expected words [2] to start with "fu" + but was: "three" + expected: "fu" +"#, + r#"expected words [3] to start with "fu" + but was: "four" + expected: "fu" +"#, + r#"expected words [4] to start with "fu" + but was: "five" + expected: "fu" +"#, + ] + ); +} + #[cfg(feature = "colored")] mod colored { use crate::prelude::*;