Skip to main content

test_that/matchers/containers/
container_eq_matcher.rs

1// Copyright 2022 Google LLC
2// Copyright 2026 Bradford Hovinen <bradford@hovinen.me>
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//      http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::{
17    description::Description,
18    matcher::{Describable, Matcher, MatcherResult},
19    matchers::{
20        containers::{
21            OwnedItems, RefItems, container_contains::unordered_matcher::__internal::MatchMatrix,
22        },
23        eq, eq_deref_of,
24    },
25};
26use alloc::{
27    string::{String, ToString},
28    vec::Vec,
29};
30use core::{fmt::Debug, marker::PhantomData};
31
32use super::container_contains::Requirements;
33
34/// Matches a container equal (in the sense of `==`) to `expected`.
35///
36/// This is similar to [`crate::matchers::eq`] except that an assertion failure
37/// message generated from this matcher will include the missing and unexpected
38/// items in the actual value, e.g.:
39///
40/// ```text
41/// Expected container to equal [1, 2, 3]
42///   but was: [1, 2, 4]
43///   Missing: [3]
44///   Unexpected: [4]
45/// ```
46///
47/// The actual value must be a container such as a `Vec`, an array, or a
48/// dereferenced slice. More precisely, a shared borrow of the actual value must
49/// implement [`IntoIterator`] whose `Item` type implements
50/// [`PartialEq<ExpectedT>`], where `ExpectedT` is the element type of the
51/// expected value.
52///
53/// If the container type is a `Vec`, then the expected type may be a slice of
54/// the same element type. For example:
55///
56/// ```
57/// # use test_that::prelude::*;
58/// # fn should_pass() -> TestResult<()> {
59/// let vec = vec![1, 2, 3];
60/// verify_that!(vec, container_eq([1, 2, 3]))?;
61/// #     Ok(())
62/// # }
63/// # should_pass().unwrap();
64/// ```
65///
66/// As an exception, if the actual type is a `Vec<String>`, the expected type
67/// may be a slice of `&str`:
68///
69/// ```
70/// # use test_that::prelude::*;
71/// # fn should_pass() -> TestResult<()> {
72/// let vec: Vec<String> = vec!["A string".into(), "Another string".into()];
73/// verify_that!(vec, container_eq(["A string", "Another string"]))?;
74/// #     Ok(())
75/// # }
76/// # should_pass().unwrap();
77/// ```
78///
79/// These exceptions allow one to avoid unnecessary allocations in test
80/// assertions.
81///
82/// One can also check container equality of a slice with an array. To do so,
83/// dereference the slice:
84///
85/// ```
86/// # use test_that::prelude::*;
87/// # fn should_pass() -> TestResult<()> {
88/// let value = &[1, 2, 3];
89/// verify_that!(*value, container_eq([1, 2, 3]))?;
90/// #     Ok(())
91/// # }
92/// # should_pass().unwrap();
93/// ```
94///
95/// Otherwise, the actual and expected types must be identical.
96///
97/// *Performance note*: In the event of a mismatch leading to an assertion
98/// failure, the construction of the lists of missing and unexpected values
99/// uses a naive algorithm requiring time proportional to the product of the
100/// sizes of the expected and actual values. This should therefore only be used
101/// when the containers are small enough that this is not a problem.
102///
103/// ## Comparing conatiners while ignoring order
104///
105/// Use [ignoring_order()][ContainerEqMatcher::ignoring_order] to ignore the
106/// order of the elements.
107///
108/// ```
109/// # use test_that::prelude::*;
110/// # fn should_pass() -> TestResult<()> {
111/// verify_that!(vec![1, 2, 3], container_eq([3, 2, 1]).ignoring_order())?;
112/// #     Ok(())
113/// # }
114/// # should_pass().unwrap();
115/// ```
116// This returns ContainerEqMatcher and not impl Matcher because
117// ContainerEqMatcher has some specialisations for slice types (see
118// documentation above). Returning impl Matcher would hide those from the
119// compiler.
120pub fn container_eq<ExpectedContainerT, Mode>(
121    expected: ExpectedContainerT,
122) -> ContainerEqMatcher<ExpectedContainerT, Mode> {
123    ContainerEqMatcher { expected, ignore_order: false, phantom: Default::default() }
124}
125
126/// A matcher which matches a container equal (in the sense of [`PartialEq`])
127/// to the given value.
128pub struct ContainerEqMatcher<ExpectedContainerT, Mode> {
129    expected: ExpectedContainerT,
130    ignore_order: bool,
131    phantom: PhantomData<Mode>,
132}
133
134impl<ExpectedContainerT, Mode> ContainerEqMatcher<ExpectedContainerT, Mode> {
135    /// Ignores the order of the elements when comparing containers.
136    ///
137    /// The containers will be deemed equal if there is a 1:1 correspondence of
138    /// elements in the actual and expected containers in which each actual
139    /// element equals (in the sense of [`PartialEq`]) its corresponding
140    /// expected element.
141    pub fn ignoring_order(self) -> Self {
142        Self { ignore_order: true, ..self }
143    }
144}
145
146impl<ActualElementT, ActualContainerT, ExpectedElementT, ExpectedContainerT>
147    Matcher<ActualContainerT> for ContainerEqMatcher<ExpectedContainerT, RefItems>
148where
149    ActualElementT: PartialEq<ExpectedElementT> + Debug + ?Sized,
150    ActualContainerT: PartialEq<ExpectedContainerT> + Debug + ?Sized,
151    ExpectedElementT: Debug,
152    ExpectedContainerT: Debug,
153    for<'a> &'a ActualContainerT: IntoIterator<Item = &'a ActualElementT>,
154    for<'a> &'a ExpectedContainerT: IntoIterator<Item = &'a ExpectedElementT>,
155{
156    fn matches(&self, actual: &ActualContainerT) -> MatcherResult {
157        if self.ignore_order {
158            let expected: Vec<_> = self.expected.into_iter().map(eq_deref_of).collect();
159            let match_matrix = MatchMatrix::generate_for_fixed_matcher::<_, ActualElementT, _>(
160                actual.into_iter(),
161                expected.len(),
162                &expected,
163            );
164            match_matrix.is_match_for(Requirements::PerfectMatch).into()
165        } else {
166            (*actual == self.expected).into()
167        }
168    }
169
170    fn explain_match(&self, actual: &ActualContainerT) -> Description {
171        build_explanation(
172            self.get_missing_items(actual),
173            self.get_unexpected_items(actual),
174            self.matches(actual),
175        )
176        .into()
177    }
178}
179
180impl<ExpectedElementT, ExpectedContainerT> ContainerEqMatcher<ExpectedContainerT, RefItems>
181where
182    for<'a> &'a ExpectedContainerT: IntoIterator<Item = &'a ExpectedElementT>,
183{
184    fn get_missing_items<ActualElementT, ActualContainerT>(
185        &self,
186        actual: &ActualContainerT,
187    ) -> Vec<&ExpectedElementT>
188    where
189        ActualElementT: PartialEq<ExpectedElementT> + ?Sized,
190        ActualContainerT: PartialEq<ExpectedContainerT> + ?Sized,
191        ExpectedElementT: Debug,
192        for<'a> &'a ActualContainerT: IntoIterator<Item = &'a ActualElementT>,
193    {
194        self.expected.into_iter().filter(|&i| !actual.into_iter().any(|j| j == i)).collect()
195    }
196
197    fn get_unexpected_items<'a, ActualElementT, ActualContainerT>(
198        &self,
199        actual: &'a ActualContainerT,
200    ) -> Vec<&'a ActualElementT>
201    where
202        ActualElementT: PartialEq<ExpectedElementT> + ?Sized,
203        ActualContainerT: PartialEq<ExpectedContainerT> + ?Sized,
204        ExpectedElementT: Debug,
205        for<'b> &'b ActualContainerT: IntoIterator<Item = &'b ActualElementT>,
206    {
207        actual.into_iter().filter(|&i| !self.expected.into_iter().any(|j| i == j)).collect()
208    }
209}
210
211impl<ActualElementT, ActualContainerT, ExpectedElementT, ExpectedContainerT>
212    Matcher<ActualContainerT> for ContainerEqMatcher<ExpectedContainerT, OwnedItems>
213where
214    ActualElementT: PartialEq<ExpectedElementT> + Debug,
215    ActualContainerT: PartialEq<ExpectedContainerT> + Debug + ?Sized,
216    ExpectedElementT: Debug,
217    ExpectedContainerT: Debug,
218    for<'a> &'a ActualContainerT: IntoIterator<Item = ActualElementT>,
219    for<'a> &'a ExpectedContainerT: IntoIterator<Item = ExpectedElementT>,
220{
221    fn matches(&self, actual: &ActualContainerT) -> MatcherResult {
222        if self.ignore_order {
223            let expected: Vec<_> = self.expected.into_iter().map(eq).collect();
224            let match_matrix = MatchMatrix::generate_for_fixed_matcher(
225                actual.into_iter(),
226                expected.len(),
227                &expected,
228            );
229            match_matrix.is_match_for(Requirements::PerfectMatch).into()
230        } else {
231            (*actual == self.expected).into()
232        }
233    }
234
235    fn explain_match(&self, actual: &ActualContainerT) -> Description {
236        build_explanation(
237            self.get_missing_items(actual),
238            self.get_unexpected_items(actual),
239            self.matches(actual),
240        )
241        .into()
242    }
243}
244
245impl<ExpectedElementT, ExpectedContainerT> ContainerEqMatcher<ExpectedContainerT, OwnedItems>
246where
247    for<'a> &'a ExpectedContainerT: IntoIterator<Item = ExpectedElementT>,
248{
249    fn get_missing_items<ActualElementT, ActualContainerT>(
250        &self,
251        actual: &ActualContainerT,
252    ) -> Vec<ExpectedElementT>
253    where
254        ActualElementT: PartialEq<ExpectedElementT>,
255        ActualContainerT: PartialEq<ExpectedContainerT> + ?Sized,
256        ExpectedElementT: Debug,
257        for<'a> &'a ActualContainerT: IntoIterator<Item = ActualElementT>,
258    {
259        self.expected.into_iter().filter(|i| !actual.into_iter().any(|j| j == *i)).collect()
260    }
261
262    fn get_unexpected_items<'a, ActualElementT, ActualContainerT>(
263        &self,
264        actual: &'a ActualContainerT,
265    ) -> Vec<ActualElementT>
266    where
267        ActualElementT: PartialEq<ExpectedElementT>,
268        ActualContainerT: PartialEq<ExpectedContainerT> + ?Sized,
269        ExpectedElementT: Debug,
270        for<'b> &'b ActualContainerT: IntoIterator<Item = ActualElementT>,
271    {
272        actual.into_iter().filter(|i| !self.expected.into_iter().any(|j| *i == j)).collect()
273    }
274}
275
276impl<ExpectedContainerT: Debug, Mode> Describable for ContainerEqMatcher<ExpectedContainerT, Mode> {
277    fn describe(&self, matcher_result: MatcherResult) -> Description {
278        match matcher_result {
279            MatcherResult::Match => format!("is equal to {:?}", self.expected).into(),
280            MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected).into(),
281        }
282    }
283}
284
285fn build_explanation<T: Debug, U: Debug>(
286    missing: Vec<T>,
287    unexpected: Vec<U>,
288    matcher_result: MatcherResult,
289) -> String {
290    match (missing.len(), unexpected.len(), matcher_result) {
291        // TODO(b/261175849) add more data here (out of order elements, duplicated elements, etc...)
292        (0, 0, MatcherResult::NoMatch) => {
293            "which contains all the elements but in the wrong order".to_string()
294        }
295        (0, 0, MatcherResult::Match) => "which contains all the elements".to_string(),
296        (0, 1, _) => format!("which contains the unexpected element {:?}", unexpected[0]),
297        (0, _, _) => format!("which contains the unexpected elements {unexpected:?}",),
298        (1, 0, _) => format!("which is missing the element {:?}", missing[0]),
299        (1, 1, _) => {
300            format!(
301                "which is missing the element {:?} and contains the unexpected element {:?}",
302                missing[0], unexpected[0]
303            )
304        }
305        (1, _, _) => {
306            format!(
307                "which is missing the element {:?} and contains the unexpected elements {unexpected:?}",
308                missing[0]
309            )
310        }
311        (_, 0, _) => format!("which is missing the elements {missing:?}"),
312        (_, 1, _) => {
313            format!(
314                "which is missing the elements {missing:?} and contains the unexpected element {:?}",
315                unexpected[0]
316            )
317        }
318        (_, _, _) => {
319            format!(
320                "which is missing the elements {missing:?} and contains the unexpected elements {unexpected:?}",
321            )
322        }
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use crate::prelude::*;
329    use alloc::{
330        string::{String, ToString},
331        vec::Vec,
332    };
333    use indoc::indoc;
334
335    #[test]
336    fn container_eq_returns_match_when_containers_match() -> TestResult<()> {
337        verify_that!(vec![1, 2, 3], container_eq(vec![1, 2, 3]))
338    }
339
340    #[test]
341    fn container_eq_matches_array_with_array() -> TestResult<()> {
342        verify_that!([1, 2, 3], container_eq([1, 2, 3]))
343    }
344
345    #[test]
346    fn container_eq_matches_vec_with_array() -> TestResult<()> {
347        verify_that!(vec![1, 2, 3], container_eq([1, 2, 3]))
348    }
349
350    #[test]
351    fn container_eq_matches_slice_using_points_to() -> TestResult<()> {
352        let value = vec![1, 2, 3];
353        let slice = value.as_slice();
354        verify_that!(slice, points_to(container_eq([1, 2, 3])))
355    }
356
357    #[test]
358    fn container_eq_matches_slice_using_deref_notation() -> TestResult<()> {
359        let value = vec![1, 2, 3];
360        let slice = value.as_slice();
361        verify_that!(*slice, container_eq([1, 2, 3]))
362    }
363
364    #[test]
365    fn container_eq_matches_ref_to_array_using_points_to() -> TestResult<()> {
366        verify_that!(&[1, 2, 3], points_to(container_eq([1, 2, 3])))
367    }
368
369    #[test]
370    fn container_eq_matches_ref_to_array_using_deref_notation() -> TestResult<()> {
371        let value = &[1, 2, 3];
372        verify_that!(*value, container_eq([1, 2, 3]))
373    }
374
375    #[test]
376    fn container_eq_matches_owned_vec_of_owned_strings_with_array_of_string_slices()
377    -> TestResult<()> {
378        verify_that!(
379            vec!["A string".to_string(), "Another string".to_string()],
380            container_eq(["A string", "Another string"])
381        )
382    }
383
384    #[test]
385    fn container_eq_does_not_match_vec_of_owned_strings_with_shorter_array_of_string_slices()
386    -> TestResult<()> {
387        verify_that!(
388            vec!["A string".to_string(), "Another string".to_string()],
389            not(container_eq(["A string"]))
390        )
391    }
392
393    #[test]
394    fn container_eq_matches_vec_of_string_slices_with_array_of_string_slices() -> TestResult<()> {
395        verify_that!(
396            vec!["A string", "Another string"],
397            container_eq(["A string", "Another string"])
398        )
399    }
400
401    #[test]
402    fn container_eq_matches_array_of_string_slices_with_array_of_string_slices() -> TestResult<()> {
403        verify_that!(["A string", "Another string"], container_eq(["A string", "Another string"]))
404    }
405
406    #[test]
407    fn container_eq_matches_array_of_string_slices_with_non_static_lifetime_with_array_of_string_slices()
408    -> TestResult<()> {
409        let string_1 = String::from("A string");
410        let string_2 = String::from("Another string");
411        verify_that!(
412            [string_1.as_str(), string_2.as_str()],
413            container_eq(["A string", "Another string"])
414        )
415    }
416
417    #[cfg(feature = "std")]
418    #[test]
419    fn container_eq_matches_hash_set_with_array() -> TestResult<()> {
420        use std::collections::HashSet;
421        verify_that!(HashSet::from([1, 2, 3]), container_eq([1, 2, 3].into()))
422    }
423
424    #[test]
425    fn container_eq_produces_correct_failure_message() -> TestResult<()> {
426        let result = verify_that!(vec![1, 3, 2], container_eq(vec![1, 2, 3]));
427        verify_that!(
428            result,
429            err(displays_as(contains_substring(indoc!(
430                "
431                    Value of: vec![1, 3, 2]
432                    Expected: is equal to [1, 2, 3]
433                    Actual: [1, 3, 2],
434                      which contains all the elements but in the wrong order
435                "
436            ))))
437        )
438    }
439
440    #[test]
441    fn container_eq_returns_mismatch_when_elements_out_of_order() -> TestResult<()> {
442        let result = verify_that!(vec![1, 3, 2], container_eq(vec![1, 2, 3]));
443        verify_that!(
444            result,
445            err(displays_as(contains_substring(
446                "which contains all the elements but in the wrong order"
447            )))
448        )
449    }
450
451    #[test]
452    fn container_eq_does_not_show_part_about_wrong_order_when_ignoring_order() -> TestResult<()> {
453        let result = verify_that!(vec![1, 3, 2], not(container_eq(vec![1, 2, 3]).ignoring_order()));
454        verify_that!(result, err(displays_as(not(contains_substring("but in the wrong order")))))
455    }
456
457    #[test]
458    fn container_eq_mismatch_shows_missing_elements_in_container() -> TestResult<()> {
459        let result = verify_that!(vec![1, 2], container_eq(vec![1, 2, 3]));
460        verify_that!(result, err(displays_as(contains_substring("which is missing the element 3"))))
461    }
462
463    #[test]
464    fn container_eq_mismatch_shows_missing_elements_in_container_when_matching_vec_with_array()
465    -> TestResult<()> {
466        let result = verify_that!(vec![1, 2], container_eq([1, 2, 3]));
467        verify_that!(result, err(displays_as(contains_substring("which is missing the element 3"))))
468    }
469
470    #[test]
471    fn container_eq_mismatch_shows_surplus_elements_in_container() -> TestResult<()> {
472        let result = verify_that!(vec![1, 2, 3], container_eq(vec![1, 2]));
473        verify_that!(
474            result,
475            err(displays_as(contains_substring("which contains the unexpected element 3")))
476        )
477    }
478
479    #[test]
480    fn container_eq_mismatch_shows_missing_and_surplus_elements_in_container() -> TestResult<()> {
481        let result = verify_that!(vec![1, 2, 4], container_eq(vec![1, 2, 3]));
482        verify_that!(
483            result,
484            err(displays_as(contains_substring(
485                "which is missing the element 3 and contains the unexpected element 4"
486            )))
487        )
488    }
489
490    #[test]
491    fn container_eq_mismatch_does_not_show_duplicated_element() -> TestResult<()> {
492        let result = verify_that!(vec![1, 2, 3, 3], container_eq(vec![1, 2, 3]));
493        verify_that!(
494            result,
495            err(displays_as(contains_substring("which contains all the elements")))
496        )
497    }
498
499    #[test]
500    fn container_eq_mismatch_with_str_slice_shows_missing_elements_in_container() -> TestResult<()>
501    {
502        let result =
503            verify_that!(vec!["A".to_string(), "B".to_string()], container_eq(["A", "B", "C"]));
504        verify_that!(
505            result,
506            err(displays_as(contains_substring(r#"which is missing the element "C""#)))
507        )
508    }
509
510    #[test]
511    fn container_eq_mismatch_with_str_slice_shows_surplus_elements_in_container() -> TestResult<()>
512    {
513        let result = verify_that!(
514            vec!["A".to_string(), "B".to_string(), "C".to_string()],
515            container_eq(["A", "B"])
516        );
517        verify_that!(
518            result,
519            err(displays_as(contains_substring(r#"which contains the unexpected element "C""#)))
520        )
521    }
522
523    #[derive(Debug, PartialEq)]
524    struct OwnedItemContainer(Vec<i32>);
525
526    impl<'a> IntoIterator for &'a OwnedItemContainer {
527        type Item = i32;
528        type IntoIter = core::iter::Copied<core::slice::Iter<'a, i32>>;
529        fn into_iter(self) -> Self::IntoIter {
530            self.0.iter().copied()
531        }
532    }
533
534    #[test]
535    fn container_eq_matches_on_container_when_ref_to_container_has_into_iterator_producing_owned_values()
536    -> TestResult<()> {
537        verify_that!(OwnedItemContainer(vec![1]), container_eq(OwnedItemContainer(vec![1])))
538    }
539
540    #[test]
541    fn container_eq_matches_vec_of_string_slices() -> TestResult<()> {
542        verify_that!(
543            vec!["String 1", "String 2", "String 3"],
544            container_eq(["String 1", "String 2", "String 3"])
545        )
546    }
547
548    #[test]
549    fn container_eq_matches_vec_of_string_slices_with_non_static_lifetime() -> TestResult<()> {
550        let string_1 = String::from("String 1");
551        let string_2 = String::from("String 2");
552        let string_3 = String::from("String 3");
553        verify_that!(
554            vec![string_1.as_str(), string_2.as_str(), string_3.as_str()],
555            container_eq(["String 1", "String 2", "String 3"])
556        )
557    }
558
559    #[test]
560    fn container_eq_matches_slice_of_string_slices() -> TestResult<()> {
561        let value = vec!["String 1", "String 2", "String 3"];
562        verify_that!(
563            value.as_slice(),
564            points_to(container_eq(["String 1", "String 2", "String 3"]))
565        )
566    }
567
568    #[test]
569    fn container_eq_matches_slice_of_string_slices_with_non_static_lifetime() -> TestResult<()> {
570        let string_1 = String::from("String 1");
571        let string_2 = String::from("String 2");
572        let string_3 = String::from("String 3");
573        let value = vec![string_1.as_str(), string_2.as_str(), string_3.as_str()];
574        verify_that!(
575            value.as_slice(),
576            points_to(container_eq(["String 1", "String 2", "String 3"]))
577        )
578    }
579
580    #[test]
581    fn container_eq_matches_ignoring_order_when_requested() -> TestResult<()> {
582        verify_that!(vec![1, 2, 3], container_eq(vec![3, 2, 1]).ignoring_order())
583    }
584
585    #[test]
586    fn container_eq_does_not_match_non_matching_container_when_ignoring_order() -> TestResult<()> {
587        verify_that!(vec![1, 2, 3], not(container_eq(vec![4, 2, 1]).ignoring_order()))
588    }
589
590    #[test]
591    fn container_eq_matches_slice_of_strings_while_ignoring_order() -> TestResult<()> {
592        let string_1 = String::from("String 1");
593        let string_2 = String::from("String 2");
594        let string_3 = String::from("String 3");
595        let value = vec![string_1, string_2, string_3];
596        verify_that!(
597            value.as_slice(),
598            points_to(container_eq(["String 3", "String 2", "String 1"]).ignoring_order())
599        )
600    }
601
602    #[test]
603    fn container_eq_matches_with_owned_item_conatiner_ignoring_order_when_requested()
604    -> TestResult<()> {
605        verify_that!(
606            OwnedItemContainer(vec![1, 2, 3]),
607            container_eq(OwnedItemContainer(vec![3, 2, 1])).ignoring_order()
608        )
609    }
610
611    #[test]
612    fn container_eq_does_not_match_non_matching_with_owned_item_conatiner_when_ignoring_order()
613    -> TestResult<()> {
614        verify_that!(
615            OwnedItemContainer(vec![1, 2, 3]),
616            not(container_eq(OwnedItemContainer(vec![4, 2, 1])).ignoring_order())
617        )
618    }
619}