Skip to content

Commit 2fb2002

Browse files
hovinenbcopybara-github
authored andcommitted
Support matching HashMap and similar collections in unordered_elements_are!.
This requires adding a new struct UnorderedElementsOfMapAreMatcher which parallels the existing UnorderedElementsAre. Due to limitations in the Rust type system, it does not appear to be possible to unify the implementations further. However, since nearly all of the actual logic is in other structs, the duplication is minimal. This also analogously renames UnorderedElementsAre to UnorderedElementsAreMatcher, in line with the naming convention used elsewhere in the library. PiperOrigin-RevId: 523632743
1 parent 7a16277 commit 2fb2002

File tree

2 files changed

+331
-20
lines changed

2 files changed

+331
-20
lines changed

googletest/src/matchers/unordered_elements_are_matcher.rs

Lines changed: 209 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@
2929
/// The actual value must be a container implementing [`IntoIterator`]. This
3030
/// includes standard containers, slices (when dereferenced) and arrays.
3131
///
32+
/// This can also match against [`HashMap`][std::collections::HashMap] and
33+
/// similar collections. The arguments are a sequence of pairs of matchers
34+
/// corresponding to the keys and their respective values.
35+
///
36+
/// ```
37+
/// let value: HashMap<u32, &'static str> =
38+
/// HashMap::from_iter([(1, "One"), (2, "Two"), (3, "Three")]);
39+
/// verify_that!(
40+
/// value,
41+
/// unordered_elements_are![(eq(2), eq("Two")), (eq(1), eq("One")), (eq(3), eq("Three"))]
42+
/// )
43+
/// ```
44+
///
3245
/// This matcher does not support matching directly against an [`Iterator`]. To
3346
/// match against an iterator, use [`Iterator::collect`] to build a [`Vec`].
3447
///
@@ -57,13 +70,40 @@
5770
/// [`Vec`]: https://doc.rust-lang.org/std/vec/struct.Vec.html
5871
#[macro_export]
5972
macro_rules! unordered_elements_are {
73+
($(,)?) => {{
74+
#[cfg(google3)]
75+
use $crate::internal::{UnorderedElementsAreMatcher, Requirements};
76+
#[cfg(not(google3))]
77+
use $crate::matchers::unordered_elements_are_matcher::internal::{
78+
UnorderedElementsAreMatcher, Requirements
79+
};
80+
UnorderedElementsAreMatcher::new([], Requirements::PerfectMatch)
81+
}};
82+
83+
// TODO: Consider an alternative map-like syntax here similar to that used in
84+
// https://crates.io/crates/maplit.
85+
($(($key_matcher:expr, $value_matcher:expr)),* $(,)?) => {{
86+
#[cfg(google3)]
87+
use $crate::internal::{UnorderedElementsOfMapAreMatcher, Requirements};
88+
#[cfg(not(google3))]
89+
use $crate::matchers::unordered_elements_are_matcher::internal::{
90+
UnorderedElementsOfMapAreMatcher, Requirements
91+
};
92+
UnorderedElementsOfMapAreMatcher::new(
93+
[$((&$key_matcher, &$value_matcher)),*],
94+
Requirements::PerfectMatch
95+
)
96+
}};
97+
6098
($($matcher:expr),* $(,)?) => {{
6199
#[cfg(google3)]
62-
use $crate::internal::{UnorderedElementsAre, Requirements};
100+
use $crate::internal::{UnorderedElementsAreMatcher, Requirements};
63101
#[cfg(not(google3))]
64-
use $crate::matchers::unordered_elements_are_matcher::internal::{UnorderedElementsAre, Requirements};
65-
UnorderedElementsAre::new([$(&$matcher),*], Requirements::PerfectMatch)
66-
}}
102+
use $crate::matchers::unordered_elements_are_matcher::internal::{
103+
UnorderedElementsAreMatcher, Requirements
104+
};
105+
UnorderedElementsAreMatcher::new([$(&$matcher),*], Requirements::PerfectMatch)
106+
}};
67107
}
68108

