diff --git a/googletest/crate_docs.md b/googletest/crate_docs.md index aace6567..42398d64 100644 --- a/googletest/crate_docs.md +++ b/googletest/crate_docs.md @@ -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. | diff --git a/googletest/src/lib.rs b/googletest/src/lib.rs index a02f1c3f..7780261f 100644 --- a/googletest/src/lib.rs +++ b/googletest/src/lib.rs @@ -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, }; } diff --git a/googletest/src/matchers/any_matcher.rs b/googletest/src/matchers/any_matcher.rs new file mode 100644 index 00000000..25a63039 --- /dev/null +++ b/googletest/src/matchers/any_matcher.rs @@ -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 + '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 + '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::().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::(); + 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::().describe(matcher_result), + 1 => self.components[0].describe(matcher_result), + _ => { + let properties = self + .components + .iter() + .map(|m| m.describe(matcher_result)) + .collect::() + .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 = 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 = 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 = 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 = any!(first_matcher); + + verify_that!( + matcher.explain_match("A string"), + displays_as(eq("which does not start with \"Another\"")) + ) + } +} diff --git a/googletest/src/matchers/mod.rs b/googletest/src/matchers/mod.rs index a46c29ca..60b29594 100644 --- a/googletest/src/matchers/mod.rs +++ b/googletest/src/matchers/mod.rs @@ -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; diff --git a/googletest/tests/any_matcher_test.rs b/googletest/tests/any_matcher_test.rs new file mode 100644 index 00000000..1bdd794b --- /dev/null +++ b/googletest/tests/any_matcher_test.rs @@ -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" + )))) + ) +} diff --git a/googletest/tests/lib.rs b/googletest/tests/lib.rs index e96519f6..f6b3ec10 100644 --- a/googletest/tests/lib.rs +++ b/googletest/tests/lib.rs @@ -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;