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::fmt::Debug;
18 use std::marker::PhantomData;
19 
20 /// Matches a container equal (in the sense of `==`) to `expected`.
21 ///
22 /// This is similar to [`crate::matchers::eq`] except that an assertion failure
23 /// message generated from this matcher will include the missing and unexpected
24 /// items in the actual value, e.g.:
25 ///
26 /// ```text
27 /// Expected container to equal [1, 2, 3]
28 ///   but was: [1, 2, 4]
29 ///   Missing: [3]
30 ///   Unexpected: [4]
31 /// ```
32 ///
33 /// The actual value must be a container such as a `Vec`, an array, or a
34 /// dereferenced slice. More precisely, a shared borrow of the actual value must
35 /// implement [`IntoIterator`] whose `Item` type implements
36 /// [`PartialEq<ExpectedT>`], where `ExpectedT` is the element type of the
37 /// expected value.
38 ///
39 /// If the container type is a `Vec`, then the expected type may be a slice of
40 /// the same element type. For example:
41 ///
42 /// ```
43 /// # use googletest::prelude::*;
44 /// # fn should_pass() -> Result<()> {
45 /// let vec = vec![1, 2, 3];
46 /// verify_that!(vec, container_eq([1, 2, 3]))?;
47 /// #     Ok(())
48 /// # }
49 /// # should_pass().unwrap();
50 /// ```
51 ///
52 /// As an exception, if the actual type is a `Vec<String>`, the expected type
53 /// may be a slice of `&str`:
54 ///
55 /// ```
56 /// # use googletest::prelude::*;
57 /// # fn should_pass() -> Result<()> {
58 /// let vec: Vec<String> = vec!["A string".into(), "Another string".into()];
59 /// verify_that!(vec, container_eq(["A string", "Another string"]))?;
60 /// #     Ok(())
61 /// # }
62 /// # should_pass().unwrap();
63 /// ```
64 ///
65 /// These exceptions allow one to avoid unnecessary allocations in test
66 /// assertions.
67 ///
68 /// One can also check container equality of a slice with an array. To do so,
69 /// dereference the slice:
70 ///
71 /// ```
72 /// # use googletest::prelude::*;
73 /// # fn should_pass() -> Result<()> {
74 /// let value = &[1, 2, 3];
75 /// verify_that!(*value, container_eq([1, 2, 3]))?;
76 /// #     Ok(())
77 /// # }
78 /// # should_pass().unwrap();
79 /// ```
80 ///
81 /// Otherwise, the actual and expected types must be identical.
82 ///
83 /// *Performance note*: In the event of a mismatch leading to an assertion
84 /// failure, the construction of the lists of missing and unexpected values
85 /// uses a naive algorithm requiring time proportional to the product of the
86 /// sizes of the expected and actual values. This should therefore only be used
87 /// when the containers are small enough that this is not a problem.
88 // This returns ContainerEqMatcher and not impl Matcher because
89 // ContainerEqMatcher has some specialisations for slice types (see
90 // documentation above). Returning impl Matcher would hide those from the
91 // compiler.
container_eq<ActualContainerT, ExpectedContainerT>( expected: ExpectedContainerT, ) -> ContainerEqMatcher<ActualContainerT, ExpectedContainerT> where ActualContainerT: PartialEq<ExpectedContainerT> + Debug + ?Sized, ExpectedContainerT: Debug,92 pub fn container_eq<ActualContainerT, ExpectedContainerT>(
93     expected: ExpectedContainerT,
94 ) -> ContainerEqMatcher<ActualContainerT, ExpectedContainerT>
95 where
96     ActualContainerT: PartialEq<ExpectedContainerT> + Debug + ?Sized,
97     ExpectedContainerT: Debug,
98 {
99     ContainerEqMatcher { expected, phantom: Default::default() }
100 }
101 
102 pub struct ContainerEqMatcher<ActualContainerT: ?Sized, ExpectedContainerT> {
103     expected: ExpectedContainerT,
104     phantom: PhantomData<ActualContainerT>,
105 }
106 
107 impl<ActualElementT, ActualContainerT, ExpectedElementT, ExpectedContainerT> Matcher
108     for ContainerEqMatcher<ActualContainerT, ExpectedContainerT>
109 where
110     ActualElementT: PartialEq<ExpectedElementT> + Debug + ?Sized,
111     ActualContainerT: PartialEq<ExpectedContainerT> + Debug + ?Sized,
112     ExpectedElementT: Debug,
113     ExpectedContainerT: Debug,
114     for<'a> &'a ActualContainerT: IntoIterator<Item = &'a ActualElementT>,
115     for<'a> &'a ExpectedContainerT: IntoIterator<Item = &'a ExpectedElementT>,
116 {
117     type ActualT = ActualContainerT;
118 
matches(&self, actual: &ActualContainerT) -> MatcherResult119     fn matches(&self, actual: &ActualContainerT) -> MatcherResult {
120         (*actual == self.expected).into()
121     }
122 
explain_match(&self, actual: &ActualContainerT) -> Description123     fn explain_match(&self, actual: &ActualContainerT) -> Description {
124         build_explanation(self.get_missing_items(actual), self.get_unexpected_items(actual)).into()
125     }
126 
describe(&self, matcher_result: MatcherResult) -> Description127     fn describe(&self, matcher_result: MatcherResult) -> Description {
128         match matcher_result {
129             MatcherResult::Match => format!("is equal to {:?}", self.expected).into(),
130             MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected).into(),
131         }
132     }
133 }
134 
135 impl<ActualElementT, ActualContainerT, ExpectedElementT, ExpectedContainerT>
136     ContainerEqMatcher<ActualContainerT, ExpectedContainerT>
137 where
138     ActualElementT: PartialEq<ExpectedElementT> + ?Sized,
139     ActualContainerT: PartialEq<ExpectedContainerT> + ?Sized,
140     for<'a> &'a ActualContainerT: IntoIterator<Item = &'a ActualElementT>,
141     for<'a> &'a ExpectedContainerT: IntoIterator<Item = &'a ExpectedElementT>,
142 {
get_missing_items(&self, actual: &ActualContainerT) -> Vec<&ExpectedElementT>143     fn get_missing_items(&self, actual: &ActualContainerT) -> Vec<&ExpectedElementT> {
144         self.expected.into_iter().filter(|&i| !actual.into_iter().any(|j| j == i)).collect()
145     }
146 
get_unexpected_items<'a>(&self, actual: &'a ActualContainerT) -> Vec<&'a ActualElementT>147     fn get_unexpected_items<'a>(&self, actual: &'a ActualContainerT) -> Vec<&'a ActualElementT> {
148         actual.into_iter().filter(|&i| !self.expected.into_iter().any(|j| i == j)).collect()
149     }
150 }
151 
build_explanation<T: Debug, U: Debug>(missing: Vec<T>, unexpected: Vec<U>) -> String152 fn build_explanation<T: Debug, U: Debug>(missing: Vec<T>, unexpected: Vec<U>) -> String {
153     match (missing.len(), unexpected.len()) {
154         // TODO(b/261175849) add more data here (out of order elements, duplicated elements, etc...)
155         (0, 0) => "which contains all the elements".to_string(),
156         (0, 1) => format!("which contains the unexpected element {:?}", unexpected[0]),
157         (0, _) => format!("which contains the unexpected elements {unexpected:?}",),
158         (1, 0) => format!("which is missing the element {:?}", missing[0]),
159         (1, 1) => {
160             format!(
161                 "which is missing the element {:?} and contains the unexpected element {:?}",
162                 missing[0], unexpected[0]
163             )
164         }
165         (1, _) => {
166             format!(
167                 "which is missing the element {:?} and contains the unexpected elements {unexpected:?}",
168                 missing[0]
169             )
170         }
171         (_, 0) => format!("which is missing the elements {missing:?}"),
172         (_, 1) => {
173             format!(
174                 "which is missing the elements {missing:?} and contains the unexpected element {:?}",
175                 unexpected[0]
176             )
177         }
178         (_, _) => {
179             format!(
180                 "which is missing the elements {missing:?} and contains the unexpected elements {unexpected:?}",
181             )
182         }
183     }
184 }
185 
186 #[cfg(test)]
187 mod tests {
188     use super::container_eq;
189     use crate::matcher::{Matcher, MatcherResult};
190     use crate::prelude::*;
191     use indoc::indoc;
192     use std::collections::HashSet;
193 
194     #[test]
container_eq_returns_match_when_containers_match() -> Result<()>195     fn container_eq_returns_match_when_containers_match() -> Result<()> {
196         verify_that!(vec![1, 2, 3], container_eq(vec![1, 2, 3]))
197     }
198 
199     #[test]
container_eq_matches_array_with_slice() -> Result<()>200     fn container_eq_matches_array_with_slice() -> Result<()> {
201         let value = &[1, 2, 3];
202         verify_that!(*value, container_eq([1, 2, 3]))
203     }
204 
205     #[test]
container_eq_matches_hash_set() -> Result<()>206     fn container_eq_matches_hash_set() -> Result<()> {
207         let value: HashSet<i32> = [1, 2, 3].into();
208         verify_that!(value, container_eq([1, 2, 3].into()))
209     }
210 
211     #[test]
container_eq_full_error_message() -> Result<()>212     fn container_eq_full_error_message() -> Result<()> {
213         let result = verify_that!(vec![1, 3, 2], container_eq(vec![1, 2, 3]));
214         verify_that!(
215             result,
216             err(displays_as(contains_substring(indoc!(
217                 "
218                     Value of: vec![1, 3, 2]
219                     Expected: is equal to [1, 2, 3]
220                     Actual: [1, 3, 2],
221                       which contains all the elements
222                 "
223             ))))
224         )
225     }
226 
227     #[test]
container_eq_returns_mismatch_when_elements_out_of_order() -> Result<()>228     fn container_eq_returns_mismatch_when_elements_out_of_order() -> Result<()> {
229         verify_that!(
230             container_eq(vec![1, 2, 3]).explain_match(&vec![1, 3, 2]),
231             displays_as(eq("which contains all the elements"))
232         )
233     }
234 
235     #[test]
container_eq_mismatch_shows_missing_elements_in_container() -> Result<()>236     fn container_eq_mismatch_shows_missing_elements_in_container() -> Result<()> {
237         verify_that!(
238             container_eq(vec![1, 2, 3]).explain_match(&vec![1, 2]),
239             displays_as(eq("which is missing the element 3"))
240         )
241     }
242 
243     #[test]
container_eq_mismatch_shows_surplus_elements_in_container() -> Result<()>244     fn container_eq_mismatch_shows_surplus_elements_in_container() -> Result<()> {
245         verify_that!(
246             container_eq(vec![1, 2]).explain_match(&vec![1, 2, 3]),
247             displays_as(eq("which contains the unexpected element 3"))
248         )
249     }
250 
251     #[test]
container_eq_mismatch_shows_missing_and_surplus_elements_in_container() -> Result<()>252     fn container_eq_mismatch_shows_missing_and_surplus_elements_in_container() -> Result<()> {
253         verify_that!(
254             container_eq(vec![1, 2, 3]).explain_match(&vec![1, 2, 4]),
255             displays_as(eq("which is missing the element 3 and contains the unexpected element 4"))
256         )
257     }
258 
259     #[test]
container_eq_mismatch_does_not_show_duplicated_element() -> Result<()>260     fn container_eq_mismatch_does_not_show_duplicated_element() -> Result<()> {
261         verify_that!(
262             container_eq(vec![1, 2, 3]).explain_match(&vec![1, 2, 3, 3]),
263             displays_as(eq("which contains all the elements"))
264         )
265     }
266 
267     #[test]
container_eq_matches_owned_vec_with_array() -> Result<()>268     fn container_eq_matches_owned_vec_with_array() -> Result<()> {
269         let vector = vec![123, 234];
270         verify_that!(vector, container_eq([123, 234]))
271     }
272 
273     #[test]
container_eq_matches_owned_vec_of_owned_strings_with_slice_of_string_references() -> Result<()>274     fn container_eq_matches_owned_vec_of_owned_strings_with_slice_of_string_references()
275     -> Result<()> {
276         let vector = vec!["A string".to_string(), "Another string".to_string()];
277         verify_that!(vector, container_eq(["A string", "Another string"]))
278     }
279 
280     #[test]
container_eq_matches_owned_vec_of_owned_strings_with_shorter_slice_of_string_references() -> Result<()>281     fn container_eq_matches_owned_vec_of_owned_strings_with_shorter_slice_of_string_references()
282     -> Result<()> {
283         let actual = vec!["A string".to_string(), "Another string".to_string()];
284         let matcher = container_eq(["A string"]);
285 
286         let result = matcher.matches(&actual);
287 
288         verify_that!(result, eq(MatcherResult::NoMatch))
289     }
290 
291     #[test]
container_eq_mismatch_with_slice_shows_missing_elements_in_container() -> Result<()>292     fn container_eq_mismatch_with_slice_shows_missing_elements_in_container() -> Result<()> {
293         verify_that!(
294             container_eq([1, 2, 3]).explain_match(&vec![1, 2]),
295             displays_as(eq("which is missing the element 3"))
296         )
297     }
298 
299     #[test]
container_eq_mismatch_with_str_slice_shows_missing_elements_in_container() -> Result<()>300     fn container_eq_mismatch_with_str_slice_shows_missing_elements_in_container() -> Result<()> {
301         verify_that!(
302             container_eq(["A", "B", "C"]).explain_match(&vec!["A".to_string(), "B".to_string()]),
303             displays_as(eq("which is missing the element \"C\""))
304         )
305     }
306 
307     #[test]
container_eq_mismatch_with_str_slice_shows_surplus_elements_in_container() -> Result<()>308     fn container_eq_mismatch_with_str_slice_shows_surplus_elements_in_container() -> Result<()> {
309         verify_that!(
310             container_eq(["A", "B"]).explain_match(&vec![
311                 "A".to_string(),
312                 "B".to_string(),
313                 "C".to_string()
314             ]),
315             displays_as(eq("which contains the unexpected element \"C\""))
316         )
317     }
318 }
319