69109
/// Matches a container containing elements matched by the given matchers.
@@ -113,10 +153,12 @@ macro_rules! unordered_elements_are {
113153
macro_rules! contains_each {
114154
($($matcher:expr),* $(,)?) => {{
115155
#[cfg(google3)]
116-
use $crate::internal::{UnorderedElementsAre, Requirements};
156+
use $crate::internal::{UnorderedElementsAreMatcher, Requirements};
117157
#[cfg(not(google3))]
118-
use $crate::matchers::unordered_elements_are_matcher::internal::{UnorderedElementsAre, Requirements};
119-
UnorderedElementsAre::new([$(&$matcher),*], Requirements::Superset)
158+
use $crate::matchers::unordered_elements_are_matcher::internal::{
159+
UnorderedElementsAreMatcher, Requirements
160+
};
161+
UnorderedElementsAreMatcher::new([$(&$matcher),*], Requirements::Superset)
120162
}}
121163
}
122164

@@ -169,10 +211,12 @@ macro_rules! contains_each {
169211
macro_rules! is_contained_in {
170212
($($matcher:expr),* $(,)?) => {{
171213
#[cfg(google3)]
172-
use $crate::internal::{UnorderedElementsAre, Requirements};
214+
use $crate::internal::{UnorderedElementsAreMatcher, Requirements};
173215
#[cfg(not(google3))]
174-
use $crate::matchers::unordered_elements_are_matcher::internal::{UnorderedElementsAre, Requirements};
175-
UnorderedElementsAre::new([$(&$matcher),*], Requirements::Subset)
216+
use $crate::matchers::unordered_elements_are_matcher::internal::{
217+
UnorderedElementsAreMatcher, Requirements
218+
};
219+
UnorderedElementsAreMatcher::new([$(&$matcher),*], Requirements::Subset)
176220
}}
177221
}
178222

@@ -200,12 +244,12 @@ pub mod internal {
200244
///
201245
/// **For internal use only. API stablility is not guaranteed!**
202246
#[doc(hidden)]
203-
pub struct UnorderedElementsAre<'a, T: Debug, const N: usize> {
247+
pub struct UnorderedElementsAreMatcher<'a, T: Debug, const N: usize> {
204248
elements: [&'a dyn Matcher<T>; N],
205249
requirements: Requirements,
206250
}
207251

208-
impl<'a, T: Debug, const N: usize> UnorderedElementsAre<'a, T, N> {
252+
impl<'a, T: Debug, const N: usize> UnorderedElementsAreMatcher<'a, T, N> {
209253
pub fn new(elements: [&'a dyn Matcher<T>; N], requirements: Requirements) -> Self {
210254
Self { elements, requirements }
211255
}
@@ -214,14 +258,14 @@ pub mod internal {
214258
// This matcher performs the checks in three different steps in both `matches`
215259
// and `explain_match`. This is useful for performance but also to produce
216260
// an actionable error message.
217-
// 1. `UnorderedElementsAre` verifies that both collections have the same
261+
// 1. `UnorderedElementsAreMatcher` verifies that both collections have the same
218262
// size
219-
// 2. `UnorderedElementsAre` verifies that each actual element matches at least
220-
// one expected element and vice versa.
221-
// 3. `UnorderedElementsAre` verifies that a perfect matching exists using
222-
// Ford-Fulkerson.
263+
// 2. `UnorderedElementsAreMatcher` verifies that each actual element matches at
264+
// least one expected element and vice versa.
265+
// 3. `UnorderedElementsAreMatcher` verifies that a perfect matching exists
266+
// using Ford-Fulkerson.
223267
impl<'a, T: Debug, ContainerT: Debug + ?Sized, const N: usize> Matcher<ContainerT>
224-
for UnorderedElementsAre<'a, T, N>
268+
for UnorderedElementsAreMatcher<'a, T, N>
225269
where
226270
for<'b> &'b ContainerT: IntoIterator<Item = &'b T>,
227271
{
@@ -266,6 +310,76 @@ pub mod internal {
266310
}
267311
}
268312

313+
/// This is the analogue to [UnorderedElementsAreMatcher] for maps and
314+
/// map-like collections.
315+
///
316+
/// **For internal use only. API stablility is not guaranteed!**
317+
#[doc(hidden)]
318+
pub struct UnorderedElementsOfMapAreMatcher<'a, KeyT: Debug, ValueT: Debug, const N: usize> {
319+
elements: [(&'a dyn Matcher<KeyT>, &'a dyn Matcher<ValueT>); N],
320+
requirements: Requirements,
321+
}
322+
323+
impl<'a, KeyT: Debug, ValueT: Debug, const N: usize>
324+
UnorderedElementsOfMapAreMatcher<'a, KeyT, ValueT, N>
325+
{
326+
pub fn new(
327+
elements: [(&'a dyn Matcher<KeyT>, &'a dyn Matcher<ValueT>); N],
328+
requirements: Requirements,
329+
) -> Self {
330+
Self { elements, requirements }
331+
}
332+
}
333+
334+
impl<'a, KeyT: Debug, ValueT: Debug, ContainerT: Debug + ?Sized, const N: usize>
335+
Matcher<ContainerT> for UnorderedElementsOfMapAreMatcher<'a, KeyT, ValueT, N>
336+
where
337+
for<'b> &'b ContainerT: IntoIterator<Item = (&'b KeyT, &'b ValueT)>,
338+
{
339+
fn matches(&self, actual: &ContainerT) -> MatcherResult {
340+
let match_matrix = MatchMatrix::generate_for_map(actual, &self.elements);
341+
match_matrix.is_match_for(self.requirements).into()
342+
}
343+
344+
fn explain_match(&self, actual: &ContainerT) -> MatchExplanation {
345+
if let Some(size_mismatch_explanation) =
346+
self.requirements.explain_size_mismatch(actual, N)
347+
{
348+
return size_mismatch_explanation;
349+
}
350+
351+
let match_matrix = MatchMatrix::generate_for_map(actual, &self.elements);
352+
if let Some(unmatchable_explanation) =
353+
match_matrix.explain_unmatchable(self.requirements)
354+
{
355+
return unmatchable_explanation;
356+
}
357+
358+
let best_match = match_matrix.find_best_match();
359+
MatchExplanation::create(
360+
best_match
361+
.get_explanation_for_map(actual, &self.elements, self.requirements)
362+
.unwrap_or("whose elements all match".to_string()),
363+
)
364+
}
365+
366+
fn describe(&self, matcher_result: MatcherResult) -> String {
367+
format!(
368+
"{} elements matching in any order:\n{}",
369+
if matcher_result.into() { "contains" } else { "doesn't contain" },
370+
self.elements
371+
.iter()
372+
.map(|(key_matcher, value_matcher)| format!(
373+
"{} => {}",
374+
key_matcher.describe(MatcherResult::Matches),
375+
value_matcher.describe(MatcherResult::Matches)
376+
))
377+
.collect::<Description>()
378+
.indent()
379+
)
380+
}
381+
}
382+
269383
/// The requirements of the mapping between matchers and actual values by
270384
/// which [`UnorderedElemetnsAre`] is deemed to match its input.
271385
///
@@ -360,6 +474,25 @@ pub mod internal {
360474
matrix
361475
}
362476

477+
fn generate_for_map<'a, KeyT: Debug, ValueT: Debug, ContainerT: Debug + ?Sized>(
478+
actual: &ContainerT,
479+
expected: &[(&'a dyn Matcher<KeyT>, &'a dyn Matcher<ValueT>); N],
480+
) -> Self
481+
where
482+
for<'b> &'b ContainerT: IntoIterator<Item = (&'b KeyT, &'b ValueT)>,
483+
{
484+
let mut matrix =
485+
MatchMatrix(vec![[MatcherResult::DoesNotMatch; N]; count_elements(actual)]);
486+
for (actual_idx, (actual_key, actual_value)) in actual.into_iter().enumerate() {
487+
for (expected_idx, (expected_key, expected_value)) in expected.iter().enumerate() {
488+
matrix.0[actual_idx][expected_idx] = (expected_key.matches(actual_key).into()
489+
&& expected_value.matches(actual_value).into())
490+
.into();
491+
}
492+
}
493+
matrix
494+
}
495+
363496
fn is_match_for(&self, requirements: Requirements) -> bool {
364497
match requirements {
365498
Requirements::PerfectMatch => {
@@ -730,5 +863,63 @@ pub mod internal {
730863
"which does not have a {requirements} match with the expected elements. The best match found was:\n{best_match}"
731864
))
732865
}
866+
867+
fn get_explanation_for_map<'a, KeyT: Debug, ValueT: Debug, ContainerT: Debug + ?Sized>(
868+
&self,
869+
actual: &ContainerT,
870+
expected: &[(&'a dyn Matcher<KeyT>, &'a dyn Matcher<ValueT>); N],
871+
requirements: Requirements,
872+
) -> Option<String>
873+
where
874+
for<'b> &'b ContainerT: IntoIterator<Item = (&'b KeyT, &'b ValueT)>,
875+
{
876+
let actual: Vec<_> = actual.into_iter().collect();
877+
if self.is_full_match() {
878+
return None;
879+
}
880+
let mut error_message =
881+
format!("which does not have a {requirements} match with the expected elements.");
882+
883+
error_message.push_str("\n The best match found was: ");
884+
885+
let matches = self.get_matches()
886+
.map(|(actual_idx, expected_idx)| {
887+
format!(
888+
"Actual element {:?} => {:?} at index {actual_idx} matched expected element `{}` => `{}` at index {expected_idx}.",
889+
actual[actual_idx].0,
890+
actual[actual_idx].1,
891+
expected[expected_idx].0.describe(MatcherResult::Matches),
892+
expected[expected_idx].1.describe(MatcherResult::Matches),
893+
)
894+
});
895+
896+
let unmatched_actual = self.get_unmatched_actual()
897+
.map(|actual_idx| {
898+
format!(
899+
"Actual element {:#?} => {:#?} at index {actual_idx} did not match any remaining expected element.",
900+
actual[actual_idx].0,
901+
actual[actual_idx].1,
902+
)
903+
});
904+
905+
let unmatched_expected = self.get_unmatched_expected()
906+
.into_iter()
907+
.map(|expected_idx| {
908+
format!(
909+
"Expected element `{}` => `{}` at index {expected_idx} did not match any remaining actual element.",
910+
expected[expected_idx].0.describe(MatcherResult::Matches),
911+
expected[expected_idx].1.describe(MatcherResult::Matches),
912+
)
913+
});
914+
915+
let best_match = matches
916+
.chain(unmatched_actual)
917+
.chain(unmatched_expected)
918+
.collect::<Description>()
919+
.indent();
920+
Some(format!(
921+
"which does not have a {requirements} match with the expected elements. The best match found was:\n{best_match}"
922+
))
923+
}
733924
}
734925
}

0 commit comments

Comments
 (0)