Skip to content

Commit

Permalink
Introduce a macro any! as a complement to the macro all!.
Browse files Browse the repository at this point in the history
This macro creates a matcher which matches the value if at least one of its constituent matchers does. Having such a matcher is expected given the existence of `all!`.
  • Loading branch information
hovinen committed Jul 24, 2023
1 parent e45d943 commit d106e73
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 2 deletions.
1 change: 1 addition & 0 deletions googletest/crate_docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ The following matchers are provided in GoogleTest Rust:
| Matcher | What it matches |
|----------------------|--------------------------------------------------------------------------|
| [`all!`] | Anything matched by all given matchers. |
| [`any!`] | Anything matched by at least one of the given matchers. |
| [`anything`] | Any input. |
| [`approx_eq`] | A floating point number within a standard tolerance of the argument. |
| [`char_count`] | A string with a Unicode scalar count matching the argument. |
Expand Down
4 changes: 2 additions & 2 deletions googletest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ pub mod prelude {
pub use super::{assert_that, expect_pred, expect_that, fail, verify_pred, verify_that};
// Matcher macros
pub use super::{
all, contains_each, elements_are, field, is_contained_in, matches_pattern, pat, pointwise,
property, unordered_elements_are,
all, any, contains_each, elements_are, field, is_contained_in, matches_pattern, pat,
pointwise, property, unordered_elements_are,
};
}

Expand Down
207 changes: 207 additions & 0 deletions googletest/src/matchers/any_matcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// There are no visible documentation elements in this module; the declarative
// macro is documented at the top level.
#![doc(hidden)]

/// Matches a value which at least one of the given matchers match.
///
/// Each argument is a [`Matcher`][crate::matcher::Matcher] which matches
/// against the actual value.
///
/// For example:
///
/// ```
/// # use googletest::prelude::*;
/// # fn should_pass() -> Result<()> {
/// verify_that!("A string", any!(starts_with("A"), ends_with("string")))?; // Passes
/// verify_that!("A string", any!(starts_with("A"), starts_with("string")))?; // Passes
/// verify_that!("A string", any!(ends_with("A"), ends_with("string")))?; // Passes
/// # Ok(())
/// # }
/// # fn should_fail() -> Result<()> {
/// verify_that!("A string", any!(starts_with("An"), ends_with("not a string")))?; // Fails
/// # Ok(())
/// # }
/// # should_pass().unwrap();
/// # should_fail().unwrap_err();
/// ```
///
/// Using this macro is equivalent to using the
/// [`or`][crate::matcher::Matcher::or] method:
///
/// ```
/// # use googletest::prelude::*;
/// # fn should_pass() -> Result<()> {
/// verify_that!(10, gt(9).or(lt(8)))?; // Also passes
/// # Ok(())
/// # }
/// # should_pass().unwrap();
/// ```
///
/// Assertion failure messages are not guaranteed to be identical, however.
#[macro_export]
macro_rules! any {
($($matcher:expr),* $(,)?) => {{
use $crate::matchers::any_matcher::internal::AnyMatcher;
AnyMatcher::new([$(Box::new($matcher)),*])
}}
}

