1use crate::{
17 description::Description,
18 matcher::{Describable, Matcher, MatcherResult},
19 matcher_support::{
20 edit_distance,
21 summarize_diff::{create_diff, create_diff_reversed},
22 },
23 matchers::{
24 eq_deref_of_matcher::__internal::EqDerefOfMatcher, eq_matcher::__internal::EqMatcher,
25 },
26};
27use alloc::{borrow::Cow, boxed::Box, vec::Vec};
28use core::{fmt::Debug, ops::Deref};
29
30pub fn contains_substring<T>(expected: T) -> StrMatcher<T> {
64 StrMatcher {
65 configuration: Configuration { mode: MatchMode::Contains, ..Default::default() },
66 expected,
67 }
68}
69
70pub fn starts_with<T>(expected: T) -> StrMatcher<T> {
103 StrMatcher {
104 configuration: Configuration { mode: MatchMode::StartsWith, ..Default::default() },
105 expected,
106 }
107}
108
109pub fn ends_with<T>(expected: T) -> StrMatcher<T> {
142 StrMatcher {
143 configuration: Configuration { mode: MatchMode::EndsWith, ..Default::default() },
144 expected,
145 }
146}
147
148pub trait StrMatcherConfigurator<ExpectedT> {
160 fn ignoring_leading_whitespace(self) -> StrMatcher<ExpectedT>;
179
180 fn ignoring_trailing_whitespace(self) -> StrMatcher<ExpectedT>;
199
200 fn ignoring_outer_whitespace(self) -> StrMatcher<ExpectedT>;
223
224 fn ignoring_ascii_case(self) -> StrMatcher<ExpectedT>;
245
246 fn ignoring_unicode_case(self) -> StrMatcher<ExpectedT>;
268
269 fn times(self, times: impl Matcher<usize> + 'static) -> StrMatcher<ExpectedT>;
303}
304
305pub struct StrMatcher<ExpectedT> {
318 expected: ExpectedT,
319 configuration: Configuration,
320}
321
322impl<ExpectedT, ActualT> Matcher<ActualT> for StrMatcher<ExpectedT>
323where
324 ExpectedT: Deref<Target = str> + Debug,
325 ActualT: AsRef<str> + Debug + ?Sized,
326{
327 fn matches(&self, actual: &ActualT) -> MatcherResult {
328 self.configuration.do_strings_match(self.expected.deref(), actual.as_ref()).into()
329 }
330
331 fn explain_match(&self, actual: &ActualT) -> Description {
332 self.configuration.explain_match(self.expected.deref(), actual.as_ref())
333 }
334}
335
336impl<ExpectedT: Deref<Target = str>> Describable for StrMatcher<ExpectedT> {
337 fn describe(&self, matcher_result: MatcherResult) -> Description {
338 self.configuration.describe(matcher_result, self.expected.deref())
339 }
340}
341
342impl<ExpectedT, MatcherT: Into<StrMatcher<ExpectedT>>> StrMatcherConfigurator<ExpectedT>
343 for MatcherT
344{
345 fn ignoring_leading_whitespace(self) -> StrMatcher<ExpectedT> {
346 let existing = self.into();
347 StrMatcher {
348 configuration: existing.configuration.ignoring_leading_whitespace(),
349 ..existing
350 }
351 }
352
353 fn ignoring_trailing_whitespace(self) -> StrMatcher<ExpectedT> {
354 let existing = self.into();
355 StrMatcher {
356 configuration: existing.configuration.ignoring_trailing_whitespace(),
357 ..existing
358 }
359 }
360
361 fn ignoring_outer_whitespace(self) -> StrMatcher<ExpectedT> {
362 let existing = self.into();
363 StrMatcher { configuration: existing.configuration.ignoring_outer_whitespace(), ..existing }
364 }
365
366 fn ignoring_ascii_case(self) -> StrMatcher<ExpectedT> {
367 let existing = self.into();
368 StrMatcher { configuration: existing.configuration.ignoring_ascii_case(), ..existing }
369 }
370
371 fn ignoring_unicode_case(self) -> StrMatcher<ExpectedT> {
372 let existing = self.into();
373 StrMatcher { configuration: existing.configuration.ignoring_unicode_case(), ..existing }
374 }
375
376 fn times(self, times: impl Matcher<usize> + 'static) -> StrMatcher<ExpectedT> {
377 let existing = self.into();
378 if !matches!(existing.configuration.mode, MatchMode::Contains) {
379 panic!("The times() configurator is only meaningful with contains_substring().");
380 }
381 StrMatcher { configuration: existing.configuration.times(times), ..existing }
382 }
383}
384
385impl<T: Deref<Target = str>> From<EqMatcher<T>> for StrMatcher<T> {
386 fn from(value: EqMatcher<T>) -> Self {
387 Self::with_default_config(value.expected)
388 }
389}
390
391impl<T: Deref<Target = str>> From<EqDerefOfMatcher<T>> for StrMatcher<T> {
392 fn from(value: EqDerefOfMatcher<T>) -> Self {
393 Self::with_default_config(value.expected)
394 }
395}
396
397impl<T> StrMatcher<T> {
398 fn with_default_config(expected: T) -> Self {
403 Self { expected, configuration: Default::default() }
404 }
405}
406
407struct Configuration {
414 mode: MatchMode,
415 ignore_leading_whitespace: bool,
416 ignore_trailing_whitespace: bool,
417 case_policy: CasePolicy,
418 times: Option<Box<dyn Matcher<usize>>>,
419}
420
421#[derive(Clone)]
422enum MatchMode {
423 Equals,
424 Contains,
425 StartsWith,
426 EndsWith,
427}
428
429impl MatchMode {
430 fn to_diff_mode(&self) -> edit_distance::Mode {
431 match self {
432 MatchMode::StartsWith | MatchMode::EndsWith => edit_distance::Mode::Prefix,
433 MatchMode::Contains => edit_distance::Mode::Contains,
434 MatchMode::Equals => edit_distance::Mode::Exact,
435 }
436 }
437}
438
439#[derive(Clone)]
440enum CasePolicy {
441 Respect,
442 IgnoreAscii,
443 IgnoreUnicode,
444}
445
446impl Configuration {
447 fn do_strings_match(&self, expected: &str, actual: &str) -> bool {
450 let (expected, actual) =
451 match (self.ignore_leading_whitespace, self.ignore_trailing_whitespace) {
452 (true, true) => (expected.trim(), actual.trim()),
453 (true, false) => (expected.trim_start(), actual.trim_start()),
454 (false, true) => (expected.trim_end(), actual.trim_end()),
455 (false, false) => (expected, actual),
456 };
457 match self.mode {
458 MatchMode::Equals => match self.case_policy {
459 CasePolicy::Respect => expected == actual,
460 CasePolicy::IgnoreAscii => expected.eq_ignore_ascii_case(actual),
461 CasePolicy::IgnoreUnicode => expected.to_lowercase() == actual.to_lowercase(),
462 },
463 MatchMode::Contains => match self.case_policy {
464 CasePolicy::Respect => self.does_containment_match(actual, expected),
465 CasePolicy::IgnoreAscii => self.does_containment_match(
466 actual.to_ascii_lowercase().as_str(),
467 expected.to_ascii_lowercase().as_str(),
468 ),
469 CasePolicy::IgnoreUnicode => self.does_containment_match(
470 actual.to_lowercase().as_str(),
471 expected.to_lowercase().as_str(),
472 ),
473 },
474 MatchMode::StartsWith => match self.case_policy {
475 CasePolicy::Respect => actual.starts_with(expected),
476 CasePolicy::IgnoreAscii => {
477 actual.len() >= expected.len()
478 && actual[..expected.len()].eq_ignore_ascii_case(expected)
479 }
480 CasePolicy::IgnoreUnicode => {
481 actual.len() >= expected.len()
482 && actual.is_char_boundary(expected.len())
483 && actual[..expected.len()].to_lowercase() == expected.to_lowercase()
484 }
485 },
486 MatchMode::EndsWith => match self.case_policy {
487 CasePolicy::Respect => actual.ends_with(expected),
488 CasePolicy::IgnoreAscii => {
489 actual.len() >= expected.len()
490 && actual[actual.len() - expected.len()..].eq_ignore_ascii_case(expected)
491 }
492 CasePolicy::IgnoreUnicode => {
493 actual.len() >= expected.len()
494 && actual.is_char_boundary(actual.len() - expected.len())
495 && actual[actual.len() - expected.len()..].to_lowercase()
496 == expected.to_lowercase()
497 }
498 },
499 }
500 }
501
502 fn does_containment_match(&self, actual: &str, expected: &str) -> bool {
505 if let Some(times) = self.times.as_ref() {
506 matches!(times.matches(&(actual.split(expected).count() - 1)), MatcherResult::Match)
510 } else {
511 actual.contains(expected)
512 }
513 }
514
515 fn describe(&self, matcher_result: MatcherResult, expected: &str) -> Description {
517 let mut addenda: Vec<Cow<'static, str>> = Vec::with_capacity(3);
518 match (self.ignore_leading_whitespace, self.ignore_trailing_whitespace) {
519 (true, true) => addenda.push("ignoring leading and trailing whitespace".into()),
520 (true, false) => addenda.push("ignoring leading whitespace".into()),
521 (false, true) => addenda.push("ignoring trailing whitespace".into()),
522 (false, false) => {}
523 }
524 match self.case_policy {
525 CasePolicy::Respect => {}
526 CasePolicy::IgnoreAscii => addenda.push("ignoring ASCII case".into()),
527 CasePolicy::IgnoreUnicode => addenda.push("ignoring Unicode case".into()),
528 }
529 if let Some(times) = self.times.as_ref() {
530 addenda.push(format!("count {}", times.describe(matcher_result)).into());
531 }
532 let extra =
533 if !addenda.is_empty() { format!(" ({})", addenda.join(", ")) } else { "".into() };
534 let match_mode_description = match self.mode {
535 MatchMode::Equals => match matcher_result {
536 MatcherResult::Match => "is equal to",
537 MatcherResult::NoMatch => "isn't equal to",
538 },
539 MatchMode::Contains => match matcher_result {
540 MatcherResult::Match => "contains a substring",
541 MatcherResult::NoMatch => "does not contain a substring",
542 },
543 MatchMode::StartsWith => match matcher_result {
544 MatcherResult::Match => "starts with prefix",
545 MatcherResult::NoMatch => "does not start with",
546 },
547 MatchMode::EndsWith => match matcher_result {
548 MatcherResult::Match => "ends with suffix",
549 MatcherResult::NoMatch => "does not end with",
550 },
551 };
552 format!("{match_mode_description} {expected:?}{extra}").into()
553 }
554
555 fn explain_match(&self, expected: &str, actual: &str) -> Description {
556 let default_explanation = format!(
557 "which {}",
558 self.describe(self.do_strings_match(expected, actual).into(), expected)
559 )
560 .into();
561 if !expected.contains('\n') || !actual.contains('\n') {
562 return default_explanation;
563 }
564
565 if self.ignore_leading_whitespace {
566 return default_explanation;
568 }
569
570 if self.ignore_trailing_whitespace {
571 return default_explanation;
573 }
574
575 if self.times.is_some() {
576 return default_explanation;
578 }
579 if matches!(self.case_policy, CasePolicy::IgnoreAscii) {
580 return default_explanation;
582 }
583 if self.do_strings_match(expected, actual) {
584 return default_explanation;
588 }
589
590 let diff = match self.mode {
591 MatchMode::Equals | MatchMode::StartsWith | MatchMode::Contains => {
592 create_diff(actual, expected, self.mode.to_diff_mode())
596 }
597 MatchMode::EndsWith => create_diff_reversed(actual, expected, self.mode.to_diff_mode()),
598 };
599
600 format!("{default_explanation}\n{diff}").into()
601 }
602
603 fn ignoring_leading_whitespace(self) -> Self {
604 Self { ignore_leading_whitespace: true, ..self }
605 }
606
607 fn ignoring_trailing_whitespace(self) -> Self {
608 Self { ignore_trailing_whitespace: true, ..self }
609 }
610
611 fn ignoring_outer_whitespace(self) -> Self {
612 Self { ignore_leading_whitespace: true, ignore_trailing_whitespace: true, ..self }
613 }
614
615 fn ignoring_ascii_case(self) -> Self {
616 Self { case_policy: CasePolicy::IgnoreAscii, ..self }
617 }
618
619 fn ignoring_unicode_case(self) -> Self {
620 Self { case_policy: CasePolicy::IgnoreUnicode, ..self }
621 }
622
623 fn times(self, times: impl Matcher<usize> + 'static) -> Self {
624 Self { times: Some(Box::new(times)), ..self }
625 }
626}
627
628impl Default for Configuration {
629 fn default() -> Self {
630 Self {
631 mode: MatchMode::Equals,
632 ignore_leading_whitespace: false,
633 ignore_trailing_whitespace: false,
634 case_policy: CasePolicy::Respect,
635 times: None,
636 }
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::{StrMatcher, StrMatcherConfigurator, contains_substring, ends_with, starts_with};
643 use crate::matcher::{Describable as _, MatcherResult};
644 use crate::prelude::*;
645 use alloc::string::ToString;
646 use indoc::indoc;
647
648 #[test]
649 fn matches_string_reference_with_equal_string_reference() -> TestResult<()> {
650 let matcher = StrMatcher::with_default_config("A string");
651 verify_that!("A string", matcher)
652 }
653
654 #[test]
655 fn does_not_match_string_reference_with_non_equal_string_reference() -> TestResult<()> {
656 let matcher = StrMatcher::with_default_config("Another string");
657 verify_that!("A string", not(matcher))
658 }
659
660 #[test]
661 fn matches_owned_string_with_string_reference() -> TestResult<()> {
662 let matcher = StrMatcher::with_default_config("A string");
663 let value = "A string".to_string();
664 verify_that!(value, matcher)
665 }
666
667 #[test]
668 fn matches_owned_string_reference_with_string_reference() -> TestResult<()> {
669 let matcher = StrMatcher::with_default_config("A string");
670 let value = "A string".to_string();
671 verify_that!(&value, matcher)
672 }
673
674 #[test]
675 fn ignores_leading_whitespace_in_expected_when_requested() -> TestResult<()> {
676 let matcher = StrMatcher::with_default_config(" \n\tA string");
677 verify_that!("A string", matcher.ignoring_leading_whitespace())
678 }
679
680 #[test]
681 fn ignores_leading_whitespace_in_actual_when_requested() -> TestResult<()> {
682 let matcher = StrMatcher::with_default_config("A string");
683 verify_that!(" \n\tA string", matcher.ignoring_leading_whitespace())
684 }
685
686 #[test]
687 fn does_not_match_unequal_remaining_string_when_ignoring_leading_whitespace() -> TestResult<()>
688 {
689 let matcher = StrMatcher::with_default_config(" \n\tAnother string");
690 verify_that!("A string", not(matcher.ignoring_leading_whitespace()))
691 }
692
693 #[test]
694 fn remains_sensitive_to_trailing_whitespace_when_ignoring_leading_whitespace() -> TestResult<()>
695 {
696 let matcher = StrMatcher::with_default_config("A string \n\t");
697 verify_that!("A string", not(matcher.ignoring_leading_whitespace()))
698 }
699
700 #[test]
701 fn ignores_trailing_whitespace_in_expected_when_requested() -> TestResult<()> {
702 let matcher = StrMatcher::with_default_config("A string \n\t");
703 verify_that!("A string", matcher.ignoring_trailing_whitespace())
704 }
705
706 #[test]
707 fn ignores_trailing_whitespace_in_actual_when_requested() -> TestResult<()> {
708 let matcher = StrMatcher::with_default_config("A string");
709 verify_that!("A string \n\t", matcher.ignoring_trailing_whitespace())
710 }
711
712 #[test]
713 fn does_not_match_unequal_remaining_string_when_ignoring_trailing_whitespace() -> TestResult<()>
714 {
715 let matcher = StrMatcher::with_default_config("Another string \n\t");
716 verify_that!("A string", not(matcher.ignoring_trailing_whitespace()))
717 }
718
719 #[test]
720 fn remains_sensitive_to_leading_whitespace_when_ignoring_trailing_whitespace() -> TestResult<()>
721 {
722 let matcher = StrMatcher::with_default_config(" \n\tA string");
723 verify_that!("A string", not(matcher.ignoring_trailing_whitespace()))
724 }
725
726 #[test]
727 fn ignores_leading_and_trailing_whitespace_in_expected_when_requested() -> TestResult<()> {
728 let matcher = StrMatcher::with_default_config(" \n\tA string \n\t");
729 verify_that!("A string", matcher.ignoring_outer_whitespace())
730 }
731
732 #[test]
733 fn ignores_leading_and_trailing_whitespace_in_actual_when_requested() -> TestResult<()> {
734 let matcher = StrMatcher::with_default_config("A string");
735 verify_that!(" \n\tA string \n\t", matcher.ignoring_outer_whitespace())
736 }
737
738 #[test]
739 fn respects_ascii_case_by_default() -> TestResult<()> {
740 let matcher = StrMatcher::with_default_config("A string");
741 verify_that!("A STRING", not(matcher))
742 }
743
744 #[test]
745 fn ignores_ascii_case_when_requested() -> TestResult<()> {
746 let matcher = StrMatcher::with_default_config("A string");
747 verify_that!("A STRING", matcher.ignoring_ascii_case())
748 }
749
750 #[test]
751 fn allows_ignoring_leading_whitespace_from_eq() -> TestResult<()> {
752 verify_that!("A string", eq(" \n\tA string").ignoring_leading_whitespace())
753 }
754
755 #[test]
756 fn allows_ignoring_trailing_whitespace_from_eq() -> TestResult<()> {
757 verify_that!("A string", eq("A string \n\t").ignoring_trailing_whitespace())
758 }
759
760 #[test]
761 fn allows_ignoring_outer_whitespace_from_eq() -> TestResult<()> {
762 verify_that!("A string", eq(" \n\tA string \n\t").ignoring_outer_whitespace())
763 }
764
765 #[test]
766 fn allows_ignoring_ascii_case_from_eq() -> TestResult<()> {
767 verify_that!("A string", eq("A STRING").ignoring_ascii_case())
768 }
769
770 #[test]
771 fn allows_ignoring_ascii_case_from_eq_deref_of_str_slice() -> TestResult<()> {
772 verify_that!("A string", eq_deref_of("A STRING").ignoring_ascii_case())
773 }
774
775 #[test]
776 fn allows_ignoring_ascii_case_from_eq_deref_of_owned_string() -> TestResult<()> {
777 verify_that!("A string", eq_deref_of("A STRING".to_string()).ignoring_ascii_case())
778 }
779
780 #[test]
781 fn matches_string_containing_expected_value_in_contains_mode() -> TestResult<()> {
782 verify_that!("Some string", contains_substring("str"))
783 }
784
785 #[test]
786 fn matches_string_containing_expected_value_in_contains_mode_while_ignoring_ascii_case()
787 -> TestResult<()> {
788 verify_that!("Some string", contains_substring("STR").ignoring_ascii_case())
789 }
790
791 #[test]
792 fn contains_substring_matches_correct_number_of_substrings() -> TestResult<()> {
793 verify_that!("Some string", contains_substring("str").times(eq(1)))
794 }
795
796 #[test]
797 fn contains_substring_does_not_match_incorrect_number_of_substrings() -> TestResult<()> {
798 verify_that!("Some string\nSome string", not(contains_substring("string").times(eq(1))))
799 }
800
801 #[test]
802 fn contains_substring_does_not_match_when_substrings_overlap() -> TestResult<()> {
803 verify_that!("ababab", not(contains_substring("abab").times(eq(2))))
804 }
805
806 #[test]
807 fn starts_with_matches_string_reference_with_prefix() -> TestResult<()> {
808 verify_that!("Some value", starts_with("Some"))
809 }
810
811 #[test]
812 fn starts_with_matches_string_reference_with_prefix_ignoring_ascii_case() -> TestResult<()> {
813 verify_that!("Some value", starts_with("SOME").ignoring_ascii_case())
814 }
815
816 #[test]
817 fn starts_with_does_not_match_wrong_prefix_ignoring_ascii_case() -> TestResult<()> {
818 verify_that!("Some value", not(starts_with("OTHER").ignoring_ascii_case()))
819 }
820
821 #[test]
822 fn ends_with_does_not_match_short_string_ignoring_ascii_case() -> TestResult<()> {
823 verify_that!("Some", not(starts_with("OTHER").ignoring_ascii_case()))
824 }
825
826 #[test]
827 fn starts_with_does_not_match_string_without_prefix() -> TestResult<()> {
828 verify_that!("Some value", not(starts_with("Another")))
829 }
830
831 #[test]
832 fn starts_with_does_not_match_string_with_substring_not_at_beginning() -> TestResult<()> {
833 verify_that!("Some value", not(starts_with("value")))
834 }
835
836 #[test]
837 fn ends_with_matches_string_reference_with_suffix() -> TestResult<()> {
838 verify_that!("Some value", ends_with("value"))
839 }
840
841 #[test]
842 fn ends_with_matches_string_reference_with_suffix_ignoring_ascii_case() -> TestResult<()> {
843 verify_that!("Some value", ends_with("VALUE").ignoring_ascii_case())
844 }
845
846 #[test]
847 fn ends_with_does_not_match_wrong_suffix_ignoring_ascii_case() -> TestResult<()> {
848 verify_that!("Some value", not(ends_with("OTHER").ignoring_ascii_case()))
849 }
850
851 #[test]
852 fn ends_with_does_not_match_too_short_string_ignoring_ascii_case() -> TestResult<()> {
853 verify_that!("Some", not(ends_with("OTHER").ignoring_ascii_case()))
854 }
855
856 #[test]
857 fn ends_with_does_not_match_string_without_suffix() -> TestResult<()> {
858 verify_that!("Some value", not(ends_with("other value")))
859 }
860
861 #[test]
862 fn ends_with_does_not_match_string_with_substring_not_at_end() -> TestResult<()> {
863 verify_that!("Some value", not(ends_with("Some")))
864 }
865
866 #[test]
867 fn describes_itself_for_matching_result() -> TestResult<()> {
868 let matcher = StrMatcher::with_default_config("A string");
869 verify_that!(
870 matcher.describe(MatcherResult::Match),
871 displays_as(eq("is equal to \"A string\""))
872 )
873 }
874
875 #[test]
876 fn describes_itself_for_non_matching_result() -> TestResult<()> {
877 let matcher = StrMatcher::with_default_config("A string");
878 verify_that!(
879 matcher.describe(MatcherResult::NoMatch),
880 displays_as(eq("isn't equal to \"A string\""))
881 )
882 }
883
884 #[test]
885 fn describes_itself_for_matching_result_ignoring_leading_whitespace() -> TestResult<()> {
886 let matcher = StrMatcher::with_default_config("A string").ignoring_leading_whitespace();
887 verify_that!(
888 matcher.describe(MatcherResult::Match),
889 displays_as(eq("is equal to \"A string\" (ignoring leading whitespace)"))
890 )
891 }
892
893 #[test]
894 fn describes_itself_for_non_matching_result_ignoring_leading_whitespace() -> TestResult<()> {
895 let matcher = StrMatcher::with_default_config("A string").ignoring_leading_whitespace();
896 verify_that!(
897 matcher.describe(MatcherResult::NoMatch),
898 displays_as(eq("isn't equal to \"A string\" (ignoring leading whitespace)"))
899 )
900 }
901
902 #[test]
903 fn describes_itself_for_matching_result_ignoring_trailing_whitespace() -> TestResult<()> {
904 let matcher = StrMatcher::with_default_config("A string").ignoring_trailing_whitespace();
905 verify_that!(
906 matcher.describe(MatcherResult::Match),
907 displays_as(eq("is equal to \"A string\" (ignoring trailing whitespace)"))
908 )
909 }
910
911 #[test]
912 fn describes_itself_for_matching_result_ignoring_leading_and_trailing_whitespace()
913 -> TestResult<()> {
914 let matcher = StrMatcher::with_default_config("A string").ignoring_outer_whitespace();
915 verify_that!(
916 matcher.describe(MatcherResult::Match),
917 displays_as(eq("is equal to \"A string\" (ignoring leading and trailing whitespace)"))
918 )
919 }
920
921 #[test]
922 fn describes_itself_for_matching_result_ignoring_ascii_case() -> TestResult<()> {
923 let matcher = StrMatcher::with_default_config("A string").ignoring_ascii_case();
924 verify_that!(
925 matcher.describe(MatcherResult::Match),
926 displays_as(eq("is equal to \"A string\" (ignoring ASCII case)"))
927 )
928 }
929
930 #[test]
931 fn describes_itself_for_matching_result_ignoring_ascii_case_and_leading_whitespace()
932 -> TestResult<()> {
933 let matcher = StrMatcher::with_default_config("A string")
934 .ignoring_leading_whitespace()
935 .ignoring_ascii_case();
936 verify_that!(
937 matcher.describe(MatcherResult::Match),
938 displays_as(eq(
939 "is equal to \"A string\" (ignoring leading whitespace, ignoring ASCII case)"
940 ))
941 )
942 }
943
944 #[test]
945 fn describes_itself_for_matching_result_in_contains_mode() -> TestResult<()> {
946 let matcher = contains_substring("A string");
947 verify_that!(
948 matcher.describe(MatcherResult::Match),
949 displays_as(eq("contains a substring \"A string\""))
950 )
951 }
952
953 #[test]
954 fn describes_itself_for_non_matching_result_in_contains_mode() -> TestResult<()> {
955 let matcher = contains_substring("A string");
956 verify_that!(
957 matcher.describe(MatcherResult::NoMatch),
958 displays_as(eq("does not contain a substring \"A string\""))
959 )
960 }
961
962 #[test]
963 fn describes_itself_with_count_number() -> TestResult<()> {
964 let matcher = contains_substring("A string").times(gt(2));
965 verify_that!(
966 matcher.describe(MatcherResult::Match),
967 displays_as(eq("contains a substring \"A string\" (count is greater than 2)"))
968 )
969 }
970
971 #[test]
972 fn describes_itself_for_matching_result_in_starts_with_mode() -> TestResult<()> {
973 let matcher = starts_with("A string");
974 verify_that!(
975 matcher.describe(MatcherResult::Match),
976 displays_as(eq("starts with prefix \"A string\""))
977 )
978 }
979
980 #[test]
981 fn describes_itself_for_non_matching_result_in_starts_with_mode() -> TestResult<()> {
982 let matcher = starts_with("A string");
983 verify_that!(
984 matcher.describe(MatcherResult::NoMatch),
985 displays_as(eq("does not start with \"A string\""))
986 )
987 }
988
989 #[test]
990 fn describes_itself_for_matching_result_in_ends_with_mode() -> TestResult<()> {
991 let matcher = ends_with("A string");
992 verify_that!(
993 matcher.describe(MatcherResult::Match),
994 displays_as(eq("ends with suffix \"A string\""))
995 )
996 }
997
998 #[test]
999 fn describes_itself_for_non_matching_result_in_ends_with_mode() -> TestResult<()> {
1000 let matcher = ends_with("A string");
1001 verify_that!(
1002 matcher.describe(MatcherResult::NoMatch),
1003 displays_as(eq("does not end with \"A string\""))
1004 )
1005 }
1006
1007 #[test]
1008 fn match_explanation_contains_diff_of_strings_if_more_than_one_line() -> TestResult<()> {
1009 let result = verify_that!(
1010 indoc!(
1011 "
1012 First line
1013 Second line
1014 Third line
1015 "
1016 ),
1017 starts_with(indoc!(
1018 "
1019 First line
1020 Second lines
1021 Third line
1022 "
1023 ))
1024 );
1025
1026 verify_that!(
1027 result,
1028 err(displays_as(contains_substring(
1029 "\
1030 First line
1031 -Second line
1032 +Second lines
1033 Third line"
1034 )))
1035 )
1036 }
1037
1038 #[test]
1039 fn match_explanation_for_starts_with_ignores_trailing_lines_in_actual_string() -> TestResult<()>
1040 {
1041 let result = verify_that!(
1042 indoc!(
1043 "
1044 First line
1045 Second line
1046 Third line
1047 Fourth line
1048 "
1049 ),
1050 starts_with(indoc!(
1051 "
1052 First line
1053 Second lines
1054 Third line
1055 "
1056 ))
1057 );
1058
1059 verify_that!(
1060 result,
1061 err(displays_as(contains_substring(
1062 "
1063 First line
1064 -Second line
1065 +Second lines
1066 Third line
1067 <---- remaining lines omitted ---->"
1068 )))
1069 )
1070 }
1071
1072 #[test]
1073 fn match_explanation_for_starts_with_includes_both_versions_of_differing_last_line()
1074 -> TestResult<()> {
1075 let result = verify_that!(
1076 indoc!(
1077 "
1078 First line
1079 Second line
1080 Third line
1081 "
1082 ),
1083 starts_with(indoc!(
1084 "
1085 First line
1086 Second lines
1087 "
1088 ))
1089 );
1090
1091 verify_that!(
1092 result,
1093 err(displays_as(contains_substring(
1094 "\
1095 First line
1096 -Second line
1097 +Second lines
1098 <---- remaining lines omitted ---->"
1099 )))
1100 )
1101 }
1102
1103 #[test]
1104 fn match_explanation_for_ends_with_ignores_leading_lines_in_actual_string() -> TestResult<()> {
1105 let result = verify_that!(
1106 indoc!(
1107 "
1108 First line
1109 Second line
1110 Third line
1111 Fourth line
1112 "
1113 ),
1114 ends_with(indoc!(
1115 "
1116 Second line
1117 Third lines
1118 Fourth line
1119 "
1120 ))
1121 );
1122
1123 verify_that!(
1124 result,
1125 err(displays_as(contains_substring(
1126 "
1127 Difference(-actual / +expected):
1128 <---- remaining lines omitted ---->
1129 Second line
1130 -Third line
1131 +Third lines
1132 Fourth line"
1133 )))
1134 )
1135 }
1136
1137 #[test]
1138 fn match_explanation_for_contains_substring_ignores_outer_lines_in_actual_string()
1139 -> TestResult<()> {
1140 let result = verify_that!(
1141 indoc!(
1142 "
1143 First line
1144 Second line
1145 Third line
1146 Fourth line
1147 Fifth line
1148 "
1149 ),
1150 contains_substring(indoc!(
1151 "
1152 Second line
1153 Third lines
1154 Fourth line
1155 "
1156 ))
1157 );
1158
1159 verify_that!(
1160 result,
1161 err(displays_as(contains_substring(
1162 "
1163 Difference(-actual / +expected):
1164 <---- remaining lines omitted ---->
1165 Second line
1166 -Third line
1167 +Third lines
1168 Fourth line
1169 <---- remaining lines omitted ---->"
1170 )))
1171 )
1172 }
1173
1174 #[test]
1175 fn match_explanation_for_contains_substring_shows_diff_when_first_and_last_line_are_incomplete()
1176 -> TestResult<()> {
1177 let result = verify_that!(
1178 indoc!(
1179 "
1180 First line
1181 Second line
1182 Third line
1183 Fourth line
1184 Fifth line
1185 "
1186 ),
1187 contains_substring(indoc!(
1188 "
1189 line
1190 Third line
1191 Foorth line
1192 Fifth"
1193 ))
1194 );
1195
1196 verify_that!(
1197 result,
1198 err(displays_as(contains_substring(
1199 "
1200 Difference(-actual / +expected):
1201 <---- remaining lines omitted ---->
1202 -Second line
1203 +line
1204 Third line
1205 -Fourth line
1206 +Foorth line
1207 -Fifth line
1208 +Fifth
1209 <---- remaining lines omitted ---->"
1210 )))
1211 )
1212 }
1213
1214 #[test]
1215 fn match_explanation_for_eq_does_not_ignore_trailing_lines_in_actual_string() -> TestResult<()>
1216 {
1217 let result = verify_that!(
1218 indoc!(
1219 "
1220 First line
1221 Second line
1222 Third line
1223 Fourth line
1224 "
1225 ),
1226 eq(indoc!(
1227 "
1228 First line
1229 Second lines
1230 Third line
1231 "
1232 ))
1233 );
1234
1235 verify_that!(
1236 result,
1237 err(displays_as(contains_substring(
1238 "\
1239 First line
1240 -Second line
1241 +Second lines
1242 Third line
1243 -Fourth line"
1244 )))
1245 )
1246 }
1247
1248 #[test]
1249 fn match_explanation_does_not_show_diff_if_actual_value_is_single_line() -> TestResult<()> {
1250 let result = verify_that!(
1251 "First line",
1252 starts_with(indoc!(
1253 "
1254 Second line
1255 Third line
1256 "
1257 ))
1258 );
1259
1260 verify_that!(
1261 result,
1262 err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
1263 )
1264 }
1265
1266 #[test]
1267 fn match_explanation_does_not_show_diff_if_expected_value_is_single_line() -> TestResult<()> {
1268 let result = verify_that!(
1269 indoc!(
1270 "
1271 First line
1272 Second line
1273 Third line
1274 "
1275 ),
1276 starts_with("Second line")
1277 );
1278
1279 verify_that!(
1280 result,
1281 err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
1282 )
1283 }
1284
1285 #[test]
1286 fn eq_ignoring_unicode_case_matches_string_with_non_unicode_different_case() -> TestResult<()> {
1287 verify_that!("Κάποια τιμή", eq("ΚΆΠΟΙΑ ΤΙΜΉ").ignoring_unicode_case())
1288 }
1289
1290 #[test]
1291 fn eq_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1292 verify_that!("Κάποια τιμή", not(eq("Some Value").ignoring_unicode_case()))
1293 }
1294
1295 #[test]
1296 fn starts_with_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1297 -> TestResult<()> {
1298 verify_that!("Κάποια τιμή", starts_with("ΚΆΠΟΙΑ").ignoring_unicode_case())
1299 }
1300
1301 #[test]
1302 fn starts_with_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1303 verify_that!("Κάποια τιμή", not(starts_with("Some Value").ignoring_unicode_case()))
1304 }
1305
1306 #[test]
1307 fn ends_with_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1308 -> TestResult<()> {
1309 verify_that!("Κάποια τιμή", ends_with("ΤΙΜΉ").ignoring_unicode_case())
1310 }
1311
1312 #[test]
1313 fn ends_with_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1314 verify_that!("Κάποια τιμή", not(ends_with("Some Value").ignoring_unicode_case()))
1315 }
1316
1317 #[test]
1318 fn contains_substring_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1319 -> TestResult<()> {
1320 verify_that!("Κάποια τιμή", contains_substring("ΠΟΙΑ ").ignoring_unicode_case())
1321 }
1322
1323 #[test]
1324 fn contains_substring_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()>
1325 {
1326 verify_that!("Κάποια τιμή", not(contains_substring("me Val").ignoring_unicode_case()))
1327 }
1328}