Skip to main content

test_that/matchers/
str_matcher.rs

1// Copyright 2023 Google LLC
2// Copyright 2026 Bradford Hovinen <bradford@hovinen.me>
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//      http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use 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
30/// Matches a string containing a given substring.
31///
32/// Both the actual value and the expected substring may be either a `String` or
33/// a string reference.
34///
35/// ```
36/// # use test_that::prelude::*;
37/// # fn should_pass_1() -> TestResult<()> {
38/// verify_that!("Some value", contains_substring("Some"))?;  // Passes
39/// #     Ok(())
40/// # }
41/// # fn should_fail() -> TestResult<()> {
42/// verify_that!("Another value", contains_substring("Some"))?;   // Fails
43/// #     Ok(())
44/// # }
45/// # fn should_pass_2() -> TestResult<()> {
46/// verify_that!("Some value".to_string(), contains_substring("value"))?;   // Passes
47/// verify_that!("Some value", contains_substring("value".to_string()))?;   // Passes
48/// #     Ok(())
49/// # }
50/// # should_pass_1().unwrap();
51/// # should_fail().unwrap_err();
52/// # should_pass_2().unwrap();
53/// ```
54///
55/// See the [`StrMatcherConfigurator`] extension trait for more options on how
56/// the string is matched.
57///
58/// > Note on memory use: In most cases, this matcher does not allocate memory
59/// > when matching strings. However, it must allocate copies of both the actual
60/// > and expected values when matching strings while
61/// > [`ignoring_ascii_case`][StrMatcherConfigurator::ignoring_ascii_case] is
62/// > set.
63pub fn contains_substring<T>(expected: T) -> StrMatcher<T> {
64    StrMatcher {
65        configuration: Configuration { mode: MatchMode::Contains, ..Default::default() },
66        expected,
67    }
68}
69
70/// Matches a string which starts with the given prefix.
71///
72/// Both the actual value and the expected prefix may be either a `String` or
73/// a string reference.
74///
75/// ```
76/// # use test_that::prelude::*;
77/// # fn should_pass_1() -> TestResult<()> {
78/// verify_that!("Some value", starts_with("Some"))?;  // Passes
79/// #     Ok(())
80/// # }
81/// # fn should_fail_1() -> TestResult<()> {
82/// verify_that!("Another value", starts_with("Some"))?;   // Fails
83/// #     Ok(())
84/// # }
85/// # fn should_fail_2() -> TestResult<()> {
86/// verify_that!("Some value", starts_with("value"))?;  // Fails
87/// #     Ok(())
88/// # }
89/// # fn should_pass_2() -> TestResult<()> {
90/// verify_that!("Some value".to_string(), starts_with("Some"))?;   // Passes
91/// verify_that!("Some value", starts_with("Some".to_string()))?;   // Passes
92/// #     Ok(())
93/// # }
94/// # should_pass_1().unwrap();
95/// # should_fail_1().unwrap_err();
96/// # should_fail_2().unwrap_err();
97/// # should_pass_2().unwrap();
98/// ```
99///
100/// See the [`StrMatcherConfigurator`] extension trait for more options on how
101/// the string is matched.
102pub fn starts_with<T>(expected: T) -> StrMatcher<T> {
103    StrMatcher {
104        configuration: Configuration { mode: MatchMode::StartsWith, ..Default::default() },
105        expected,
106    }
107}
108
109/// Matches a string which ends with the given suffix.
110///
111/// Both the actual value and the expected suffix may be either a `String` or
112/// a string reference.
113///
114/// ```
115/// # use test_that::prelude::*;
116/// # fn should_pass_1() -> TestResult<()> {
117/// verify_that!("Some value", ends_with("value"))?;  // Passes
118/// #     Ok(())
119/// # }
120/// # fn should_fail_1() -> TestResult<()> {
121/// verify_that!("Some value", ends_with("other value"))?;   // Fails
122/// #     Ok(())
123/// # }
124/// # fn should_fail_2() -> TestResult<()> {
125/// verify_that!("Some value", ends_with("Some"))?;  // Fails
126/// #     Ok(())
127/// # }
128/// # fn should_pass_2() -> TestResult<()> {
129/// verify_that!("Some value".to_string(), ends_with("value"))?;   // Passes
130/// verify_that!("Some value", ends_with("value".to_string()))?;   // Passes
131/// #     Ok(())
132/// # }
133/// # should_pass_1().unwrap();
134/// # should_fail_1().unwrap_err();
135/// # should_fail_2().unwrap_err();
136/// # should_pass_2().unwrap();
137/// ```
138///
139/// See the [`StrMatcherConfigurator`] extension trait for more options on how
140/// the string is matched.
141pub fn ends_with<T>(expected: T) -> StrMatcher<T> {
142    StrMatcher {
143        configuration: Configuration { mode: MatchMode::EndsWith, ..Default::default() },
144        expected,
145    }
146}
147
148/// Extension trait to configure [`StrMatcher`].
149///
150/// This can be used with the following matchers:
151///
152///  * [`eq`][crate::matchers::eq_matcher::eq] when used with types which deref
153///    to `str`, including `String` and `&str`,
154///  * [`eq_deref_of`][crate::matchers::eq_deref_of_matcher::eq_deref_of] when
155///    used with types which deref to `str`,
156///  * [`contains_substring`],
157///  * [`starts_with`],
158///  * [`ends_with`].
159pub trait StrMatcherConfigurator<ExpectedT> {
160    /// Configures the matcher to ignore any leading whitespace in either the
161    /// actual or the expected value.
162    ///
163    /// Whitespace is defined as in [`str::trim_start`].
164    ///
165    /// ```
166    /// # use test_that::prelude::*;
167    /// # fn should_pass() -> TestResult<()> {
168    /// verify_that!("A string", eq("   A string").ignoring_leading_whitespace())?; // Passes
169    /// verify_that!("   A string", eq("A string").ignoring_leading_whitespace())?; // Passes
170    /// #     Ok(())
171    /// # }
172    /// # should_pass().unwrap();
173    /// ```
174    ///
175    /// When all other configuration options are left as the defaults, this is
176    /// equivalent to invoking [`str::trim_start`] on both the expected and
177    /// actual value.
178    fn ignoring_leading_whitespace(self) -> StrMatcher<ExpectedT>;
179
180    /// Configures the matcher to ignore any trailing whitespace in either the
181    /// actual or the expected value.
182    ///
183    /// Whitespace is defined as in [`str::trim_end`].
184    ///
185    /// ```
186    /// # use test_that::prelude::*;
187    /// # fn should_pass() -> TestResult<()> {
188    /// verify_that!("A string", eq("A string   ").ignoring_trailing_whitespace())?; // Passes
189    /// verify_that!("A string   ", eq("A string").ignoring_trailing_whitespace())?; // Passes
190    /// #     Ok(())
191    /// # }
192    /// # should_pass().unwrap();
193    /// ```
194    ///
195    /// When all other configuration options are left as the defaults, this is
196    /// equivalent to invoking [`str::trim_end`] on both the expected and
197    /// actual value.
198    fn ignoring_trailing_whitespace(self) -> StrMatcher<ExpectedT>;
199
200    /// Configures the matcher to ignore both leading and trailing whitespace in
201    /// either the actual or the expected value.
202    ///
203    /// Whitespace is defined as in [`str::trim`].
204    ///
205    /// ```
206    /// # use test_that::prelude::*;
207    /// # fn should_pass() -> TestResult<()> {
208    /// verify_that!("A string", eq("   A string   ").ignoring_outer_whitespace())?; // Passes
209    /// verify_that!("   A string   ", eq("A string").ignoring_outer_whitespace())?; // Passes
210    /// #     Ok(())
211    /// # }
212    /// # should_pass().unwrap();
213    /// ```
214    ///
215    /// This is equivalent to invoking both
216    /// [`ignoring_leading_whitespace`][StrMatcherConfigurator::ignoring_leading_whitespace] and
217    /// [`ignoring_trailing_whitespace`][StrMatcherConfigurator::ignoring_trailing_whitespace].
218    ///
219    /// When all other configuration options are left as the defaults, this is
220    /// equivalent to invoking [`str::trim`] on both the expected and actual
221    /// value.
222    fn ignoring_outer_whitespace(self) -> StrMatcher<ExpectedT>;
223
224    /// Configures the matcher to ignore ASCII case when comparing values.
225    ///
226    /// This uses the same rules for case as [`str::eq_ignore_ascii_case`].
227    ///
228    /// ```
229    /// # use test_that::prelude::*;
230    /// # fn should_pass() -> TestResult<()> {
231    /// verify_that!("Some value", eq("SOME VALUE").ignoring_ascii_case())?;  // Passes
232    /// #     Ok(())
233    /// # }
234    /// # fn should_fail() -> TestResult<()> {
235    /// verify_that!("Another value", eq("Some value").ignoring_ascii_case())?;   // Fails
236    /// #     Ok(())
237    /// # }
238    /// # should_pass().unwrap();
239    /// # should_fail().unwrap_err();
240    /// ```
241    ///
242    /// This is **not guaranteed** to match strings with differing upper/lower
243    /// case characters outside of the codepoints 0-127 covered by ASCII.
244    fn ignoring_ascii_case(self) -> StrMatcher<ExpectedT>;
245
246    /// Configures the matcher to match only strings which otherwise satisfy the
247    /// conditions a number times matched by the matcher `times`.
248    ///
249    /// ```
250    /// # use test_that::prelude::*;
251    /// # fn should_pass() -> TestResult<()> {
252    /// verify_that!("Some value\nSome value", contains_substring("value").times(eq(2)))?; // Passes
253    /// #     Ok(())
254    /// # }
255    /// # fn should_fail() -> TestResult<()> {
256    /// verify_that!("Some value", contains_substring("value").times(eq(2)))?; // Fails
257    /// #     Ok(())
258    /// # }
259    /// # should_pass().unwrap();
260    /// # should_fail().unwrap_err();
261    /// ```
262    ///
263    /// The matched substrings must be disjoint from one another to be counted.
264    /// For example:
265    ///
266    /// ```
267    /// # use test_that::prelude::*;
268    /// # fn should_fail() -> TestResult<()> {
269    /// // Fails: substrings distinct but not disjoint!
270    /// verify_that!("ababab", contains_substring("abab").times(eq(2)))?;
271    /// #     Ok(())
272    /// # }
273    /// # should_fail().unwrap_err();
274    /// ```
275    ///
276    /// This is only meaningful when the matcher was constructed with
277    /// [`contains_substring`]. This method will panic when it is used with any
278    /// other matcher construction.
279    fn times(self, times: impl Matcher<usize> + 'static) -> StrMatcher<ExpectedT>;
280}
281
282/// A matcher which matches equality or containment of a string-like value in a
283/// configurable way.
284///
285/// See [`StrMatcherConfigurator`] for methods which modify the behaviour of
286/// this matcher.
287///
288/// The following matcher functions instantiate this:
289///
290///  * [`contains_substring`],
291///  * [`starts_with`],
292///  * [`ends_with`],
293///  * all methods of [`StrMatcherConfigurator`].
294pub 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    /// Returns a [`StrMatcher`] with a default configuration to match against
371    /// the given expected value.
372    ///
373    /// This default configuration is sensitive to whitespace and case.
374    fn with_default_config(expected: T) -> Self {
375        Self { expected, configuration: Default::default() }
376    }
377}
378
379// Holds all the information on how the expected and actual strings are to be
380// compared. Its associated functions perform the actual matching operations
381// on string references. The struct and comparison methods therefore need not be
382// parameterised, saving compilation time and binary size on monomorphisation.
383//
384// The default value represents exact equality of the strings.
385struct 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    // The entry point for all string matching. StrMatcher::matches redirects
419    // immediately to this function.
420    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    // Returns whether actual contains expected a number of times matched by the
458    // matcher self.times. Does not take other configuration into account.
459    fn does_containment_match(&self, actual: &str, expected: &str) -> bool {
460        if let Some(times) = self.times.as_ref() {
461            // Split returns an iterator over the "boundaries" left and right of the
462            // substring to be matched, of which there is one more than the number of
463            // substrings.
464            matches!(times.matches(&(actual.split(expected).count() - 1)), MatcherResult::Match)
465        } else {
466            actual.contains(expected)
467        }
468    }
469
470    // StrMatcher::describe redirects immediately to this function.
471    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            // TODO - b/283448414 : Support StrMatcher with ignore_leading_whitespace.
521            return default_explanation;
522        }
523
524        if self.ignore_trailing_whitespace {
525            // TODO - b/283448414 : Support StrMatcher with ignore_trailing_whitespace.
526            return default_explanation;
527        }
528
529        if self.times.is_some() {
530            // TODO - b/283448414 : Support StrMatcher with times.
531            return default_explanation;
532        }
533        if matches!(self.case_policy, CasePolicy::IgnoreAscii) {
534            // TODO - b/283448414 : Support StrMatcher with ignore ascii case policy.
535            return default_explanation;
536        }
537        if self.do_strings_match(expected, actual) {
538            // TODO - b/283448414 : Consider supporting debug difference if the
539            // strings match. This can be useful when a small contains is found
540            // in a long string.
541            return default_explanation;
542        }
543
544        let diff = match self.mode {
545            MatchMode::Equals | MatchMode::StartsWith | MatchMode::Contains => {
546                // TODO(b/287632452): Also consider improving the output in MatchMode::Contains
547                // when the substring begins or ends in the middle of a line of the actual
548                // value.
549                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}