1 // Copyright 2022 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 crate::description::Description;
16 use crate::matcher::{Matcher, MatcherResult};
17 use std::collections::HashMap;
18 use std::fmt::Debug;
19 use std::hash::Hash;
20 use std::marker::PhantomData;
21
22 /// Matches a HashMap containing the given `key` whose value is matched by the
23 /// matcher `inner`.
24 ///
25 /// ```
26 /// # use googletest::prelude::*;
27 /// # use std::collections::HashMap;
28 /// # fn should_pass() -> Result<()> {
29 /// let value = HashMap::from([(0, 1), (1, -1)]);
30 /// verify_that!(value, has_entry(0, eq(1)))?; // Passes
31 /// # Ok(())
32 /// # }
33 /// # fn should_fail_1() -> Result<()> {
34 /// # let value = HashMap::from([(0, 1), (1, -1)]);
35 /// verify_that!(value, has_entry(1, gt(0)))?; // Fails: value not matched
36 /// # Ok(())
37 /// # }
38 /// # fn should_fail_2() -> Result<()> {
39 /// # let value = HashMap::from([(0, 1), (1, -1)]);
40 /// verify_that!(value, has_entry(2, eq(0)))?; // Fails: key not present
41 /// # Ok(())
42 /// # }
43 /// # should_pass().unwrap();
44 /// # should_fail_1().unwrap_err();
45 /// # should_fail_2().unwrap_err();
46 /// ```
47 ///
48 /// Note: One could obtain the same effect by collecting entries into a `Vec`
49 /// and using `contains`:
50 ///
51 /// ```
52 /// # use googletest::prelude::*;
53 /// # use std::collections::HashMap;
54 /// # fn should_pass() -> Result<()> {
55 /// let value = HashMap::from([(0, 1), (1, -1)]);
56 /// verify_that!(value.into_iter().collect::<Vec<_>>(), contains(eq((0, 1))))?;
57 /// # Ok(())
58 /// # }
59 /// # should_pass().unwrap();
60 /// ```
61 ///
62 /// However, `has_entry` will offer somewhat better diagnostic messages in the
63 /// case of assertion failure. And it avoid the extra allocation hidden in the
64 /// code above.
has_entry<KeyT: Debug + Eq + Hash, ValueT: Debug, MatcherT: Matcher<ActualT = ValueT>>( key: KeyT, inner: MatcherT, ) -> impl Matcher<ActualT = HashMap<KeyT, ValueT>>65 pub fn has_entry<KeyT: Debug + Eq + Hash, ValueT: Debug, MatcherT: Matcher<ActualT = ValueT>>(
66 key: KeyT,
67 inner: MatcherT,
68 ) -> impl Matcher<ActualT = HashMap<KeyT, ValueT>> {
69 HasEntryMatcher { key, inner, phantom: Default::default() }
70 }
71
72 struct HasEntryMatcher<KeyT, ValueT, MatcherT> {
73 key: KeyT,
74 inner: MatcherT,
75 phantom: PhantomData<ValueT>,
76 }
77
78 impl<KeyT: Debug + Eq + Hash, ValueT: Debug, MatcherT: Matcher<ActualT = ValueT>> Matcher
79 for HasEntryMatcher<KeyT, ValueT, MatcherT>
80 {
81 type ActualT = HashMap<KeyT, ValueT>;
82
matches(&self, actual: &HashMap<KeyT, ValueT>) -> MatcherResult83 fn matches(&self, actual: &HashMap<KeyT, ValueT>) -> MatcherResult {
84 if let Some(value) = actual.get(&self.key) {
85 self.inner.matches(value)
86 } else {
87 MatcherResult::NoMatch
88 }
89 }
90
explain_match(&self, actual: &HashMap<KeyT, ValueT>) -> Description91 fn explain_match(&self, actual: &HashMap<KeyT, ValueT>) -> Description {
92 if let Some(value) = actual.get(&self.key) {
93 format!(
94 "which contains key {:?}, but is mapped to value {:#?}, {}",
95 self.key,
96 value,
97 self.inner.explain_match(value)
98 )
99 .into()
100 } else {
101 format!("which doesn't contain key {:?}", self.key).into()
102 }
103 }
104
describe(&self, matcher_result: MatcherResult) -> Description105 fn describe(&self, matcher_result: MatcherResult) -> Description {
106 match matcher_result {
107 MatcherResult::Match => format!(
108 "contains key {:?}, which value {}",
109 self.key,
110 self.inner.describe(MatcherResult::Match)
111 )
112 .into(),
113 MatcherResult::NoMatch => format!(
114 "doesn't contain key {:?} or contains key {:?}, which value {}",
115 self.key,
116 self.key,
117 self.inner.describe(MatcherResult::NoMatch)
118 )
119 .into(),
120 }
121 }
122 }
123
124 #[cfg(test)]
125 mod tests {
126 use super::has_entry;
127 use crate::prelude::*;
128 use indoc::indoc;
129 use std::collections::HashMap;
130
131 #[test]
has_entry_does_not_match_empty_hash_map() -> Result<()>132 fn has_entry_does_not_match_empty_hash_map() -> Result<()> {
133 let value: HashMap<i32, i32> = HashMap::new();
134 verify_that!(value, not(has_entry(0, eq(0))))
135 }
136
137 #[test]
has_entry_matches_hash_map_with_value() -> Result<()>138 fn has_entry_matches_hash_map_with_value() -> Result<()> {
139 let value: HashMap<i32, i32> = HashMap::from([(0, 0)]);
140 verify_that!(value, has_entry(0, eq(0)))
141 }
142
143 #[test]
has_entry_does_not_match_hash_map_with_wrong_value() -> Result<()>144 fn has_entry_does_not_match_hash_map_with_wrong_value() -> Result<()> {
145 let value: HashMap<i32, i32> = HashMap::from([(0, 1)]);
146 verify_that!(value, not(has_entry(0, eq(0))))
147 }
148
149 #[test]
has_entry_does_not_match_hash_map_with_wrong_key() -> Result<()>150 fn has_entry_does_not_match_hash_map_with_wrong_key() -> Result<()> {
151 let value: HashMap<i32, i32> = HashMap::from([(1, 0)]);
152 verify_that!(value, not(has_entry(0, eq(0))))
153 }
154
155 #[test]
has_entry_shows_correct_message_when_key_is_not_present() -> Result<()>156 fn has_entry_shows_correct_message_when_key_is_not_present() -> Result<()> {
157 let result = verify_that!(HashMap::from([(0, 0)]), has_entry(1, eq(0)));
158
159 verify_that!(
160 result,
161 err(displays_as(contains_substring(indoc!(
162 "
163 Value of: HashMap::from([(0, 0)])
164 Expected: contains key 1, which value is equal to 0
165 Actual: {0: 0},
166 which doesn't contain key 1
167 "
168 ))))
169 )
170 }
171
172 #[test]
has_entry_shows_correct_message_when_key_has_non_matching_value() -> Result<()>173 fn has_entry_shows_correct_message_when_key_has_non_matching_value() -> Result<()> {
174 let result = verify_that!(HashMap::from([(0, 0)]), has_entry(0, eq(1)));
175
176 verify_that!(
177 result,
178 err(displays_as(contains_substring(indoc!(
179 "
180 Value of: HashMap::from([(0, 0)])
181 Expected: contains key 0, which value is equal to 1
182 Actual: {0: 0},
183 which contains key 0, but is mapped to value 0, which isn't equal to 1
184 "
185 ))))
186 )
187 }
188 }
189