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
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