Skip to main content

test_that/matchers/containers/
contains_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::containers::{OwnedItems, RefItems},
20};
21use alloc::boxed::Box;
22use core::{fmt::Debug, marker::PhantomData};
23
24/// Matches an iterable type whose elements contain a value matched by `inner`.
25///
26/// ```
27/// # use test_that::prelude::*;
28/// # fn should_pass() -> TestResult<()> {
29/// verify_that!(["Some value"], contains(eq("Some value")))?;  // Passes
30/// verify_that!(vec!["Some value"], contains(eq("Some value")))?;  // Passes
31/// #     Ok(())
32/// # }
33/// # fn should_fail_1() -> TestResult<()> {
34/// verify_that!([] as [String; 0], contains(eq("Some value")))?;   // Fails
35/// #     Ok(())
36/// # }
37/// # fn should_fail_2() -> TestResult<()> {
38/// verify_that!(["Some value"], contains(eq("Some other value")))?;   // Fails
39/// #     Ok(())
40/// # }
41/// # should_pass().unwrap();
42/// # should_fail_1().unwrap_err();
43/// # should_fail_2().unwrap_err();
44/// ```
45///
46/// By default, this matches a container with any number of elements matched
47/// by `inner`. Use the method [`ContainsMatcher::times`] to constrain the
48/// matched containers to a specific number of matching elements.
49///
50/// ```
51/// # use test_that::prelude::*;
52/// # fn should_pass() -> TestResult<()> {
53/// verify_that!([1, 1, 1], contains(eq(1)))?;              // Passes
54/// verify_that!([1, 1, 1], contains(eq(1)).times(eq(3)))?; // Passes
55/// #     Ok(())
56/// # }
57/// # fn should_fail() -> TestResult<()> {
58/// verify_that!([1, 1, 1], contains(eq(1)).times(eq(2)))?; // Fails: wrong count
59/// #     Ok(())
60/// # }
61/// # should_pass().unwrap();
62/// # should_fail().unwrap_err();
63/// ```
64///
65/// See [module documentation][crate::matchers::containers] for information
66/// about what types this matcher can match.
67pub fn contains<InnerMatcherT, ModeT>(
68    inner: InnerMatcherT,
69) -> ContainsMatcher<InnerMatcherT, ModeT> {
70    ContainsMatcher { inner, count: None, phantom: Default::default() }
71}
72
73/// A matcher which matches a container containing one or more elements a given
74/// inner [`Matcher`] matches.
75pub struct ContainsMatcher<InnerMatcherT, ModeT> {
76    inner: InnerMatcherT,
77    count: Option<Box<dyn Matcher<usize>>>,
78    phantom: PhantomData<ModeT>,
79}
80
81impl<InnerMatcherT, ModeT> ContainsMatcher<InnerMatcherT, ModeT> {
82    /// Configures this instance to match containers which contain a number of
83    /// matching items matched by `count`.
84    ///
85    /// For example, to assert that exactly three matching items must be
86    /// present, use:
87    ///
88    /// ```ignore
89    /// contains(...).times(eq(3))
90    /// ```
91    ///
92    /// One can also use `times(eq(0))` to test for the *absence* of an item
93    /// matching the expected value.
94    pub fn times(mut self, count: impl Matcher<usize> + 'static) -> Self {
95        self.count = Some(Box::new(count));
96        self
97    }
98}
99
100// Case 4: `&Container: IntoIterator<Item = T>` (owned items from a borrowed
101// container). Used when the container yields owned T (not &T) when iterated via
102// reference, e.g. a custom container that copies or clones its elements on
103// iteration.
104impl<T: Debug, InnerMatcherT: Matcher<T>, ContainerT: Debug + ?Sized> Matcher<ContainerT>
105    for ContainsMatcher<InnerMatcherT, OwnedItems>
106where
107    for<'a> &'a ContainerT: IntoIterator<Item = T>,
108{
109    fn matches(&self, actual: &ContainerT) -> MatcherResult {
110        if let Some(count) = &self.count {
111            count.matches(&self.count_matches_owned(actual))
112        } else {
113            for v in actual.into_iter() {
114                if self.inner.matches(&v).into() {
115                    return MatcherResult::Match;
116                }
117            }
118            MatcherResult::NoMatch
119        }
120    }
121
122    fn explain_match(&self, actual: &ContainerT) -> Description {
123        let count = self.count_matches_owned(actual);
124        match (count, &self.count) {
125            (_, Some(_)) => format!("which contains {} matching elements", count).into(),
126            (0, None) => "which does not contain a matching element".into(),
127            (_, None) => "which contains a matching element".into(),
128        }
129    }
130}
131
132// TODO(hovinen): Revisit the trait bounds to see whether this can be made more
133//  flexible. Namely, the following doesn't compile currently:
134//
135//      let matcher = contains(eq(&42));
136//      let val = 42;
137//      let _ = matcher.matches(&vec![&val]);
138//
139//  because val is dropped before matcher but the trait bound requires that
140//  the argument to matches outlive the matcher. It works fine if one defines
141//  val before matcher.
142impl<T: Debug, InnerMatcherT: Matcher<T>, ContainerT: Debug + ?Sized> Matcher<ContainerT>
143    for ContainsMatcher<InnerMatcherT, RefItems>
144where
145    for<'a> &'a ContainerT: IntoIterator<Item = &'a T>,
146{
147    fn matches(&self, actual: &ContainerT) -> MatcherResult {
148        if let Some(count) = &self.count {
149            count.matches(&self.count_matches_ref(actual))
150        } else {
151            for v in actual.into_iter() {
152                if self.inner.matches(v).into() {
153                    return MatcherResult::Match;
154                }
155            }
156            MatcherResult::NoMatch
157        }
158    }
159
160    fn explain_match(&self, actual: &ContainerT) -> Description {
161        let count = self.count_matches_ref(actual);
162        match (count, &self.count) {
163            (_, Some(_)) => format!("which contains {} matching elements", count).into(),
164            (0, None) => "which does not contain a matching element".into(),
165            (_, None) => "which contains a matching element".into(),
166        }
167    }
168}
169
170impl<InnerMatcherT: Describable, ModeT> Describable for ContainsMatcher<InnerMatcherT, ModeT> {
171    fn describe(&self, matcher_result: MatcherResult) -> Description {
172        match (matcher_result, &self.count) {
173            (MatcherResult::Match, Some(count)) => format!(
174                "contains n elements which {}\n  where n {}",
175                self.inner.describe(MatcherResult::Match),
176                count.describe(MatcherResult::Match)
177            )
178            .into(),
179            (MatcherResult::NoMatch, Some(count)) => format!(
180                "doesn't contain n elements which {}\n  where n {}",
181                self.inner.describe(MatcherResult::Match),
182                count.describe(MatcherResult::Match)
183            )
184            .into(),
185            (MatcherResult::Match, None) => format!(
186                "contains at least one element which {}",
187                self.inner.describe(MatcherResult::Match)
188            )
189            .into(),
190            (MatcherResult::NoMatch, None) => {
191                format!("contains no element which {}", self.inner.describe(MatcherResult::Match))
192                    .into()
193            }
194        }
195    }
196}
197
198impl<InnerMatcherT, ModeT> ContainsMatcher<InnerMatcherT, ModeT> {
199    fn count_matches_ref<T: Debug, ContainerT: ?Sized>(&self, actual: &ContainerT) -> usize
200    where
201        for<'b> &'b ContainerT: IntoIterator<Item = &'b T>,
202        InnerMatcherT: Matcher<T>,
203    {
204        let mut count = 0;
205        for v in actual.into_iter() {
206            if self.inner.matches(v).into() {
207                count += 1;
208            }
209        }
210        count
211    }
212
213    fn count_matches_owned<T: Debug, ContainerT: ?Sized>(&self, actual: &ContainerT) -> usize
214    where
215        for<'b> &'b ContainerT: IntoIterator<Item = T>,
216        InnerMatcherT: Matcher<T>,
217    {
218        let mut count = 0;
219        for v in actual.into_iter() {
220            if self.inner.matches(&v).into() {
221                count += 1;
222            }
223        }
224        count
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::{ContainsMatcher, contains};
231    use crate::matcher::{Describable as _, Matcher, MatcherResult};
232    use crate::matchers::containers::RefItems;
233    use crate::prelude::*;
234    use alloc::{string::String, vec::Vec};
235
236    #[test]
237    fn contains_matches_singleton_slice_with_value() -> TestResult<()> {
238        let matcher = contains(eq(1));
239
240        let result = matcher.matches(&vec![1]);
241
242        verify_that!(result, eq(MatcherResult::Match))
243    }
244
245    #[test]
246    fn contains_matches_singleton_vec_with_value() -> TestResult<()> {
247        let matcher = contains(eq(1));
248
249        let result = matcher.matches(&vec![1]);
250
251        verify_that!(result, eq(MatcherResult::Match))
252    }
253
254    #[test]
255    fn contains_matches_two_element_slice_with_value() -> TestResult<()> {
256        let matcher = contains(eq(1));
257
258        let result = matcher.matches(&[0, 1]);
259
260        verify_that!(result, eq(MatcherResult::Match))
261    }
262
263    #[test]
264    fn contains_does_not_match_singleton_slice_with_wrong_value() -> TestResult<()> {
265        let matcher = contains(eq(1));
266
267        let result = matcher.matches(&[0]);
268
269        verify_that!(result, eq(MatcherResult::NoMatch))
270    }
271
272    #[test]
273    fn contains_does_not_match_empty_slice() -> TestResult<()> {
274        let value = Vec::<i32>::new();
275        verify_that!(value.as_slice(), not(points_to(contains(eq(1)))))
276    }
277
278    #[test]
279    fn contains_matches_slice_with_repeated_value() -> TestResult<()> {
280        let matcher = contains(eq(1)).times(eq(2));
281
282        let result = matcher.matches(&[1, 1]);
283
284        verify_that!(result, eq(MatcherResult::Match))
285    }
286
287    #[test]
288    fn contains_does_not_match_slice_with_too_few_of_value() -> TestResult<()> {
289        let matcher = contains(eq(1)).times(eq(2));
290
291        let result = matcher.matches(&[0, 1]);
292
293        verify_that!(result, eq(MatcherResult::NoMatch))
294    }
295
296    #[test]
297    fn contains_does_not_match_slice_with_too_many_of_value() -> TestResult<()> {
298        let matcher = contains(eq(1)).times(eq(1));
299
300        let result = matcher.matches(&[1, 1]);
301
302        verify_that!(result, eq(MatcherResult::NoMatch))
303    }
304
305    #[test]
306    fn contains_matches_on_vec_of_values() -> TestResult<()> {
307        verify_that!(vec![1, 2, 3], contains(eq(1)))
308    }
309
310    #[test]
311    fn contains_matches_on_array_of_values() -> TestResult<()> {
312        verify_that!([1, 2, 3], contains(eq(1)))
313    }
314
315    #[test]
316    fn contains_matches_on_slice_of_values_with_points_to_slice() -> TestResult<()> {
317        verify_that!(&[1, 2, 3], points_to(contains(eq(1))))
318    }
319
320    #[test]
321    fn contains_matches_on_slice_of_values_with_deref_notation_on_macro() -> TestResult<()> {
322        let slice = &[1, 2, 3];
323        verify_that!(*slice, contains(eq(1)))
324    }
325
326    #[derive(Debug)]
327    struct OwnedItemContainer(Vec<i32>);
328
329    impl<'a> IntoIterator for &'a OwnedItemContainer {
330        type Item = i32;
331        type IntoIter = core::iter::Copied<core::slice::Iter<'a, i32>>;
332        fn into_iter(self) -> Self::IntoIter {
333            self.0.iter().copied()
334        }
335    }
336
337    #[test]
338    fn contains_matches_on_container_when_ref_to_container_has_into_iterator_producing_owned_values()
339    -> TestResult<()> {
340        verify_that!(OwnedItemContainer(vec![1, 2, 3]), contains(eq(1)))
341    }
342
343    #[test]
344    fn contains_matches_vec_of_string_slices() -> TestResult<()> {
345        verify_that!(vec!["String 1", "String 2", "String 3"], contains(contains_substring("1")))
346    }
347
348    #[test]
349    fn contains_matches_vec_of_string_slices_with_non_static_lifetime() -> TestResult<()> {
350        let string_1 = String::from("String 1");
351        let string_2 = String::from("String 2");
352        let string_3 = String::from("String 3");
353        verify_that!(
354            vec![string_1.as_str(), string_2.as_str(), string_3.as_str()],
355            contains(contains_substring("1"))
356        )
357    }
358
359    #[test]
360    fn contains_matches_slice_of_string_slices() -> TestResult<()> {
361        let value = vec!["String 1", "String 2", "String 3"];
362        verify_that!(value.as_slice(), points_to(contains(contains_substring("1"))))
363    }
364
365    #[test]
366    fn contains_matches_slice_of_string_slices_with_non_static_lifetime() -> TestResult<()> {
367        let string_1 = String::from("String 1");
368        let string_2 = String::from("String 2");
369        let string_3 = String::from("String 3");
370        let value = vec![string_1.as_str(), string_2.as_str(), string_3.as_str()];
371        verify_that!(value.as_slice(), points_to(contains(contains_substring("1"))))
372    }
373
374    #[test]
375    fn contains_formats_without_multiplicity_by_default() -> TestResult<()> {
376        let matcher: ContainsMatcher<_, RefItems> = contains(eq(1));
377
378        verify_that!(
379            matcher.describe(MatcherResult::Match),
380            displays_as(eq("contains at least one element which is equal to 1"))
381        )
382    }
383
384    #[test]
385    fn contains_formats_with_multiplicity_when_specified() -> TestResult<()> {
386        let matcher: ContainsMatcher<_, RefItems> = contains(eq(1)).times(eq(2));
387
388        verify_that!(
389            matcher.describe(MatcherResult::Match),
390            displays_as(eq("contains n elements which is equal to 1\n  where n is equal to 2"))
391        )
392    }
393
394    #[test]
395    fn contains_mismatch_shows_number_of_times_element_was_found() -> TestResult<()> {
396        verify_that!(
397            contains(eq(3)).times(eq(1)).explain_match(&vec![1, 2, 3, 3]),
398            displays_as(eq("which contains 2 matching elements"))
399        )
400    }
401
402    #[test]
403    fn contains_mismatch_shows_when_matches() -> TestResult<()> {
404        verify_that!(
405            contains(eq(3)).explain_match(&vec![1, 2, 3, 3]),
406            displays_as(eq("which contains a matching element"))
407        )
408    }
409
410    #[test]
411    fn contains_mismatch_shows_when_no_matches() -> TestResult<()> {
412        verify_that!(
413            contains(eq(3)).explain_match(&vec![1, 2]),
414            displays_as(eq("which does not contain a matching element"))
415        )
416    }
417}