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;
569 }
570
571 if self.ignore_trailing_whitespace {
572 return default_explanation;
575 }
576
577 if self.times.is_some() {
578 return default_explanation;
580 }
581 if matches!(self.case_policy, CasePolicy::IgnoreAscii) {
582 return default_explanation;
585 }
586 if self.do_strings_match(expected, actual) {
587 return default_explanation;
591 }
592
593 let diff = match self.mode {
594 MatchMode::Equals | MatchMode::StartsWith | MatchMode::Contains => {
595 create_diff(actual, expected, self.mode.to_diff_mode())
600 }
601 MatchMode::EndsWith => create_diff_reversed(actual, expected, self.mode.to_diff_mode()),
602 };
603
604 format!("{default_explanation}\n{diff}").into()
605 }
606
607 fn ignoring_leading_whitespace(self) -> Self {
608 Self { ignore_leading_whitespace: true, ..self }
609 }
610
611 fn ignoring_trailing_whitespace(self) -> Self {
612 Self { ignore_trailing_whitespace: true, ..self }
613 }
614
615 fn ignoring_outer_whitespace(self) -> Self {
616 Self { ignore_leading_whitespace: true, ignore_trailing_whitespace: true, ..self }
617 }
618
619 fn ignoring_ascii_case(self) -> Self {
620 Self { case_policy: CasePolicy::IgnoreAscii, ..self }
621 }
622
623 fn ignoring_unicode_case(self) -> Self {
624 Self { case_policy: CasePolicy::IgnoreUnicode, ..self }
625 }
626
627 fn times(self, times: impl Matcher<usize> + 'static) -> Self {
628 Self { times: Some(Box::new(times)), ..self }
629 }
630}
631
632impl Default for Configuration {
633 fn default() -> Self {
634 Self {
635 mode: MatchMode::Equals,
636 ignore_leading_whitespace: false,
637 ignore_trailing_whitespace: false,
638 case_policy: CasePolicy::Respect,
639 times: None,
640 }
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::{StrMatcher, StrMatcherConfigurator, contains_substring, ends_with, starts_with};
647 use crate::matcher::{Describable as _, MatcherResult};
648 use crate::prelude::*;
649 use alloc::string::ToString;
650 use indoc::indoc;
651
652 #[test]
653 fn matches_string_reference_with_equal_string_reference() -> TestResult<()> {
654 let matcher = StrMatcher::with_default_config("A string");
655 verify_that!("A string", matcher)
656 }
657
658 #[test]
659 fn does_not_match_string_reference_with_non_equal_string_reference() -> TestResult<()> {
660 let matcher = StrMatcher::with_default_config("Another string");
661 verify_that!("A string", not(matcher))
662 }
663
664 #[test]
665 fn matches_owned_string_with_string_reference() -> TestResult<()> {
666 let matcher = StrMatcher::with_default_config("A string");
667 let value = "A string".to_string();
668 verify_that!(value, matcher)
669 }
670
671 #[test]
672 fn matches_owned_string_reference_with_string_reference() -> TestResult<()> {
673 let matcher = StrMatcher::with_default_config("A string");
674 let value = "A string".to_string();
675 verify_that!(&value, matcher)
676 }
677
678 #[test]
679 fn ignores_leading_whitespace_in_expected_when_requested() -> TestResult<()> {
680 let matcher = StrMatcher::with_default_config(" \n\tA string");
681 verify_that!("A string", matcher.ignoring_leading_whitespace())
682 }
683
684 #[test]
685 fn ignores_leading_whitespace_in_actual_when_requested() -> TestResult<()> {
686 let matcher = StrMatcher::with_default_config("A string");
687 verify_that!(" \n\tA string", matcher.ignoring_leading_whitespace())
688 }
689
690 #[test]
691 fn does_not_match_unequal_remaining_string_when_ignoring_leading_whitespace() -> TestResult<()>
692 {
693 let matcher = StrMatcher::with_default_config(" \n\tAnother string");
694 verify_that!("A string", not(matcher.ignoring_leading_whitespace()))
695 }
696
697 #[test]
698 fn remains_sensitive_to_trailing_whitespace_when_ignoring_leading_whitespace() -> TestResult<()>
699 {
700 let matcher = StrMatcher::with_default_config("A string \n\t");
701 verify_that!("A string", not(matcher.ignoring_leading_whitespace()))
702 }
703
704 #[test]
705 fn ignores_trailing_whitespace_in_expected_when_requested() -> TestResult<()> {
706 let matcher = StrMatcher::with_default_config("A string \n\t");
707 verify_that!("A string", matcher.ignoring_trailing_whitespace())
708 }
709
710 #[test]
711 fn ignores_trailing_whitespace_in_actual_when_requested() -> TestResult<()> {
712 let matcher = StrMatcher::with_default_config("A string");
713 verify_that!("A string \n\t", matcher.ignoring_trailing_whitespace())
714 }
715
716 #[test]
717 fn does_not_match_unequal_remaining_string_when_ignoring_trailing_whitespace() -> TestResult<()>
718 {
719 let matcher = StrMatcher::with_default_config("Another string \n\t");
720 verify_that!("A string", not(matcher.ignoring_trailing_whitespace()))
721 }
722
723 #[test]
724 fn remains_sensitive_to_leading_whitespace_when_ignoring_trailing_whitespace() -> TestResult<()>
725 {
726 let matcher = StrMatcher::with_default_config(" \n\tA string");
727 verify_that!("A string", not(matcher.ignoring_trailing_whitespace()))
728 }
729
730 #[test]
731 fn ignores_leading_and_trailing_whitespace_in_expected_when_requested() -> TestResult<()> {
732 let matcher = StrMatcher::with_default_config(" \n\tA string \n\t");
733 verify_that!("A string", matcher.ignoring_outer_whitespace())
734 }
735
736 #[test]
737 fn ignores_leading_and_trailing_whitespace_in_actual_when_requested() -> TestResult<()> {
738 let matcher = StrMatcher::with_default_config("A string");
739 verify_that!(" \n\tA string \n\t", matcher.ignoring_outer_whitespace())
740 }
741
742 #[test]
743 fn respects_ascii_case_by_default() -> TestResult<()> {
744 let matcher = StrMatcher::with_default_config("A string");
745 verify_that!("A STRING", not(matcher))
746 }
747
748 #[test]
749 fn ignores_ascii_case_when_requested() -> TestResult<()> {
750 let matcher = StrMatcher::with_default_config("A string");
751 verify_that!("A STRING", matcher.ignoring_ascii_case())
752 }
753
754 #[test]
755 fn allows_ignoring_leading_whitespace_from_eq() -> TestResult<()> {
756 verify_that!("A string", eq(" \n\tA string").ignoring_leading_whitespace())
757 }
758
759 #[test]
760 fn allows_ignoring_trailing_whitespace_from_eq() -> TestResult<()> {
761 verify_that!("A string", eq("A string \n\t").ignoring_trailing_whitespace())
762 }
763
764 #[test]
765 fn allows_ignoring_outer_whitespace_from_eq() -> TestResult<()> {
766 verify_that!("A string", eq(" \n\tA string \n\t").ignoring_outer_whitespace())
767 }
768
769 #[test]
770 fn allows_ignoring_ascii_case_from_eq() -> TestResult<()> {
771 verify_that!("A string", eq("A STRING").ignoring_ascii_case())
772 }
773
774 #[test]
775 fn allows_ignoring_ascii_case_from_eq_deref_of_str_slice() -> TestResult<()> {
776 verify_that!("A string", eq_deref_of("A STRING").ignoring_ascii_case())
777 }
778
779 #[test]
780 fn allows_ignoring_ascii_case_from_eq_deref_of_owned_string() -> TestResult<()> {
781 verify_that!("A string", eq_deref_of("A STRING".to_string()).ignoring_ascii_case())
782 }
783
784 #[test]
785 fn matches_string_containing_expected_value_in_contains_mode() -> TestResult<()> {
786 verify_that!("Some string", contains_substring("str"))
787 }
788
789 #[test]
790 fn matches_string_containing_expected_value_in_contains_mode_while_ignoring_ascii_case()
791 -> TestResult<()> {
792 verify_that!("Some string", contains_substring("STR").ignoring_ascii_case())
793 }
794
795 #[test]
796 fn contains_substring_matches_correct_number_of_substrings() -> TestResult<()> {
797 verify_that!("Some string", contains_substring("str").times(eq(1)))
798 }
799
800 #[test]
801 fn contains_substring_does_not_match_incorrect_number_of_substrings() -> TestResult<()> {
802 verify_that!("Some string\nSome string", not(contains_substring("string").times(eq(1))))
803 }
804
805 #[test]
806 fn contains_substring_does_not_match_when_substrings_overlap() -> TestResult<()> {
807 verify_that!("ababab", not(contains_substring("abab").times(eq(2))))
808 }
809
810 #[test]
811 fn starts_with_matches_string_reference_with_prefix() -> TestResult<()> {
812 verify_that!("Some value", starts_with("Some"))
813 }
814
815 #[test]
816 fn starts_with_matches_string_reference_with_prefix_ignoring_ascii_case() -> TestResult<()> {
817 verify_that!("Some value", starts_with("SOME").ignoring_ascii_case())
818 }
819
820 #[test]
821 fn starts_with_does_not_match_wrong_prefix_ignoring_ascii_case() -> TestResult<()> {
822 verify_that!("Some value", not(starts_with("OTHER").ignoring_ascii_case()))
823 }
824
825 #[test]
826 fn ends_with_does_not_match_short_string_ignoring_ascii_case() -> TestResult<()> {
827 verify_that!("Some", not(starts_with("OTHER").ignoring_ascii_case()))
828 }
829
830 #[test]
831 fn starts_with_does_not_match_string_without_prefix() -> TestResult<()> {
832 verify_that!("Some value", not(starts_with("Another")))
833 }
834
835 #[test]
836 fn starts_with_does_not_match_string_with_substring_not_at_beginning() -> TestResult<()> {
837 verify_that!("Some value", not(starts_with("value")))
838 }
839
840 #[test]
841 fn ends_with_matches_string_reference_with_suffix() -> TestResult<()> {
842 verify_that!("Some value", ends_with("value"))
843 }
844
845 #[test]
846 fn ends_with_matches_string_reference_with_suffix_ignoring_ascii_case() -> TestResult<()> {
847 verify_that!("Some value", ends_with("VALUE").ignoring_ascii_case())
848 }
849
850 #[test]
851 fn ends_with_does_not_match_wrong_suffix_ignoring_ascii_case() -> TestResult<()> {
852 verify_that!("Some value", not(ends_with("OTHER").ignoring_ascii_case()))
853 }
854
855 #[test]
856 fn ends_with_does_not_match_too_short_string_ignoring_ascii_case() -> TestResult<()> {
857 verify_that!("Some", not(ends_with("OTHER").ignoring_ascii_case()))
858 }
859
860 #[test]
861 fn ends_with_does_not_match_string_without_suffix() -> TestResult<()> {
862 verify_that!("Some value", not(ends_with("other value")))
863 }
864
865 #[test]
866 fn ends_with_does_not_match_string_with_substring_not_at_end() -> TestResult<()> {
867 verify_that!("Some value", not(ends_with("Some")))
868 }
869
870 #[test]
871 fn describes_itself_for_matching_result() -> TestResult<()> {
872 let matcher = StrMatcher::with_default_config("A string");
873 verify_that!(
874 matcher.describe(MatcherResult::Match),
875 displays_as(eq("is equal to \"A string\""))
876 )
877 }
878
879 #[test]
880 fn describes_itself_for_non_matching_result() -> TestResult<()> {
881 let matcher = StrMatcher::with_default_config("A string");
882 verify_that!(
883 matcher.describe(MatcherResult::NoMatch),
884 displays_as(eq("isn't equal to \"A string\""))
885 )
886 }
887
888 #[test]
889 fn describes_itself_for_matching_result_ignoring_leading_whitespace() -> TestResult<()> {
890 let matcher = StrMatcher::with_default_config("A string").ignoring_leading_whitespace();
891 verify_that!(
892 matcher.describe(MatcherResult::Match),
893 displays_as(eq("is equal to \"A string\" (ignoring leading whitespace)"))
894 )
895 }
896
897 #[test]
898 fn describes_itself_for_non_matching_result_ignoring_leading_whitespace() -> TestResult<()> {
899 let matcher = StrMatcher::with_default_config("A string").ignoring_leading_whitespace();
900 verify_that!(
901 matcher.describe(MatcherResult::NoMatch),
902 displays_as(eq("isn't equal to \"A string\" (ignoring leading whitespace)"))
903 )
904 }
905
906 #[test]
907 fn describes_itself_for_matching_result_ignoring_trailing_whitespace() -> TestResult<()> {
908 let matcher = StrMatcher::with_default_config("A string").ignoring_trailing_whitespace();
909 verify_that!(
910 matcher.describe(MatcherResult::Match),
911 displays_as(eq("is equal to \"A string\" (ignoring trailing whitespace)"))
912 )
913 }
914
915 #[test]
916 fn describes_itself_for_matching_result_ignoring_leading_and_trailing_whitespace()
917 -> TestResult<()> {
918 let matcher = StrMatcher::with_default_config("A string").ignoring_outer_whitespace();
919 verify_that!(
920 matcher.describe(MatcherResult::Match),
921 displays_as(eq("is equal to \"A string\" (ignoring leading and trailing whitespace)"))
922 )
923 }
924
925 #[test]
926 fn describes_itself_for_matching_result_ignoring_ascii_case() -> TestResult<()> {
927 let matcher = StrMatcher::with_default_config("A string").ignoring_ascii_case();
928 verify_that!(
929 matcher.describe(MatcherResult::Match),
930 displays_as(eq("is equal to \"A string\" (ignoring ASCII case)"))
931 )
932 }
933
934 #[test]
935 fn describes_itself_for_matching_result_ignoring_ascii_case_and_leading_whitespace()
936 -> TestResult<()> {
937 let matcher = StrMatcher::with_default_config("A string")
938 .ignoring_leading_whitespace()
939 .ignoring_ascii_case();
940 verify_that!(
941 matcher.describe(MatcherResult::Match),
942 displays_as(eq(
943 "is equal to \"A string\" (ignoring leading whitespace, ignoring ASCII case)"
944 ))
945 )
946 }
947
948 #[test]
949 fn describes_itself_for_matching_result_in_contains_mode() -> TestResult<()> {
950 let matcher = contains_substring("A string");
951 verify_that!(
952 matcher.describe(MatcherResult::Match),
953 displays_as(eq("contains a substring \"A string\""))
954 )
955 }
956
957 #[test]
958 fn describes_itself_for_non_matching_result_in_contains_mode() -> TestResult<()> {
959 let matcher = contains_substring("A string");
960 verify_that!(
961 matcher.describe(MatcherResult::NoMatch),
962 displays_as(eq("does not contain a substring \"A string\""))
963 )
964 }
965
966 #[test]
967 fn describes_itself_with_count_number() -> TestResult<()> {
968 let matcher = contains_substring("A string").times(gt(2));
969 verify_that!(
970 matcher.describe(MatcherResult::Match),
971 displays_as(eq("contains a substring \"A string\" (count is greater than 2)"))
972 )
973 }
974
975 #[test]
976 fn describes_itself_for_matching_result_in_starts_with_mode() -> TestResult<()> {
977 let matcher = starts_with("A string");
978 verify_that!(
979 matcher.describe(MatcherResult::Match),
980 displays_as(eq("starts with prefix \"A string\""))
981 )
982 }
983
984 #[test]
985 fn describes_itself_for_non_matching_result_in_starts_with_mode() -> TestResult<()> {
986 let matcher = starts_with("A string");
987 verify_that!(
988 matcher.describe(MatcherResult::NoMatch),
989 displays_as(eq("does not start with \"A string\""))
990 )
991 }
992
993 #[test]
994 fn describes_itself_for_matching_result_in_ends_with_mode() -> TestResult<()> {
995 let matcher = ends_with("A string");
996 verify_that!(
997 matcher.describe(MatcherResult::Match),
998 displays_as(eq("ends with suffix \"A string\""))
999 )
1000 }
1001
1002 #[test]
1003 fn describes_itself_for_non_matching_result_in_ends_with_mode() -> TestResult<()> {
1004 let matcher = ends_with("A string");
1005 verify_that!(
1006 matcher.describe(MatcherResult::NoMatch),
1007 displays_as(eq("does not end with \"A string\""))
1008 )
1009 }
1010
1011 #[test]
1012 fn match_explanation_contains_diff_of_strings_if_more_than_one_line() -> TestResult<()> {
1013 let result = verify_that!(
1014 indoc!(
1015 "
1016 First line
1017 Second line
1018 Third line
1019 "
1020 ),
1021 starts_with(indoc!(
1022 "
1023 First line
1024 Second lines
1025 Third line
1026 "
1027 ))
1028 );
1029
1030 verify_that!(
1031 result,
1032 err(displays_as(contains_substring(
1033 "\
1034 First line
1035 -Second line
1036 +Second lines
1037 Third line"
1038 )))
1039 )
1040 }
1041
1042 #[test]
1043 fn match_explanation_for_starts_with_ignores_trailing_lines_in_actual_string() -> TestResult<()>
1044 {
1045 let result = verify_that!(
1046 indoc!(
1047 "
1048 First line
1049 Second line
1050 Third line
1051 Fourth line
1052 "
1053 ),
1054 starts_with(indoc!(
1055 "
1056 First line
1057 Second lines
1058 Third line
1059 "
1060 ))
1061 );
1062
1063 verify_that!(
1064 result,
1065 err(displays_as(contains_substring(
1066 "
1067 First line
1068 -Second line
1069 +Second lines
1070 Third line
1071 <---- remaining lines omitted ---->"
1072 )))
1073 )
1074 }
1075
1076 #[test]
1077 fn match_explanation_for_starts_with_includes_both_versions_of_differing_last_line()
1078 -> TestResult<()> {
1079 let result = verify_that!(
1080 indoc!(
1081 "
1082 First line
1083 Second line
1084 Third line
1085 "
1086 ),
1087 starts_with(indoc!(
1088 "
1089 First line
1090 Second lines
1091 "
1092 ))
1093 );
1094
1095 verify_that!(
1096 result,
1097 err(displays_as(contains_substring(
1098 "\
1099 First line
1100 -Second line
1101 +Second lines
1102 <---- remaining lines omitted ---->"
1103 )))
1104 )
1105 }
1106
1107 #[test]
1108 fn match_explanation_for_ends_with_ignores_leading_lines_in_actual_string() -> TestResult<()> {
1109 let result = verify_that!(
1110 indoc!(
1111 "
1112 First line
1113 Second line
1114 Third line
1115 Fourth line
1116 "
1117 ),
1118 ends_with(indoc!(
1119 "
1120 Second line
1121 Third lines
1122 Fourth line
1123 "
1124 ))
1125 );
1126
1127 verify_that!(
1128 result,
1129 err(displays_as(contains_substring(
1130 "
1131 Difference(-actual / +expected):
1132 <---- remaining lines omitted ---->
1133 Second line
1134 -Third line
1135 +Third lines
1136 Fourth line"
1137 )))
1138 )
1139 }
1140
1141 #[test]
1142 fn match_explanation_for_contains_substring_ignores_outer_lines_in_actual_string()
1143 -> TestResult<()> {
1144 let result = verify_that!(
1145 indoc!(
1146 "
1147 First line
1148 Second line
1149 Third line
1150 Fourth line
1151 Fifth line
1152 "
1153 ),
1154 contains_substring(indoc!(
1155 "
1156 Second line
1157 Third lines
1158 Fourth line
1159 "
1160 ))
1161 );
1162
1163 verify_that!(
1164 result,
1165 err(displays_as(contains_substring(
1166 "
1167 Difference(-actual / +expected):
1168 <---- remaining lines omitted ---->
1169 Second line
1170 -Third line
1171 +Third lines
1172 Fourth line
1173 <---- remaining lines omitted ---->"
1174 )))
1175 )
1176 }
1177
1178 #[test]
1179 fn match_explanation_for_contains_substring_shows_diff_when_first_and_last_line_are_incomplete()
1180 -> TestResult<()> {
1181 let result = verify_that!(
1182 indoc!(
1183 "
1184 First line
1185 Second line
1186 Third line
1187 Fourth line
1188 Fifth line
1189 "
1190 ),
1191 contains_substring(indoc!(
1192 "
1193 line
1194 Third line
1195 Foorth line
1196 Fifth"
1197 ))
1198 );
1199
1200 verify_that!(
1201 result,
1202 err(displays_as(contains_substring(
1203 "
1204 Difference(-actual / +expected):
1205 <---- remaining lines omitted ---->
1206 -Second line
1207 +line
1208 Third line
1209 -Fourth line
1210 +Foorth line
1211 -Fifth line
1212 +Fifth
1213 <---- remaining lines omitted ---->"
1214 )))
1215 )
1216 }
1217
1218 #[test]
1219 fn match_explanation_for_eq_does_not_ignore_trailing_lines_in_actual_string() -> TestResult<()>
1220 {
1221 let result = verify_that!(
1222 indoc!(
1223 "
1224 First line
1225 Second line
1226 Third line
1227 Fourth line
1228 "
1229 ),
1230 eq(indoc!(
1231 "
1232 First line
1233 Second lines
1234 Third line
1235 "
1236 ))
1237 );
1238
1239 verify_that!(
1240 result,
1241 err(displays_as(contains_substring(
1242 "\
1243 First line
1244 -Second line
1245 +Second lines
1246 Third line
1247 -Fourth line"
1248 )))
1249 )
1250 }
1251
1252 #[test]
1253 fn match_explanation_does_not_show_diff_if_actual_value_is_single_line() -> TestResult<()> {
1254 let result = verify_that!(
1255 "First line",
1256 starts_with(indoc!(
1257 "
1258 Second line
1259 Third line
1260 "
1261 ))
1262 );
1263
1264 verify_that!(
1265 result,
1266 err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
1267 )
1268 }
1269
1270 #[test]
1271 fn match_explanation_does_not_show_diff_if_expected_value_is_single_line() -> TestResult<()> {
1272 let result = verify_that!(
1273 indoc!(
1274 "
1275 First line
1276 Second line
1277 Third line
1278 "
1279 ),
1280 starts_with("Second line")
1281 );
1282
1283 verify_that!(
1284 result,
1285 err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
1286 )
1287 }
1288
1289 #[test]
1290 fn eq_ignoring_unicode_case_matches_string_with_non_unicode_different_case() -> TestResult<()> {
1291 verify_that!("Κάποια τιμή", eq("ΚΆΠΟΙΑ ΤΙΜΉ").ignoring_unicode_case())
1292 }
1293
1294 #[test]
1295 fn eq_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1296 verify_that!("Κάποια τιμή", not(eq("Some Value").ignoring_unicode_case()))
1297 }
1298
1299 #[test]
1300 fn starts_with_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1301 -> TestResult<()> {
1302 verify_that!("Κάποια τιμή", starts_with("ΚΆΠΟΙΑ").ignoring_unicode_case())
1303 }
1304
1305 #[test]
1306 fn starts_with_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1307 verify_that!("Κάποια τιμή", not(starts_with("Some Value").ignoring_unicode_case()))
1308 }
1309
1310 #[test]
1311 fn ends_with_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1312 -> TestResult<()> {
1313 verify_that!("Κάποια τιμή", ends_with("ΤΙΜΉ").ignoring_unicode_case())
1314 }
1315
1316 #[test]
1317 fn ends_with_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1318 verify_that!("Κάποια τιμή", not(ends_with("Some Value").ignoring_unicode_case()))
1319 }
1320
1321 #[test]
1322 fn contains_substring_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1323 -> TestResult<()> {
1324 verify_that!("Κάποια τιμή", contains_substring("ΠΟΙΑ ").ignoring_unicode_case())
1325 }
1326
1327 #[test]
1328 fn contains_substring_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()>
1329 {
1330 verify_that!("Κάποια τιμή", not(contains_substring("me Val").ignoring_unicode_case()))
1331 }
1332}