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