Skip to content

Commit d106e73

Browse files
committed
Introduce a macro any! as a complement to the macro all!.
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!`.
1 parent e45d943 commit d106e73

File tree

6 files changed

+305
-2
lines changed

6 files changed

+305
-2
lines changed

googletest/crate_docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ The following matchers are provided in GoogleTest Rust:
7979
| Matcher | What it matches |
8080
|----------------------|--------------------------------------------------------------------------|
8181
| [`all!`] | Anything matched by all given matchers. |
82+
| [`any!`] | Anything matched by at least one of the given matchers. |
8283
| [`anything`] | Any input. |
8384
| [`approx_eq`] | A floating point number within a standard tolerance of the argument. |
8485
| [`char_count`] | A string with a Unicode scalar count matching the argument. |

googletest/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ pub mod prelude {
5151
pub use super::{assert_that, expect_pred, expect_that, fail, verify_pred, verify_that};
5252
// Matcher macros
5353
pub use super::{
54-
all, contains_each, elements_are, field, is_contained_in, matches_pattern, pat, pointwise,
55-
property, unordered_elements_are,
54+
all, any, contains_each, elements_are, field, is_contained_in, matches_pattern, pat,
55+
pointwise, property, unordered_elements_are,
5656
};
5757
}
5858

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// There are no visible documentation elements in this module; the declarative
16+
// macro is documented at the top level.
17+
#![doc(hidden)]
18+
19+
/// Matches a value which at least one of the given matchers match.
20+
///
21+
/// Each argument is a [`Matcher`][crate::matcher::Matcher] which matches
22+
/// against the actual value.
23+
///
24+
/// For example:
25+
///
26+
/// ```
27+
/// # use googletest::prelude::*;
28+
/// # fn should_pass() -> Result<()> {
29+
/// verify_that!("A string", any!(starts_with("A"), ends_with("string")))?; // Passes
30+
/// verify_that!("A string", any!(starts_with("A"), starts_with("string")))?; // Passes
31+
/// verify_that!("A string", any!(ends_with("A"), ends_with("string")))?; // Passes
32+
/// # Ok(())
33+
/// # }
34+
/// # fn should_fail() -> Result<()> {
35+
/// verify_that!("A string", any!(starts_with("An"), ends_with("not a string")))?; // Fails
36+
/// # Ok(())
37+
/// # }
38+
/// # should_pass().unwrap();
39+
/// # should_fail().unwrap_err();
40+
/// ```
41+
///
42+
/// Using this macro is equivalent to using the
43+
/// [`or`][crate::matcher::Matcher::or] method:
44+
///
45+
/// ```
46+
/// # use googletest::prelude::*;
47+
/// # fn should_pass() -> Result<()> {
48+
/// verify_that!(10, gt(9).or(lt(8)))?; // Also passes
49+
/// # Ok(())
50+
/// # }
51+
/// # should_pass().unwrap();
52+
/// ```
53+
///
54+
/// Assertion failure messages are not guaranteed to be identical, however.
55+
#[macro_export]
56+
macro_rules! any {
57+
($($matcher:expr),* $(,)?) => {{
58+
use $crate::matchers::any_matcher::internal::AnyMatcher;
59+
AnyMatcher::new([$(Box::new($matcher)),*])
60+
}}
61+
}
62+
63+
/// Functionality needed by the [`any`] macro.
64+
///
65+
/// For internal use only. API stablility is not guaranteed!
66+
#[doc(hidden)]
67+
pub mod internal {
68+
use crate::matcher::{Matcher, MatcherResult};
69+
use crate::matcher_support::description::Description;
70+
use crate::matchers::anything;
71+
use std::fmt::Debug;
72+
73+
/// A matcher which matches an input value matched by all matchers in the
74+
/// array `components`.
75+
///
76+
/// For internal use only. API stablility is not guaranteed!
77+
#[doc(hidden)]
78+
pub struct AnyMatcher<'a, T: Debug + ?Sized, const N: usize> {
79+
components: [Box<dyn Matcher<ActualT = T> + 'a>; N],
80+
}
81+
82+
impl<'a, T: Debug + ?Sized, const N: usize> AnyMatcher<'a, T, N> {
83+
/// Constructs an [`AnyMatcher`] with the given component matchers.
84+
///
85+
/// Intended for use only by the [`all`] macro.
86+
pub fn new(components: [Box<dyn Matcher<ActualT = T> + 'a>; N]) -> Self {
87+
Self { components }
88+
}
89+
}
90+
91+
impl<'a, T: Debug + ?Sized, const N: usize> Matcher for AnyMatcher<'a, T, N> {
92+
type ActualT = T;
93+
94+
fn matches(&self, actual: &Self::ActualT) -> MatcherResult {
95+
for component in &self.components {
96+
match component.matches(actual) {
97+
MatcherResult::NoMatch => {}
98+
MatcherResult::Match => {
99+
return MatcherResult::Match;
100+
}
101+
}
102+
}
103+
MatcherResult::NoMatch
104+
}
105+
106+
fn explain_match(&self, actual: &Self::ActualT) -> String {
107+
match N {
108+
0 => format!("which {}", anything::<T>().describe(MatcherResult::NoMatch)),
109+
1 => self.components[0].explain_match(actual),
110+
_ => {
111+
let failures = self
112+
.components
113+
.iter()
114+
.filter(|component| component.matches(actual).is_no_match())
115+
.map(|component| component.explain_match(actual))
116+
.collect::<Description>();
117+
if failures.len() == 1 {
118+
format!("{}", failures)
119+
} else {
120+
format!("{}", failures.bullet_list().indent_except_first_line())
121+
}
122+
}
123+
}
124+
}
125+
126+
fn describe(&self, matcher_result: MatcherResult) -> String {
127+
match N {
128+
0 => anything::<T>().describe(matcher_result),
129+
1 => self.components[0].describe(matcher_result),
130+
_ => {
131+
let properties = self
132+
.components
133+
.iter()
134+
.map(|m| m.describe(matcher_result))
135+
.collect::<Description>()
136+
.bullet_list()
137+
.indent();
138+
format!(
139+
"{}:\n{properties}",
140+
if matcher_result.into() {
141+
"has at least one of the following properties"
142+
} else {
143+
"has none of the following properties"
144+
}
145+
)
146+
}
147+
}
148+
}
149+
}
150+
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
use super::internal;
155+
use crate::matcher::{Matcher, MatcherResult};
156+
use crate::prelude::*;
157+
use indoc::indoc;
158+
159+
#[test]
160+
fn description_shows_more_than_one_matcher() -> Result<()> {
161+
let first_matcher = starts_with("A");
162+
let second_matcher = ends_with("string");
163+
let matcher: internal::AnyMatcher<String, 2> = any!(first_matcher, second_matcher);
164+
165+
verify_that!(
166+
matcher.describe(MatcherResult::Match),
167+
eq(indoc!(
168+
"
169+
has at least one of the following properties:
170+
* starts with prefix \"A\"
171+
* ends with suffix \"string\""
172+
))
173+
)
174+
}
175+
176+
#[test]
177+
fn description_shows_one_matcher_directly() -> Result<()> {
178+
let first_matcher = starts_with("A");
179+
let matcher: internal::AnyMatcher<String, 1> = any!(first_matcher);
180+
181+
verify_that!(matcher.describe(MatcherResult::Match), eq("starts with prefix \"A\""))
182+
}
183+
184+
#[test]
185+
fn mismatch_description_shows_which_matcher_failed_if_more_than_one_constituent() -> Result<()>
186+
{
187+
let first_matcher = starts_with("Another");
188+
let second_matcher = ends_with("string");
189+
let matcher: internal::AnyMatcher<str, 2> = any!(first_matcher, second_matcher);
190+
191+
verify_that!(
192+
matcher.explain_match("A string"),
193+
displays_as(eq("which does not start with \"Another\""))
194+
)
195+
}
196+
197+
#[test]
198+
fn mismatch_description_is_simple_when_only_one_consistuent() -> Result<()> {
199+
let first_matcher = starts_with("Another");
200+
let matcher: internal::AnyMatcher<str, 1> = any!(first_matcher);
201+
202+
verify_that!(
203+
matcher.explain_match("A string"),
204+
displays_as(eq("which does not start with \"Another\""))
205+
)
206+
}
207+
}

googletest/src/matchers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//! All built-in matchers of this crate are in submodules of this module.
1616
1717
pub mod all_matcher;
18+
pub mod any_matcher;
1819
pub mod anything_matcher;
1920
pub mod char_count_matcher;
2021
pub mod conjunction_matcher;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use googletest::matcher::Matcher;
16+
use googletest::prelude::*;
17+
use indoc::indoc;
18+
19+
#[test]
20+
fn does_not_match_value_when_list_is_empty() -> Result<()> {
21+
verify_that!((), not(any!()))
22+
}
23+
24+
#[test]
25+
fn matches_value_with_single_matching_component() -> Result<()> {
26+
verify_that!(123, any!(eq(123)))
27+
}
28+
29+
#[test]
30+
fn does_not_match_value_with_single_non_matching_component() -> Result<()> {
31+
verify_that!(123, not(any!(eq(456))))
32+
}
33+
34+
#[test]
35+
fn matches_value_with_first_of_two_matching_components() -> Result<()> {
36+
verify_that!("A string", any!(starts_with("A"), starts_with("string")))
37+
}
38+
39+
#[test]
40+
fn matches_value_with_second_of_two_matching_components() -> Result<()> {
41+
verify_that!("A string", any!(starts_with("string"), starts_with("A")))
42+
}
43+
44+
#[test]
45+
fn supports_trailing_comma() -> Result<()> {
46+
verify_that!(
47+
"An important string",
48+
any!(starts_with("An"), contains_substring("important"), ends_with("string"),)
49+
)
50+
}
51+
52+
#[test]
53+
fn admits_matchers_without_static_lifetime() -> Result<()> {
54+
#[derive(Debug, PartialEq)]
55+
struct AStruct(i32);
56+
let expected_value = AStruct(123);
57+
verify_that!(AStruct(123), any![eq_deref_of(&expected_value)])
58+
}
59+
60+
#[test]
61+
fn mismatch_description_two_failed_matchers() -> Result<()> {
62+
verify_that!(
63+
any!(starts_with("One"), starts_with("Two")).explain_match("Three"),
64+
displays_as(eq(
65+
"* which does not start with \"One\"\n * which does not start with \"Two\""
66+
))
67+
)
68+
}
69+
70+
#[test]
71+
fn mismatch_description_empty_matcher() -> Result<()> {
72+
verify_that!(any!().explain_match("Three"), displays_as(eq("which never matches")))
73+
}
74+
75+
#[test]
76+
fn all_multiple_failed_assertions() -> Result<()> {
77+
let result = verify_that!(4, any![eq(1), eq(2), eq(3)]);
78+
verify_that!(
79+
result,
80+
err(displays_as(contains_substring(indoc!(
81+
"
82+
Value of: 4
83+
Expected: has at least one of the following properties:
84+
* is equal to 1
85+
* is equal to 2
86+
* is equal to 3
87+
Actual: 4,
88+
* which isn't equal to 1
89+
* which isn't equal to 2
90+
* which isn't equal to 3"
91+
))))
92+
)
93+
}

googletest/tests/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
mod all_matcher_test;
16+
mod any_matcher_test;
1617
mod composition_test;
1718
mod elements_are_matcher_test;
1819
mod field_matcher_test;

0 commit comments

Comments
 (0)