-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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!`.
- Loading branch information
Showing
6 changed files
with
305 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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\"")) | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
)))) | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters