1use crate::{
16 description::Description,
17 matcher::{Matcher, MatcherBase, MatcherResult},
18};
19use std::fmt::Debug;
20
21pub fn subset_of<ExpectedT>(superset: ExpectedT) -> SubsetOfMatcher<ExpectedT> {
75 SubsetOfMatcher { superset }
76}
77
78#[derive(MatcherBase)]
79pub struct SubsetOfMatcher<ExpectedT> {
80 superset: ExpectedT,
81}
82
83impl<ElementT: Debug + PartialEq + Copy, ActualT: Debug + Copy, ExpectedT: Debug> Matcher<ActualT>
84 for SubsetOfMatcher<ExpectedT>
85where
86 ActualT: IntoIterator<Item = ElementT>,
87 for<'a> &'a ExpectedT: IntoIterator<Item = &'a ElementT>,
88{
89 fn matches(&self, actual: ActualT) -> MatcherResult {
90 for actual_item in actual {
91 if self.expected_is_missing(&actual_item) {
92 return MatcherResult::NoMatch;
93 }
94 }
95 MatcherResult::Match
96 }
97
98 fn explain_match(&self, actual: ActualT) -> Description {
99 let unexpected_elements = actual
100 .into_iter()
101 .enumerate()
102 .filter(|item| self.expected_is_missing(&item.1))
103 .map(|(idx, actual_item)| format!("{actual_item:#?} at #{idx}"))
104 .collect::<Vec<_>>();
105
106 match unexpected_elements.len() {
107 0 => "which no element is unexpected".into(),
108 1 => format!("whose element {} is unexpected", &unexpected_elements[0]).into(),
109 _ => format!("whose elements {} are unexpected", unexpected_elements.join(", ")).into(),
110 }
111 }
112
113 fn describe(&self, matcher_result: MatcherResult) -> Description {
114 match matcher_result {
115 MatcherResult::Match => format!("is a subset of {:#?}", self.superset).into(),
116 MatcherResult::NoMatch => format!("isn't a subset of {:#?}", self.superset).into(),
117 }
118 }
119}
120
121impl<ElementT: PartialEq, ExpectedT> SubsetOfMatcher<ExpectedT>
122where
123 for<'a> &'a ExpectedT: IntoIterator<Item = &'a ElementT>,
124{
125 fn expected_is_missing(&self, needle: &ElementT) -> bool {
126 !self.superset.into_iter().any(|item| item == needle)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use crate::prelude::*;
133 use crate::Result;
134 use indoc::indoc;
135 use std::collections::HashSet;
136
137 #[test]
138 fn subset_of_matches_empty_vec() -> Result<()> {
139 let value: Vec<i32> = vec![];
140 verify_that!(value, subset_of([]))
141 }
142
143 #[test]
144 fn subset_of_matches_vec_with_one_element() -> Result<()> {
145 let value = vec![1];
146 verify_that!(value, subset_of([&1]))
147 }
148
149 #[test]
150 fn subset_of_matches_vec_with_two_elements() -> Result<()> {
151 let value = vec![1, 2];
152 verify_that!(value, subset_of([&1, &2]))
153 }
154
155 #[test]
156 fn subset_of_matches_vec_when_expected_has_excess_element() -> Result<()> {
157 let value = vec![1, 2];
158 verify_that!(value, subset_of([&1, &2, &3]))
159 }
160
161 #[test]
162 fn subset_of_matches_vec_when_expected_has_excess_element_first() -> Result<()> {
163 let value = vec![1, 2];
164 verify_that!(value, subset_of([&3, &1, &2]))
165 }
166
167 #[test]
168 fn subset_of_matches_slice_with_one_element() -> Result<()> {
169 let value = &[1];
170 verify_that!(value, subset_of([&1]))
171 }
172
173 #[test]
174 fn subset_of_matches_hash_set_with_one_element() -> Result<()> {
175 let value: HashSet<i32> = [1].into();
176 verify_that!(value, subset_of([&1]))
177 }
178
179 #[test]
180 fn subset_of_does_not_match_when_first_element_does_not_match() -> Result<()> {
181 let value = vec![0];
182 verify_that!(value, not(subset_of([&1])))
183 }
184
185 #[test]
186 fn subset_of_does_not_match_when_second_element_does_not_match() -> Result<()> {
187 let value = vec![2, 0];
188 verify_that!(value, not(subset_of([&2])))
189 }
190
191 #[test]
192 fn subset_of_shows_correct_message_when_first_item_does_not_match() -> Result<()> {
193 let result = verify_that!(vec![0, 2, 3], subset_of([&1, &2, &3]));
194
195 verify_that!(
196 result,
197 err(displays_as(contains_substring(indoc!(
198 "
199 Value of: vec![0, 2, 3]
200 Expected: is a subset of [
201 1,
202 2,
203 3,
204 ]
205 Actual: [0, 2, 3],
206 whose element 0 at #0 is unexpected
207 "
208 ))))
209 )
210 }
211
212 #[test]
213 fn subset_of_shows_correct_message_when_second_item_does_not_match() -> Result<()> {
214 let result = verify_that!(vec![1, 0, 3], subset_of([&1, &2, &3]));
215
216 verify_that!(
217 result,
218 err(displays_as(contains_substring(indoc!(
219 "
220 Value of: vec![1, 0, 3]
221 Expected: is a subset of [
222 1,
223 2,
224 3,
225 ]
226 Actual: [1, 0, 3],
227 whose element 0 at #1 is unexpected
228 "
229 ))))
230 )
231 }
232
233 #[test]
234 fn subset_of_shows_correct_message_when_first_two_items_do_not_match() -> Result<()> {
235 let result = verify_that!(vec![0, 0, 3], subset_of([&1, &2, &3]));
236
237 verify_that!(
238 result,
239 err(displays_as(contains_substring(indoc!(
240 "
241 Value of: vec![0, 0, 3]
242 Expected: is a subset of [
243 1,
244 2,
245 3,
246 ]
247 Actual: [0, 0, 3],
248 whose elements 0 at #0, 0 at #1 are unexpected
249 "
250 ))))
251 )
252 }
253}