diff --git a/README.md b/README.md index 213bdb2..e22a4c4 100644 --- a/README.md +++ b/README.md @@ -419,6 +419,9 @@ 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 | +| 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 | for iterators that yield items in a well-defined order. diff --git a/src/assertions.rs b/src/assertions.rs index 25bf397..333d5b6 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -3661,6 +3661,8 @@ pub trait AssertMapContainsValue { /// /// # Examples /// +/// Filtering elements: +/// /// ``` /// use asserting::prelude::*; /// @@ -3670,6 +3672,21 @@ pub trait AssertMapContainsValue { /// let subject = [1, 2, 3, 4, 5]; /// assert_that!(subject).filtered_on(|e| e & 1 == 0).contains_exactly_in_any_order([2, 4]); /// ``` +/// +/// Some elements satisfy a predicate: +/// +/// ``` +/// use asserting::prelude::*; +/// +/// let subject = [1, 41, 43, 42, 5]; +/// assert_that!(subject).any_satisfies(|e| *e == 42); +/// +/// let subject = [43, 44, 45, 46, 47]; +/// assert_that!(subject).all_satisfy(|e| *e > 42); +/// +/// let subject = [42, 43, 44, 45, 46]; +/// assert_that!(subject).none_satisfies(|e| *e < 42); +/// ``` pub trait AssertElements<'a, T, R> { /// Verify that the iterator contains exactly one element and return a /// [`Spec`] for that single element. @@ -3706,7 +3723,57 @@ pub trait AssertElements<'a, T, R> { /// .is_equal_to("three"); /// ``` #[track_caller] - fn filtered_on(self, condition: impl Fn(&T) -> bool) -> Spec<'a, Vec, R>; + fn filtered_on(self, condition: C) -> Spec<'a, Vec, R> + where + C: FnMut(&T) -> bool; + + /// Verify that any element of a collection or an iterator satisfies a given + /// predicate. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = [1, 41, 43, 42, 5]; + /// assert_that!(subject).any_satisfies(|e| *e == 42); + /// ``` + #[track_caller] + fn any_satisfies

(self, predicate: P) -> Spec<'a, Vec, R> + where + P: FnMut(&T) -> bool; + + /// Verify that all elements of a collection or an iterator satisfy a given + /// predicate. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = [43, 44, 45, 46, 47]; + /// assert_that!(subject).all_satisfy(|e| *e > 42); + /// ``` + #[track_caller] + fn all_satisfy

(self, predicate: P) -> Spec<'a, Vec, R> + where + P: FnMut(&T) -> bool; + + /// Verify that none of the elements of a collection or an iterator + /// satisfies a given predicate. + /// + /// # Examples + /// + /// ``` + /// use asserting::prelude::*; + /// + /// let subject = [42, 43, 44, 45, 46]; + /// assert_that!(subject).none_satisfies(|e| *e < 42); + /// ``` + #[track_caller] + fn none_satisfies

(self, predicate: P) -> Spec<'a, Vec, R> + where + P: FnMut(&T) -> bool; } /// Filter assertions for elements of a collection or an iterator that yields diff --git a/src/expectations.rs b/src/expectations.rs index b067f75..ccc4c6d 100644 --- a/src/expectations.rs +++ b/src/expectations.rs @@ -1197,6 +1197,41 @@ pub struct HasAtLeastNumberOfElements { pub expected_number_of_elements: usize, } +pub fn any_satisfies

(predicate: P) -> AnySatisfies

{ + AnySatisfies { predicate } +} + +#[must_use] +pub struct AnySatisfies

{ + pub predicate: P, +} + +pub fn all_satisfy

(predicate: P) -> AllSatisfy

{ + AllSatisfy { + predicate, + failing: HashSet::new(), + } +} + +#[must_use] +pub struct AllSatisfy

{ + pub predicate: P, + pub failing: HashSet, +} + +pub fn none_satisfies

(predicate: P) -> NoneSatisfies

{ + NoneSatisfies { + predicate, + failing: HashSet::new(), + } +} + +#[must_use] +pub struct NoneSatisfies

{ + pub predicate: P, + pub failing: HashSet, +} + /// Creates a [`MapContainsKey`] expectation. pub fn map_contains_key(expected_key: E) -> MapContainsKey { MapContainsKey { expected_key } diff --git a/src/iterator/mod.rs b/src/iterator/mod.rs index fece921..90e5284 100644 --- a/src/iterator/mod.rs +++ b/src/iterator/mod.rs @@ -9,14 +9,15 @@ use crate::colored::{ mark_selected_items_in_collection, mark_unexpected, mark_unexpected_string, }; use crate::expectations::{ - has_at_least_number_of_elements, has_single_element, iterator_contains, - iterator_contains_all_in_order, iterator_contains_all_of, iterator_contains_any_of, - iterator_contains_exactly, iterator_contains_exactly_in_any_order, iterator_contains_only, - iterator_contains_only_once, iterator_contains_sequence, iterator_ends_with, - iterator_starts_with, not, HasAtLeastNumberOfElements, HasSingleElement, IteratorContains, - IteratorContainsAllInOrder, IteratorContainsAllOf, IteratorContainsAnyOf, - IteratorContainsExactly, IteratorContainsExactlyInAnyOrder, IteratorContainsOnly, - IteratorContainsOnlyOnce, IteratorContainsSequence, IteratorEndsWith, IteratorStartsWith, + all_satisfy, any_satisfies, has_at_least_number_of_elements, has_single_element, + iterator_contains, iterator_contains_all_in_order, iterator_contains_all_of, + iterator_contains_any_of, iterator_contains_exactly, iterator_contains_exactly_in_any_order, + iterator_contains_only, iterator_contains_only_once, iterator_contains_sequence, + iterator_ends_with, iterator_starts_with, none_satisfies, not, AllSatisfy, AnySatisfies, + HasAtLeastNumberOfElements, HasSingleElement, IteratorContains, IteratorContainsAllInOrder, + IteratorContainsAllOf, IteratorContainsAnyOf, IteratorContainsExactly, + IteratorContainsExactlyInAnyOrder, IteratorContainsOnly, IteratorContainsOnlyOnce, + IteratorContainsSequence, IteratorEndsWith, IteratorStartsWith, NoneSatisfies, }; use crate::properties::DefinedOrderProperty; use crate::spec::{ @@ -775,6 +776,57 @@ where } } +impl<'a, S, T, R> AssertElements<'a, T, R> for Spec<'a, S, R> +where + S: IntoIterator, + T: Debug, + R: FailingStrategy, +{ + fn single_element(self) -> Spec<'a, T, R> { + let spec = self.mapping(Vec::from_iter).expecting(has_single_element()); + if spec.has_failures() { + PanicOnFail.do_fail_with(&spec.failures()); + unreachable!("Assertion failed and should have panicked! Please report a bug.") + } + spec.extracting(|mut collection| { + collection.pop().unwrap_or_else(|| { + unreachable!("Assertion failed and should have panicked! Please report a bug.") + }) + }) + } + + fn filtered_on(self, condition: C) -> Spec<'a, Vec, R> + where + C: FnMut(&T) -> bool, + { + self.mapping(|subject| subject.into_iter().filter(condition).collect()) + } + + fn any_satisfies

(self, predicate: P) -> Spec<'a, Vec, R> + where + P: FnMut(&T) -> bool, + { + self.mapping(Vec::from_iter) + .expecting(any_satisfies(predicate)) + } + + fn all_satisfy

(self, predicate: P) -> Spec<'a, Vec, R> + where + P: FnMut(&T) -> bool, + { + self.mapping(Vec::from_iter) + .expecting(all_satisfy(predicate)) + } + + fn none_satisfies

(self, predicate: P) -> Spec<'a, Vec, R> + where + P: FnMut(&T) -> bool, + { + self.mapping(Vec::from_iter) + .expecting(none_satisfies(predicate)) + } +} + impl Expectation> for HasSingleElement where T: Debug, @@ -804,36 +856,74 @@ where } } -impl<'a, S, T, R> AssertElements<'a, T, R> for Spec<'a, S, R> +impl Expectation> for AnySatisfies

where - S: IntoIterator, T: Debug, - R: FailingStrategy, + P: FnMut(&T) -> bool, { - fn single_element(self) -> Spec<'a, T, R> { - let spec = self.mapping(Vec::from_iter).expecting(has_single_element()); - if spec.has_failures() { - PanicOnFail.do_fail_with(&spec.failures()); - unreachable!("Assertion failed and should have panicked! Please report a bug.") + fn test(&mut self, subject: &Vec) -> bool { + subject.iter().any(|e| (self.predicate)(e)) + } + + fn message( + &self, + expression: &Expression<'_>, + actual: &Vec, + _inverted: bool, + _format: &DiffFormat, + ) -> String { + format!( + r"expected any element of {expression} to satisfy the predicate, but none did + actual: {actual:?}" + ) + } +} + +impl Expectation> for AllSatisfy

