|
14 | 14 |
|
15 | 15 | use crate::description::Description;
|
16 | 16 | use crate::matcher::{Matcher, MatcherBase, MatcherResult};
|
| 17 | +use crate::matcher_support::match_matrix::internal::{MatchMatrix, Requirements}; |
| 18 | +use crate::matchers::eq_matcher::eq; |
17 | 19 | use std::fmt::Debug;
|
18 | 20 |
|
19 | 21 | /// Matches a container equal (in the sense of `==`) to `expected`.
|
@@ -153,6 +155,102 @@ fn build_explanation<T: Debug, U: Debug>(missing: Vec<T>, unexpected: Vec<U>) ->
|
153 | 155 | }
|
154 | 156 | }
|
155 | 157 |
|
| 158 | +impl<ExpectedContainerT> ContainerEqMatcher<ExpectedContainerT> { |
| 159 | + /// Match container equality, but ignoring element order. |
| 160 | + pub fn ignore_order(self) -> IgnoringOrder<ExpectedContainerT> { |
| 161 | + IgnoringOrder { expected: self.expected } |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +#[derive(MatcherBase)] |
| 166 | +pub struct IgnoringOrder<ExpectedContainerT> { |
| 167 | + expected: ExpectedContainerT, |
| 168 | +} |
| 169 | + |
| 170 | +/// Implements a matcher that ignores the relative order of the elements. |
| 171 | +impl<ActualElementT, ActualContainerT, ExpectedElementT, ExpectedContainerT> |
| 172 | + Matcher<ActualContainerT> for IgnoringOrder<ExpectedContainerT> |
| 173 | +where |
| 174 | + ActualElementT: Debug + Copy + for<'a> PartialEq<&'a ExpectedElementT>, |
| 175 | + ActualContainerT: Debug + Copy + IntoIterator<Item = ActualElementT>, |
| 176 | + ExpectedElementT: Debug, |
| 177 | + for<'a> &'a ExpectedContainerT: IntoIterator<Item = &'a ExpectedElementT>, |
| 178 | +{ |
| 179 | + fn matches(&self, actual: ActualContainerT) -> MatcherResult { |
| 180 | + let expected: Vec<Box<dyn Matcher<ActualElementT>>> = self |
| 181 | + .expected |
| 182 | + .into_iter() |
| 183 | + .map(|x| Box::new(eq(x)) as Box<dyn Matcher<ActualElementT>>) |
| 184 | + .collect(); |
| 185 | + let match_matrix = MatchMatrix::generate(actual, &expected); |
| 186 | + // TODO: investigate why Requirements::PerfectMatch and |
| 187 | + // match_matrix.is_full_match is not doing what we expect |
| 188 | + // here. |
| 189 | + (match_matrix.is_match_for(Requirements::Subset) |
| 190 | + && match_matrix.is_match_for(Requirements::Superset)) |
| 191 | + .into() |
| 192 | + } |
| 193 | + |
| 194 | + fn explain_match(&self, actual: ActualContainerT) -> Description { |
| 195 | + // We need to materialize the collections in order to have reliable iteration |
| 196 | + // order when generating our reports. |
| 197 | + let expected_items: Vec<&ExpectedElementT> = self.expected.into_iter().collect(); |
| 198 | + let actual_items: Vec<ActualElementT> = actual.into_iter().collect(); |
| 199 | + |
| 200 | + let expected_matchers: Vec<_> = expected_items |
| 201 | + .iter() |
| 202 | + .map(|&x| Box::new(eq(x)) as Box<dyn Matcher<ActualElementT>>) |
| 203 | + .collect(); |
| 204 | + let match_matrix = MatchMatrix::generate(actual_items.iter().copied(), &expected_matchers); |
| 205 | + |
| 206 | + let best_match = match_matrix.find_best_match(); |
| 207 | + |
| 208 | + // Since we are doing equality checks, we can generate a slightly less verbose |
| 209 | + // message than BestMatch::get_explanation. |
| 210 | + let matches = best_match.get_matches().map(|(actual_idx, expected_idx)|{ |
| 211 | + Description::new().text( |
| 212 | + format!( |
| 213 | + "Actual element {:?} at index {actual_idx} is equal to expected element at index {expected_idx}.", |
| 214 | + actual_items[actual_idx], |
| 215 | + ))}); |
| 216 | + |
| 217 | + let unmatched_actual = best_match.get_unmatched_actual().map(|actual_idx| { |
| 218 | + Description::new().text( |
| 219 | + format!( |
| 220 | + "Actual element {:?} at index {actual_idx} did not match any remaining expected element.", |
| 221 | + actual_items[actual_idx], |
| 222 | + )) |
| 223 | + }); |
| 224 | + |
| 225 | + let unmatched_expected = |
| 226 | + best_match.get_unmatched_expected().into_iter().map(|expected_idx| { |
| 227 | + Description::new().text(format!( |
| 228 | + "Expected element {:?} at index {expected_idx} did not match any remaining actual element.", |
| 229 | + expected_items[expected_idx] |
| 230 | + )) |
| 231 | + }); |
| 232 | + |
| 233 | + Description::new() |
| 234 | + .text("which does not have a perfect match. The best match found was:") |
| 235 | + .collect(matches.chain(unmatched_actual).chain(unmatched_expected)) |
| 236 | + } |
| 237 | + |
| 238 | + fn describe(&self, matcher_result: MatcherResult) -> Description { |
| 239 | + Description::new() |
| 240 | + .text(format!( |
| 241 | + "{} all elements matching in any order:", |
| 242 | + if matcher_result.into() { "contains" } else { "doesn't contain" }, |
| 243 | + )) |
| 244 | + .nested( |
| 245 | + self.expected |
| 246 | + .into_iter() |
| 247 | + .map(|element| format!("{:?}", element)) |
| 248 | + .collect::<Description>() |
| 249 | + .bullet_list(), |
| 250 | + ) |
| 251 | + } |
| 252 | +} |
| 253 | + |
156 | 254 | #[cfg(test)]
|
157 | 255 | mod tests {
|
158 | 256 | use crate::matcher::MatcherResult;
|
@@ -284,4 +382,135 @@ mod tests {
|
284 | 382 | displays_as(eq("which contains the unexpected element \"C\""))
|
285 | 383 | )
|
286 | 384 | }
|
| 385 | + |
| 386 | + #[test] |
| 387 | + fn ignoring_order_match() -> Result<()> { |
| 388 | + verify_that!(vec!["a", "b"], container_eq(["b", "a"]).ignore_order()) |
| 389 | + } |
| 390 | + |
| 391 | + #[test] |
| 392 | + fn ignoring_order_mismatch() -> Result<()> { |
| 393 | + verify_that!(vec!["a", "b"], not(container_eq(["1", "2"]).ignore_order())) |
| 394 | + } |
| 395 | + |
| 396 | + #[test] |
| 397 | + fn ignoring_order_mismatch_explain() -> Result<()> { |
| 398 | + let expected_err = verify_that!(vec!["a", "b"], container_eq(["1", "2"]).ignore_order()); |
| 399 | + verify_that!( |
| 400 | + expected_err, |
| 401 | + err(displays_as(contains_substring(indoc!( |
| 402 | + r#" |
| 403 | + Value of: vec!["a", "b"] |
| 404 | + Expected: contains all elements matching in any order: |
| 405 | + * "1" |
| 406 | + * "2" |
| 407 | + Actual: ["a", "b"], |
| 408 | + which does not have a perfect match. The best match found was: |
| 409 | + Actual element "a" at index 0 did not match any remaining expected element. |
| 410 | + Actual element "b" at index 1 did not match any remaining expected element. |
| 411 | + Expected element "1" at index 0 did not match any remaining actual element. |
| 412 | + Expected element "2" at index 1 did not match any remaining actual element. |
| 413 | + "# |
| 414 | + )))) |
| 415 | + ) |
| 416 | + } |
| 417 | + |
| 418 | + #[test] |
| 419 | + fn ignoring_order_unaccounted_extra_expected() -> Result<()> { |
| 420 | + verify_that!(vec!["a", "b"], not(container_eq(["a", "b", "a"]).ignore_order())) |
| 421 | + } |
| 422 | + |
| 423 | + #[test] |
| 424 | + fn ignoring_order_unaccounted_extra_expected_explain() -> Result<()> { |
| 425 | + let expected_err = |
| 426 | + verify_that!(vec!["a", "b"], container_eq(["a", "b", "a"]).ignore_order()); |
| 427 | + verify_that!( |
| 428 | + expected_err, |
| 429 | + err(displays_as(contains_substring(indoc!( |
| 430 | + r#" |
| 431 | + Value of: vec!["a", "b"] |
| 432 | + Expected: contains all elements matching in any order: |
| 433 | + * "a" |
| 434 | + * "b" |
| 435 | + * "a" |
| 436 | + Actual: ["a", "b"], |
| 437 | + which does not have a perfect match. The best match found was: |
| 438 | + Actual element "a" at index 0 is equal to expected element at index 0. |
| 439 | + Actual element "b" at index 1 is equal to expected element at index 1. |
| 440 | + Expected element "a" at index 2 did not match any remaining actual element. |
| 441 | + "# |
| 442 | + )))) |
| 443 | + ) |
| 444 | + } |
| 445 | + |
| 446 | + #[test] |
| 447 | + fn ignoring_order_unaccounted_extra_actual() -> Result<()> { |
| 448 | + verify_that!(vec!["a", "b", "a"], not(container_eq(["b", "a"]).ignore_order())) |
| 449 | + } |
| 450 | + |
| 451 | + #[test] |
| 452 | + fn ignoring_order_unaccounted_extra_actual_explain() -> Result<()> { |
| 453 | + let expected_err = |
| 454 | + verify_that!(vec!["a", "b", "a"], container_eq(["b", "a"]).ignore_order()); |
| 455 | + |
| 456 | + verify_that!( |
| 457 | + expected_err, |
| 458 | + err(displays_as(contains_substring(indoc!( |
| 459 | + r#" |
| 460 | + Value of: vec!["a", "b", "a"] |
| 461 | + Expected: contains all elements matching in any order: |
| 462 | + * "b" |
| 463 | + * "a" |
| 464 | + Actual: ["a", "b", "a"], |
| 465 | + which does not have a perfect match. The best match found was: |
| 466 | + Actual element "a" at index 0 is equal to expected element at index 1. |
| 467 | + Actual element "b" at index 1 is equal to expected element at index 0. |
| 468 | + Actual element "a" at index 2 did not match any remaining expected element. |
| 469 | + "# |
| 470 | + )))) |
| 471 | + ) |
| 472 | + } |
| 473 | + |
| 474 | + #[test] |
| 475 | + fn ignoring_order_on_sets() -> Result<()> { |
| 476 | + let mut actual = std::collections::HashSet::new(); |
| 477 | + actual.insert("b"); |
| 478 | + actual.insert("a"); |
| 479 | + actual.insert("c"); |
| 480 | + verify_that!(actual, container_eq(["c", "b", "a"]).ignore_order()) |
| 481 | + } |
| 482 | + |
| 483 | + #[test] |
| 484 | + fn ignoring_order_on_sets_explain() -> Result<()> { |
| 485 | + let mut actual = std::collections::HashSet::new(); |
| 486 | + actual.insert("b"); |
| 487 | + actual.insert("a"); |
| 488 | + actual.insert("c"); |
| 489 | + let expected_err = verify_that!(actual, container_eq(["c", "a"]).ignore_order()); |
| 490 | + verify_that!( |
| 491 | + expected_err, |
| 492 | + err(displays_as(contains_regex(indoc!( |
| 493 | + r#" |
| 494 | + Value of: actual |
| 495 | + Expected: contains all elements matching in any order: |
| 496 | + \* "c" |
| 497 | + \* "a" |
| 498 | + Actual: \{"\w", "\w", "\w"\}, |
| 499 | + which does not have a perfect match. The best match found was: |
| 500 | + Actual element "\w" at index \d is equal to expected element at index \d\. |
| 501 | + Actual element "\w" at index \d is equal to expected element at index \d\. |
| 502 | + Actual element "\w" at index \d did not match any remaining expected element\. |
| 503 | + "# |
| 504 | + )))) |
| 505 | + ) |
| 506 | + } |
| 507 | + |
| 508 | + #[test] |
| 509 | + fn ignoring_order_on_number_sets() -> Result<()> { |
| 510 | + let mut actual = std::collections::HashSet::new(); |
| 511 | + actual.insert(1); |
| 512 | + actual.insert(2); |
| 513 | + actual.insert(3); |
| 514 | + verify_that!(actual, container_eq([3, 2, 1]).ignore_order()) |
| 515 | + } |
287 | 516 | }
|
0 commit comments