Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
95 changes: 91 additions & 4 deletions src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1055,8 +1055,9 @@ impl<S> 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
Expand Down Expand Up @@ -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<T, A, B>(mut self, assert: A) -> Spec<'a, (), R>
where
I: IntoIterator<Item = T>,
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);
Expand Down Expand Up @@ -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<T, A, B>(mut self, assert: A) -> Spec<'a, (), R>
where
I: IntoIterator<Item = T>,
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>()
{
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.
Expand Down
139 changes: 136 additions & 3 deletions src/spec/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::spec::{AssertFailure, Expression, OwnedLocation};
use crate::std::{
format,
string::{String, ToString},
vec,
};

#[test]
Expand Down Expand Up @@ -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)
Expand All @@ -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() {
Expand Down Expand Up @@ -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::*;
Expand Down
Loading