+where + T: Debug, + P: FnMut(&T) -> bool, +{ + fn test(&mut self, subject: &Vec) -> bool { + for (i, e) in subject.iter().enumerate() { + if !(self.predicate)(e) { + self.failing.insert(i); + } } - spec.extracting(|mut collection| { - collection.pop().unwrap_or_else(|| { - unreachable!("Assertion failed and should have panicked! Please report a bug.") - }) - }) + self.failing.is_empty() } - fn filtered_on(self, condition: impl Fn(&T) -> bool) -> Spec<'a, Vec, R> { - self.mapping(|subject| subject.into_iter().filter(condition).collect()) + fn message( + &self, + expression: &Expression<'_>, + actual: &Vec, + _inverted: bool, + format: &DiffFormat, + ) -> String { + let number_of_failing = self.failing.len(); + let failing = collect_selected_values(&self.failing, actual); + let marked_actual = + mark_selected_items_in_collection(actual, &self.failing, format, mark_unexpected); + format!( + r"expected all elements of {expression} to satisfy the predicate, but {number_of_failing} did not + actual: {marked_actual} + failing: {failing:?}" + ) } } -impl Expectation> for HasAtLeastNumberOfElements +impl Expectation> for NoneSatisfies

where T: Debug, + P: FnMut(&T) -> bool, { fn test(&mut self, subject: &Vec) -> bool { - subject.len() >= self.expected_number_of_elements + for (i, e) in subject.iter().enumerate() { + if (self.predicate)(e) { + self.failing.insert(i); + } + } + self.failing.is_empty() } fn message( @@ -843,23 +933,14 @@ where _inverted: bool, format: &DiffFormat, ) -> String { - let actual_length = actual.len(); - let actual_elements = match actual_length { - 0 => mark_unexpected_string("no elements", format), - 1 => mark_unexpected_string("one element", format), - _ => mark_unexpected_string(&format!("{actual_length} elements"), format), - }; - let expected_elements = match self.expected_number_of_elements { - 0 => mark_missing_string("no elements", format), - 1 => mark_missing_string("at least one element", format), - _ => mark_missing_string( - &format!("at least {} elements", self.expected_number_of_elements), - format, - ), - }; + let number_of_failing = self.failing.len(); + let failing = collect_selected_values(&self.failing, actual); + let marked_actual = + mark_selected_items_in_collection(actual, &self.failing, format, mark_unexpected); format!( - r"expected {expression} to have {expected_elements}, but has {actual_elements} - actual: {actual:?}" + r"expected none of the elements of {expression} to satisfy the predicate, but {number_of_failing} did + actual: {marked_actual} + failing: {failing:?}" ) } } @@ -921,6 +1002,42 @@ where } } +impl Expectation> for HasAtLeastNumberOfElements +where + T: Debug, +{ + fn test(&mut self, subject: &Vec) -> bool { + subject.len() >= self.expected_number_of_elements + } + + fn message( + &self, + expression: &Expression<'_>, + actual: &Vec, + _inverted: bool, + format: &DiffFormat, + ) -> String { + let actual_length = actual.len(); + let actual_elements = match actual_length { + 0 => mark_unexpected_string("no elements", format), + 1 => mark_unexpected_string("one element", format), + _ => mark_unexpected_string(&format!("{actual_length} elements"), format), + }; + let expected_elements = match self.expected_number_of_elements { + 0 => mark_missing_string("no elements", format), + 1 => mark_missing_string("at least one element", format), + _ => mark_missing_string( + &format!("at least {} elements", self.expected_number_of_elements), + format, + ), + }; + format!( + r"expected {expression} to have {expected_elements}, but has {actual_elements} + actual: {actual:?}" + ) + } +} + pub fn collect_selected_values<'a, T>(indices: &HashSet, collection: &'a [T]) -> Vec<&'a T> { collection .iter() diff --git a/src/iterator/tests.rs b/src/iterator/tests.rs index c0d9794..8256dc1 100644 --- a/src/iterator/tests.rs +++ b/src/iterator/tests.rs @@ -458,4 +458,145 @@ mod element_filters { .elements_at([0, 2, 4]) .contains_exactly(["one", "three", "five"]); } + + #[test] + fn any_satisfies_on_elements_of_iterator_value_is_equal_to_42() { + let subject = CustomCollection { + inner: vec![1, 41, 43, 42, 5], + }; + + assert_that(subject).any_satisfies(|e| *e == 42); + } + + #[test] + fn verify_any_satisfies_on_elements_of_iterator_value_is_equal_to_42_fails() { + let subject = CustomCollection { + inner: vec![1, 2, 43, 41, 5], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .any_satisfies(|e| *e == 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected any element of my_numbers to satisfy the predicate, but none did + actual: [1, 2, 43, 41, 5] +" + ] + ); + } + + #[test] + fn all_satisfy_on_elements_of_iterator_value_is_greater_than_42() { + let subject = CustomCollection { + inner: vec![47, 46, 45, 44, 43], + }; + + assert_that(subject).all_satisfy(|e| *e > 42); + } + + #[test] + fn verify_all_satisfy_on_elements_of_iterator_value_is_greater_than_42_fails() { + let subject = CustomCollection { + inner: vec![43, 44, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .all_satisfy(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected all elements of my_numbers to satisfy the predicate, but 1 did not + actual: [43, 44, 45, 42, 47] + failing: [42] +" + ] + ); + } + + #[test] + fn none_satisfies_on_elements_of_iterator_value_is_greater_than_42() { + let subject = CustomCollection { + inner: vec![42, 41, 40, 39, 38], + }; + + assert_that(subject).none_satisfies(|e| *e > 42); + } + + #[test] + fn verify_none_satisfies_on_elements_of_iterator_value_is_greater_than_42_fails() { + let subject = CustomCollection { + inner: vec![41, 43, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .none_satisfies(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + r"expected none of the elements of my_numbers to satisfy the predicate, but 3 did + actual: [41, 43, 45, 42, 47] + failing: [43, 45, 47] +" + ] + ); + } + + #[cfg(feature = "colored")] + mod colored { + use super::*; + + #[test] + fn highlight_all_satisfy_on_elements_of_iterator() { + let subject = CustomCollection { + inner: vec![43, 44, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .with_diff_format(DIFF_FORMAT_RED_YELLOW) + .all_satisfy(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + "expected all elements of my_numbers to satisfy the predicate, but 1 did not\n \ + actual: [43, 44, 45, \u{1b}[31m42\u{1b}[0m, 47]\n \ + failing: [42]\n" + ] + ); + } + + #[test] + fn highlight_none_satisfies_on_elements_of_iterator() { + let subject = CustomCollection { + inner: vec![41, 43, 45, 42, 47], + }; + + let failures = verify_that(subject) + .named("my_numbers") + .with_diff_format(DIFF_FORMAT_RED_YELLOW) + .none_satisfies(|e| *e > 42) + .display_failures(); + + assert_eq!( + failures, + &[ + "expected none of the elements of my_numbers to satisfy the predicate, but 3 did\n \ + actual: [41, \u{1b}[31m43\u{1b}[0m, \u{1b}[31m45\u{1b}[0m, 42, \u{1b}[31m47\u{1b}[0m]\n \ + failing: [43, 45, 47]\n" + ] + ); + } + } } diff --git a/src/lib.rs b/src/lib.rs index e79c1a7..f8e49f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,7 +125,9 @@ //! .contains_only([1, 3, 5, 7, 9, 11, 13, 17, 19, 23, 29, 31, 37, 43]); //! ``` //! -//! ## Asserting each item of a collection or an iterator +//! ## Asserting some elements of a collection or an iterator +//! +//! Asserting some elements of a collection or an iterator: //! //! ``` //! use asserting::prelude::*; @@ -138,6 +140,21 @@ //! ); //! ``` //! +//! Assert some elements of a collection or an iterator to satisfy a predicate: +//! +//! ``` +//! use asserting::prelude::*; +//! +//! let subject = [1, 41, 43, 42, 5]; +//! assert_that!(subject).any_satisfies(|e| *e == 42); +//! +//! let subject = [43, 44, 45, 46, 47]; +//! assert_that!(subject).all_satisfy(|e| *e > 42); +//! +//! let subject = [42, 43, 44, 45, 46]; +//! assert_that!(subject).none_satisfies(|e| *e < 42); +//! ``` +//! //! For more details see [`Spec::each_item()`]. //! //! ## Asserting specific elements of a collection or an iterator