use crate::custom::BehaveMatch;
fn build_list_description(prefix: &str, matchers: &[Box<dyn BehaveMatch<impl Sized>>]) -> String {
let mut desc = format!("{prefix}:");
for m in matchers {
let sub = m.description();
desc.push_str("\n - ");
desc.push_str(&indent_subsequent_lines(sub, " "));
}
desc
}
fn indent_subsequent_lines(text: &str, indent: &str) -> String {
let mut lines = text.lines();
let Some(first) = lines.next() else {
return String::new();
};
let mut result = first.to_string();
for line in lines {
result.push('\n');
result.push_str(indent);
result.push_str(line);
}
result
}
#[non_exhaustive]
pub struct AllOf<T> {
matchers: Vec<Box<dyn BehaveMatch<T>>>,
description: String,
}
impl<T> core::fmt::Debug for AllOf<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("AllOf")
.field("description", &self.description)
.field("matcher_count", &self.matchers.len())
.finish()
}
}
impl<T> BehaveMatch<T> for AllOf<T> {
fn matches(&self, actual: &T) -> bool {
self.matchers.iter().all(|m| m.matches(actual))
}
fn description(&self) -> &str {
&self.description
}
}
pub fn all_of<T>(matchers: Vec<Box<dyn BehaveMatch<T>>>) -> AllOf<T> {
let description = build_list_description("to match all of", &matchers);
AllOf {
matchers,
description,
}
}
#[non_exhaustive]
pub struct AnyOf<T> {
matchers: Vec<Box<dyn BehaveMatch<T>>>,
description: String,
}
impl<T> core::fmt::Debug for AnyOf<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("AnyOf")
.field("description", &self.description)
.field("matcher_count", &self.matchers.len())
.finish()
}
}
impl<T> BehaveMatch<T> for AnyOf<T> {
fn matches(&self, actual: &T) -> bool {
self.matchers.iter().any(|m| m.matches(actual))
}
fn description(&self) -> &str {
&self.description
}
}
pub fn any_of<T>(matchers: Vec<Box<dyn BehaveMatch<T>>>) -> AnyOf<T> {
let description = build_list_description("to match any of", &matchers);
AnyOf {
matchers,
description,
}
}
#[non_exhaustive]
pub struct NotMatching<T> {
inner: Box<dyn BehaveMatch<T>>,
description: String,
}
impl<T> core::fmt::Debug for NotMatching<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("NotMatching")
.field("description", &self.description)
.finish_non_exhaustive()
}
}
impl<T> BehaveMatch<T> for NotMatching<T> {
fn matches(&self, actual: &T) -> bool {
!self.inner.matches(actual)
}
fn description(&self) -> &str {
&self.description
}
}
pub fn not_matching<T>(matcher: Box<dyn BehaveMatch<T>>) -> NotMatching<T> {
let description = format!("not {}", matcher.description());
NotMatching {
inner: matcher,
description,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Expectation;
struct IsPositive;
#[allow(clippy::unnecessary_literal_bound)]
impl BehaveMatch<i32> for IsPositive {
fn matches(&self, actual: &i32) -> bool {
*actual > 0
}
fn description(&self) -> &str {
"to be positive"
}
}
struct IsEven;
#[allow(clippy::unnecessary_literal_bound)]
impl BehaveMatch<i32> for IsEven {
fn matches(&self, actual: &i32) -> bool {
actual % 2 == 0
}
fn description(&self) -> &str {
"to be even"
}
}
struct IsZero;
#[allow(clippy::unnecessary_literal_bound)]
impl BehaveMatch<i32> for IsZero {
fn matches(&self, actual: &i32) -> bool {
*actual == 0
}
fn description(&self) -> &str {
"to be zero"
}
}
#[test]
fn all_of_all_pass() {
let m = all_of(vec![
Box::new(IsPositive) as Box<dyn BehaveMatch<i32>>,
Box::new(IsEven),
]);
assert!(Expectation::new(4, "4").to_match(m).is_ok());
}
#[test]
fn all_of_one_fails() {
let m = all_of(vec![
Box::new(IsPositive) as Box<dyn BehaveMatch<i32>>,
Box::new(IsEven),
]);
assert!(Expectation::new(3, "3").to_match(m).is_err());
}
#[test]
fn all_of_empty_passes() {
let m: AllOf<i32> = all_of(vec![]);
assert!(Expectation::new(99, "99").to_match(m).is_ok());
}
#[test]
fn all_of_description_format() {
let m = all_of(vec![
Box::new(IsPositive) as Box<dyn BehaveMatch<i32>>,
Box::new(IsEven),
]);
let desc = m.description();
assert!(desc.contains("to match all of:"));
assert!(desc.contains("- to be positive"));
assert!(desc.contains("- to be even"));
}
#[test]
fn any_of_one_passes() {
let m = any_of(vec![
Box::new(IsZero) as Box<dyn BehaveMatch<i32>>,
Box::new(IsPositive),
]);
assert!(Expectation::new(5, "5").to_match(m).is_ok());
}
#[test]
fn any_of_none_pass() {
let m = any_of(vec![
Box::new(IsZero) as Box<dyn BehaveMatch<i32>>,
Box::new(IsEven),
]);
assert!(Expectation::new(3, "3").to_match(m).is_err());
}
#[test]
fn any_of_empty_fails() {
let m: AnyOf<i32> = any_of(vec![]);
assert!(Expectation::new(1, "1").to_match(m).is_err());
}
#[test]
fn any_of_description_format() {
let m = any_of(vec![
Box::new(IsZero) as Box<dyn BehaveMatch<i32>>,
Box::new(IsPositive),
]);
let desc = m.description();
assert!(desc.contains("to match any of:"));
assert!(desc.contains("- to be zero"));
assert!(desc.contains("- to be positive"));
}
#[test]
fn not_matching_inverts_pass() {
let m = not_matching(Box::new(IsEven));
assert!(Expectation::new(3, "3").to_match(m).is_ok());
}
#[test]
fn not_matching_inverts_fail() {
let m = not_matching(Box::new(IsEven));
assert!(Expectation::new(4, "4").to_match(m).is_err());
}
#[test]
fn not_matching_description() {
let m = not_matching(Box::new(IsEven));
assert_eq!(m.description(), "not to be even");
}
#[test]
fn nested_all_of_inside_any_of() {
let inner = all_of(vec![
Box::new(IsPositive) as Box<dyn BehaveMatch<i32>>,
Box::new(IsEven),
]);
let m = any_of(vec![
Box::new(IsZero) as Box<dyn BehaveMatch<i32>>,
Box::new(inner),
]);
assert!(Expectation::new(4, "4").to_match(m).is_ok());
}
#[test]
fn nested_description_indentation() {
let inner = all_of(vec![
Box::new(IsPositive) as Box<dyn BehaveMatch<i32>>,
Box::new(IsEven),
]);
let m = any_of(vec![
Box::new(IsZero) as Box<dyn BehaveMatch<i32>>,
Box::new(inner),
]);
let desc = m.description();
assert!(desc.contains("to match any of:"));
assert!(desc.contains("- to match all of:"));
}
#[test]
fn not_matching_inside_all_of_pass() {
let m = all_of(vec![
Box::new(IsPositive) as Box<dyn BehaveMatch<i32>>,
Box::new(not_matching(Box::new(IsEven))),
]);
assert!(Expectation::new(3, "3").to_match(m).is_ok());
}
#[test]
fn not_matching_inside_all_of_fail() {
let m = all_of(vec![
Box::new(IsPositive) as Box<dyn BehaveMatch<i32>>,
Box::new(not_matching(Box::new(IsEven))),
]);
assert!(Expectation::new(4, "4").to_match(m).is_err());
}
#[test]
fn all_of_negated_via_expectation() {
let m = all_of(vec![
Box::new(IsPositive) as Box<dyn BehaveMatch<i32>>,
Box::new(IsEven),
]);
assert!(Expectation::new(3, "3").negate().to_match(m).is_ok());
}
#[test]
fn boxed_matcher_delegates() {
let boxed: Box<dyn BehaveMatch<i32>> = Box::new(IsEven);
assert!(Expectation::new(4, "4").to_match(boxed).is_ok());
}
#[test]
fn debug_impls() {
let a = all_of::<i32>(vec![]);
let formatted = format!("{a:?}");
assert!(formatted.contains("AllOf"));
let b = any_of::<i32>(vec![]);
let formatted = format!("{b:?}");
assert!(formatted.contains("AnyOf"));
let c = not_matching(Box::new(IsEven));
let formatted = format!("{c:?}");
assert!(formatted.contains("NotMatching"));
}
}