/// Functionality needed by the [`any`] macro.
///
/// For internal use only. API stablility is not guaranteed!
#[doc(hidden)]
pub mod internal {
use crate::matcher::{Matcher, MatcherResult};
use crate::matcher_support::description::Description;
use crate::matchers::anything;
use std::fmt::Debug;

/// A matcher which matches an input value matched by all matchers in the
/// array `components`.
///
/// For internal use only. API stablility is not guaranteed!
#[doc(hidden)]
pub struct AnyMatcher<'a, T: Debug + ?Sized, const N: usize> {
components: [Box<dyn Matcher<ActualT = T> + 'a>; N],
}

impl<'a, T: Debug + ?Sized, const N: usize> AnyMatcher<'a, T, N> {
/// Constructs an [`AnyMatcher`] with the given component matchers.
///
/// Intended for use only by the [`all`] macro.
pub fn new(components: [Box<dyn Matcher<ActualT = T> + 'a>; N]) -> Self {
Self { components }
}
}

impl<'a, T: Debug + ?Sized, const N: usize> Matcher for AnyMatcher<'a, T, N> {
type ActualT = T;

fn matches(&self, actual: &Self::ActualT) -> MatcherResult {
for component in &self.components {
match component.matches(actual) {
MatcherResult::NoMatch => {}
MatcherResult::Match => {
return MatcherResult::Match;
}
}
}
MatcherResult::NoMatch
}

fn explain_match(&self, actual: &Self::ActualT) -> String {
match N {
0 => format!("which {}", anything::<T>().describe(MatcherResult::NoMatch)),
1 => self.components[0].explain_match(actual),
_ => {
let failures = self
.components
.iter()
.filter(|component| component.matches(actual).is_no_match())
.map(|component| component.explain_match(actual))
.collect::<Description>();
if failures.len() == 1 {
format!("{}", failures)
} else {
format!("{}", failures.bullet_list().indent_except_first_line())
}
}
}
}

fn describe(&self, matcher_result: MatcherResult) -> String {
match N {
0 => anything::<T>().describe(matcher_result),
1 => self.components[0].describe(matcher_result),
_ => {
let properties = self
.components
.iter()
.map(|m| m.describe(matcher_result))
.collect::<Description>()
.bullet_list()
.indent();
format!(
"{}:\n{properties}",
if matcher_result.into() {
"has at least one of the following properties"
} else {
"has none of the following properties"
}
)
}
}
}
}
}

#[cfg(test)]
mod tests {
use super::internal;
use crate::matcher::{Matcher, MatcherResult};
use crate::prelude::*;
use indoc::indoc;

#[test]
fn description_shows_more_than_one_matcher() -> Result<()> {
let first_matcher = starts_with("A");
let second_matcher = ends_with("string");
let matcher: internal::AnyMatcher<String, 2> = any!(first_matcher, second_matcher);

verify_that!(
matcher.describe(MatcherResult::Match),
eq(indoc!(
"
has at least one of the following properties:
* starts with prefix \"A\"
* ends with suffix \"string\""
))
)
}

#[test]
fn description_shows_one_matcher_directly() -> Result<()> {
let first_matcher = starts_with("A");
let matcher: internal::AnyMatcher<String, 1> = any!(first_matcher);

verify_that!(matcher.describe(MatcherResult::Match), eq("starts with prefix \"A\""))
}

#[test]
fn mismatch_description_shows_which_matcher_failed_if_more_than_one_constituent() -> Result<()>
{
let first_matcher = starts_with("Another");
let second_matcher = ends_with("string");
let matcher: internal::AnyMatcher<str, 2> = any!(first_matcher, second_matcher);

verify_that!(
matcher.explain_match("A string"),
displays_as(eq("which does not start with \"Another\""))
)
}

#[test]
fn mismatch_description_is_simple_when_only_one_consistuent() -> Result<()> {
let first_matcher = starts_with("Another");
let matcher: internal::AnyMatcher<str, 1> = any!(first_matcher);

verify_that!(
matcher.explain_match("A string"),
displays_as(eq("which does not start with \"Another\""))
)
}
}
1 change: 1 addition & 0 deletions googletest/src/matchers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//! All built-in matchers of this crate are in submodules of this module.
pub mod all_matcher;
pub mod any_matcher;
pub mod anything_matcher;
pub mod char_count_matcher;
pub mod conjunction_matcher;
Expand Down
93 changes: 93 additions & 0 deletions googletest/tests/any_matcher_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use googletest::matcher::Matcher;
use googletest::prelude::*;
use indoc::indoc;

#[test]
fn does_not_match_value_when_list_is_empty() -> Result<()> {
verify_that!((), not(any!()))
}

#[test]
fn matches_value_with_single_matching_component() -> Result<()> {
verify_that!(123, any!(eq(123)))
}

#[test]
fn does_not_match_value_with_single_non_matching_component() -> Result<()> {
verify_that!(123, not(any!(eq(456))))
}

#[test]
fn matches_value_with_first_of_two_matching_components() -> Result<()> {
verify_that!("A string", any!(starts_with("A"), starts_with("string")))
}

#[test]
fn matches_value_with_second_of_two_matching_components() -> Result<()> {
verify_that!("A string", any!(starts_with("string"), starts_with("A")))
}

#[test]
fn supports_trailing_comma() -> Result<()> {
verify_that!(
"An important string",
any!(starts_with("An"), contains_substring("important"), ends_with("string"),)
)
}

#[test]
fn admits_matchers_without_static_lifetime() -> Result<()> {
#[derive(Debug, PartialEq)]
struct AStruct(i32);
let expected_value = AStruct(123);
verify_that!(AStruct(123), any![eq_deref_of(&expected_value)])
}

#[test]
fn mismatch_description_two_failed_matchers() -> Result<()> {
verify_that!(
any!(starts_with("One"), starts_with("Two")).explain_match("Three"),
displays_as(eq(
"* which does not start with \"One\"\n * which does not start with \"Two\""
))
)
}

#[test]
fn mismatch_description_empty_matcher() -> Result<()> {
verify_that!(any!().explain_match("Three"), displays_as(eq("which never matches")))
}

#[test]
fn all_multiple_failed_assertions() -> Result<()> {
let result = verify_that!(4, any![eq(1), eq(2), eq(3)]);
verify_that!(
result,
err(displays_as(contains_substring(indoc!(
"
Value of: 4
Expected: has at least one of the following properties:
* is equal to 1
* is equal to 2
* is equal to 3
Actual: 4,
* which isn't equal to 1
* which isn't equal to 2
* which isn't equal to 3"
))))
)
}
1 change: 1 addition & 0 deletions googletest/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

mod all_matcher_test;
mod any_matcher_test;
mod composition_test;
mod elements_are_matcher_test;
mod field_matcher_test;
Expand Down

0 comments on commit d106e73

Please sign in to comment.