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