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