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 ignore Unicode case when comparing values.
247    ///
248    /// This converts both the actual and expected values to Unicode lower case,
249    /// then checks their equality.
250    ///
251    /// ```
252    /// # use test_that::prelude::*;
253    /// # fn should_pass() -> TestResult<()> {
254    /// verify_that!("Κάποια τιμή", eq("ΚΆΠΟΙΑ ΤΙΜΉ").ignoring_unicode_case())?;  // Passes
255    /// #     Ok(())
256    /// # }
257    /// # fn should_fail() -> TestResult<()> {
258    /// verify_that!("Άλλη τιμή", eq("Κάποια τιμή").ignoring_unicode_case())?;   // Fails
259    /// #     Ok(())
260    /// # }
261    /// # should_pass().unwrap();
262    /// # should_fail().unwrap_err();
263    /// ```
264    ///
265    /// This is **not guaranteed** to match strings with differing upper/lower
266    /// case characters outside of the codepoints 0-127 covered by ASCII.
267    fn ignoring_unicode_case(self) -> StrMatcher<ExpectedT>;
268
269    /// Configures the matcher to match only strings which otherwise satisfy the
270    /// conditions a number times matched by the matcher `times`.
271    ///
272    /// ```
273    /// # use test_that::prelude::*;
274    /// # fn should_pass() -> TestResult<()> {
275    /// verify_that!("Some value\nSome value", contains_substring("value").times(eq(2)))?; // Passes
276    /// #     Ok(())
277    /// # }
278    /// # fn should_fail() -> TestResult<()> {
279    /// verify_that!("Some value", contains_substring("value").times(eq(2)))?; // Fails
280    /// #     Ok(())
281    /// # }
282    /// # should_pass().unwrap();
283    /// # should_fail().unwrap_err();
284    /// ```
285    ///
286    /// The matched substrings must be disjoint from one another to be counted.
287    /// For example:
288    ///
289    /// ```
290    /// # use test_that::prelude::*;
291    /// # fn should_fail() -> TestResult<()> {
292    /// // Fails: substrings distinct but not disjoint!
293    /// verify_that!("ababab", contains_substring("abab").times(eq(2)))?;
294    /// #     Ok(())
295    /// # }
296    /// # should_fail().unwrap_err();
297    /// ```
298    ///
299    /// This is only meaningful when the matcher was constructed with
300    /// [`contains_substring`]. This method will panic when it is used with any
301    /// other matcher construction.
302    fn times(self, times: impl Matcher<usize> + 'static) -> StrMatcher<ExpectedT>;
303}
304
305/// A matcher which matches equality or containment of a string-like value in a
306/// configurable way.
307///
308/// See [`StrMatcherConfigurator`] for methods which modify the behaviour of
309/// this matcher.
310///
311/// The following matcher functions instantiate this:
312///
313///  * [`contains_substring`],
314///  * [`starts_with`],
315///  * [`ends_with`],
316///  * all methods of [`StrMatcherConfigurator`].
317pub struct StrMatcher<ExpectedT> {
318    expected: ExpectedT,
319    configuration: Configuration,
320}
321
322impl<ExpectedT, ActualT> Matcher<ActualT> for StrMatcher<ExpectedT>
323where
324    ExpectedT: Deref<Target = str> + Debug,
325    ActualT: AsRef<str> + Debug + ?Sized,
326{
327    fn matches(&self, actual: &ActualT) -> MatcherResult {
328        self.configuration.do_strings_match(self.expected.deref(), actual.as_ref()).into()
329    }
330
331    fn explain_match(&self, actual: &ActualT) -> Description {
332        self.configuration.explain_match(self.expected.deref(), actual.as_ref())
333    }
334}
335
336impl<ExpectedT: Deref<Target = str>> Describable for StrMatcher<ExpectedT> {
337    fn describe(&self, matcher_result: MatcherResult) -> Description {
338        self.configuration.describe(matcher_result, self.expected.deref())
339    }
340}
341
342impl<ExpectedT, MatcherT: Into<StrMatcher<ExpectedT>>> StrMatcherConfigurator<ExpectedT>
343    for MatcherT
344{
345    fn ignoring_leading_whitespace(self) -> StrMatcher<ExpectedT> {
346        let existing = self.into();
347        StrMatcher {
348            configuration: existing.configuration.ignoring_leading_whitespace(),
349            ..existing
350        }
351    }
352
353    fn ignoring_trailing_whitespace(self) -> StrMatcher<ExpectedT> {
354        let existing = self.into();
355        StrMatcher {
356            configuration: existing.configuration.ignoring_trailing_whitespace(),
357            ..existing
358        }
359    }
360
361    fn ignoring_outer_whitespace(self) -> StrMatcher<ExpectedT> {
362        let existing = self.into();
363        StrMatcher { configuration: existing.configuration.ignoring_outer_whitespace(), ..existing }
364    }
365
366    fn ignoring_ascii_case(self) -> StrMatcher<ExpectedT> {
367        let existing = self.into();
368        StrMatcher { configuration: existing.configuration.ignoring_ascii_case(), ..existing }
369    }
370
371    fn ignoring_unicode_case(self) -> StrMatcher<ExpectedT> {
372        let existing = self.into();
373        StrMatcher { configuration: existing.configuration.ignoring_unicode_case(), ..existing }
374    }
375
376    fn times(self, times: impl Matcher<usize> + 'static) -> StrMatcher<ExpectedT> {
377        let existing = self.into();
378        if !matches!(existing.configuration.mode, MatchMode::Contains) {
379            panic!("The times() configurator is only meaningful with contains_substring().");
380        }
381        StrMatcher { configuration: existing.configuration.times(times), ..existing }
382    }
383}
384
385impl<T: Deref<Target = str>> From<EqMatcher<T>> for StrMatcher<T> {
386    fn from(value: EqMatcher<T>) -> Self {
387        Self::with_default_config(value.expected)
388    }
389}
390
391impl<T: Deref<Target = str>> From<EqDerefOfMatcher<T>> for StrMatcher<T> {
392    fn from(value: EqDerefOfMatcher<T>) -> Self {
393        Self::with_default_config(value.expected)
394    }
395}
396
397impl<T> StrMatcher<T> {
398    /// Returns a [`StrMatcher`] with a default configuration to match against
399    /// the given expected value.
400    ///
401    /// This default configuration is sensitive to whitespace and case.
402    fn with_default_config(expected: T) -> Self {
403        Self { expected, configuration: Default::default() }
404    }
405}
406
407// Holds all the information on how the expected and actual strings are to be
408// compared. Its associated functions perform the actual matching operations
409// on string references. The struct and comparison methods therefore need not be
410// parameterised, saving compilation time and binary size on monomorphisation.
411//
412// The default value represents exact equality of the strings.
413struct Configuration {
414    mode: MatchMode,
415    ignore_leading_whitespace: bool,
416    ignore_trailing_whitespace: bool,
417    case_policy: CasePolicy,
418    times: Option<Box<dyn Matcher<usize>>>,
419}
420
421#[derive(Clone)]
422enum MatchMode {
423    Equals,
424    Contains,
425    StartsWith,
426    EndsWith,
427}
428
429impl MatchMode {
430    fn to_diff_mode(&self) -> edit_distance::Mode {
431        match self {
432            MatchMode::StartsWith | MatchMode::EndsWith => edit_distance::Mode::Prefix,
433            MatchMode::Contains => edit_distance::Mode::Contains,
434            MatchMode::Equals => edit_distance::Mode::Exact,
435        }
436    }
437}
438
439#[derive(Clone)]
440enum CasePolicy {
441    Respect,
442    IgnoreAscii,
443    IgnoreUnicode,
444}
445
446impl Configuration {
447    // The entry point for all string matching. StrMatcher::matches redirects
448    // immediately to this function.
449    fn do_strings_match(&self, expected: &str, actual: &str) -> bool {
450        let (expected, actual) =
451            match (self.ignore_leading_whitespace, self.ignore_trailing_whitespace) {
452                (true, true) => (expected.trim(), actual.trim()),
453                (true, false) => (expected.trim_start(), actual.trim_start()),
454                (false, true) => (expected.trim_end(), actual.trim_end()),
455                (false, false) => (expected, actual),
456            };
457        match self.mode {
458            MatchMode::Equals => match self.case_policy {
459                CasePolicy::Respect => expected == actual,
460                CasePolicy::IgnoreAscii => expected.eq_ignore_ascii_case(actual),
461                CasePolicy::IgnoreUnicode => expected.to_lowercase() == actual.to_lowercase(),
462            },
463            MatchMode::Contains => match self.case_policy {
464                CasePolicy::Respect => self.does_containment_match(actual, expected),
465                CasePolicy::IgnoreAscii => self.does_containment_match(
466                    actual.to_ascii_lowercase().as_str(),
467                    expected.to_ascii_lowercase().as_str(),
468                ),
469                CasePolicy::IgnoreUnicode => self.does_containment_match(
470                    actual.to_lowercase().as_str(),
471                    expected.to_lowercase().as_str(),
472                ),
473            },
474            MatchMode::StartsWith => match self.case_policy {
475                CasePolicy::Respect => actual.starts_with(expected),
476                CasePolicy::IgnoreAscii => {
477                    actual.len() >= expected.len()
478                        && actual[..expected.len()].eq_ignore_ascii_case(expected)
479                }
480                CasePolicy::IgnoreUnicode => {
481                    actual.len() >= expected.len()
482                        && actual.is_char_boundary(expected.len())
483                        && actual[..expected.len()].to_lowercase() == expected.to_lowercase()
484                }
485            },
486            MatchMode::EndsWith => match self.case_policy {
487                CasePolicy::Respect => actual.ends_with(expected),
488                CasePolicy::IgnoreAscii => {
489                    actual.len() >= expected.len()
490                        && actual[actual.len() - expected.len()..].eq_ignore_ascii_case(expected)
491                }
492                CasePolicy::IgnoreUnicode => {
493                    actual.len() >= expected.len()
494                        && actual.is_char_boundary(actual.len() - expected.len())
495                        && actual[actual.len() - expected.len()..].to_lowercase()
496                            == expected.to_lowercase()
497                }
498            },
499        }
500    }
501
502    // Returns whether actual contains expected a number of times matched by the
503    // matcher self.times. Does not take other configuration into account.
504    fn does_containment_match(&self, actual: &str, expected: &str) -> bool {
505        if let Some(times) = self.times.as_ref() {
506            // Split returns an iterator over the "boundaries" left and right of the
507            // substring to be matched, of which there is one more than the number of
508            // substrings.
509            matches!(times.matches(&(actual.split(expected).count() - 1)), MatcherResult::Match)
510        } else {
511            actual.contains(expected)
512        }
513    }
514
515    // StrMatcher::describe redirects immediately to this function.
516    fn describe(&self, matcher_result: MatcherResult, expected: &str) -> Description {
517        let mut addenda: Vec<Cow<'static, str>> = Vec::with_capacity(3);
518        match (self.ignore_leading_whitespace, self.ignore_trailing_whitespace) {
519            (true, true) => addenda.push("ignoring leading and trailing whitespace".into()),
520            (true, false) => addenda.push("ignoring leading whitespace".into()),
521            (false, true) => addenda.push("ignoring trailing whitespace".into()),
522            (false, false) => {}
523        }
524        match self.case_policy {
525            CasePolicy::Respect => {}
526            CasePolicy::IgnoreAscii => addenda.push("ignoring ASCII case".into()),
527            CasePolicy::IgnoreUnicode => addenda.push("ignoring Unicode case".into()),
528        }
529        if let Some(times) = self.times.as_ref() {
530            addenda.push(format!("count {}", times.describe(matcher_result)).into());
531        }
532        let extra =
533            if !addenda.is_empty() { format!(" ({})", addenda.join(", ")) } else { "".into() };
534        let match_mode_description = match self.mode {
535            MatchMode::Equals => match matcher_result {
536                MatcherResult::Match => "is equal to",
537                MatcherResult::NoMatch => "isn't equal to",
538            },
539            MatchMode::Contains => match matcher_result {
540                MatcherResult::Match => "contains a substring",
541                MatcherResult::NoMatch => "does not contain a substring",
542            },
543            MatchMode::StartsWith => match matcher_result {
544                MatcherResult::Match => "starts with prefix",
545                MatcherResult::NoMatch => "does not start with",
546            },
547            MatchMode::EndsWith => match matcher_result {
548                MatcherResult::Match => "ends with suffix",
549                MatcherResult::NoMatch => "does not end with",
550            },
551        };
552        format!("{match_mode_description} {expected:?}{extra}").into()
553    }
554
555    fn explain_match(&self, expected: &str, actual: &str) -> Description {
556        let default_explanation = format!(
557            "which {}",
558            self.describe(self.do_strings_match(expected, actual).into(), expected)
559        )
560        .into();
561        if !expected.contains('\n') || !actual.contains('\n') {
562            return default_explanation;
563        }
564
565        if self.ignore_leading_whitespace {
566            // TODO - b/283448414 : Support StrMatcher with ignore_leading_whitespace.
567            return default_explanation;
568        }
569
570        if self.ignore_trailing_whitespace {
571            // TODO - b/283448414 : Support StrMatcher with ignore_trailing_whitespace.
572            return default_explanation;
573        }
574
575        if self.times.is_some() {
576            // TODO - b/283448414 : Support StrMatcher with times.
577            return default_explanation;
578        }
579        if matches!(self.case_policy, CasePolicy::IgnoreAscii) {
580            // TODO - b/283448414 : Support StrMatcher with ignore ascii case policy.
581            return default_explanation;
582        }
583        if self.do_strings_match(expected, actual) {
584            // TODO - b/283448414 : Consider supporting debug difference if the
585            // strings match. This can be useful when a small contains is found
586            // in a long string.
587            return default_explanation;
588        }
589
590        let diff = match self.mode {
591            MatchMode::Equals | MatchMode::StartsWith | MatchMode::Contains => {
592                // TODO(b/287632452): Also consider improving the output in MatchMode::Contains
593                // when the substring begins or ends in the middle of a line of the actual
594                // value.
595                create_diff(actual, expected, self.mode.to_diff_mode())
596            }
597            MatchMode::EndsWith => create_diff_reversed(actual, expected, self.mode.to_diff_mode()),
598        };
599
600        format!("{default_explanation}\n{diff}").into()
601    }
602
603    fn ignoring_leading_whitespace(self) -> Self {
604        Self { ignore_leading_whitespace: true, ..self }
605    }
606
607    fn ignoring_trailing_whitespace(self) -> Self {
608        Self { ignore_trailing_whitespace: true, ..self }
609    }
610
611    fn ignoring_outer_whitespace(self) -> Self {
612        Self { ignore_leading_whitespace: true, ignore_trailing_whitespace: true, ..self }
613    }
614
615    fn ignoring_ascii_case(self) -> Self {
616        Self { case_policy: CasePolicy::IgnoreAscii, ..self }
617    }
618
619    fn ignoring_unicode_case(self) -> Self {
620        Self { case_policy: CasePolicy::IgnoreUnicode, ..self }
621    }
622
623    fn times(self, times: impl Matcher<usize> + 'static) -> Self {
624        Self { times: Some(Box::new(times)), ..self }
625    }
626}
627
628impl Default for Configuration {
629    fn default() -> Self {
630        Self {
631            mode: MatchMode::Equals,
632            ignore_leading_whitespace: false,
633            ignore_trailing_whitespace: false,
634            case_policy: CasePolicy::Respect,
635            times: None,
636        }
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::{StrMatcher, StrMatcherConfigurator, contains_substring, ends_with, starts_with};
643    use crate::matcher::{Describable as _, MatcherResult};
644    use crate::prelude::*;
645    use alloc::string::ToString;
646    use indoc::indoc;
647
648    #[test]
649    fn matches_string_reference_with_equal_string_reference() -> TestResult<()> {
650        let matcher = StrMatcher::with_default_config("A string");
651        verify_that!("A string", matcher)
652    }
653
654    #[test]
655    fn does_not_match_string_reference_with_non_equal_string_reference() -> TestResult<()> {
656        let matcher = StrMatcher::with_default_config("Another string");
657        verify_that!("A string", not(matcher))
658    }
659
660    #[test]
661    fn matches_owned_string_with_string_reference() -> TestResult<()> {
662        let matcher = StrMatcher::with_default_config("A string");
663        let value = "A string".to_string();
664        verify_that!(value, matcher)
665    }
666
667    #[test]
668    fn matches_owned_string_reference_with_string_reference() -> TestResult<()> {
669        let matcher = StrMatcher::with_default_config("A string");
670        let value = "A string".to_string();
671        verify_that!(&value, matcher)
672    }
673
674    #[test]
675    fn ignores_leading_whitespace_in_expected_when_requested() -> TestResult<()> {
676        let matcher = StrMatcher::with_default_config(" \n\tA string");
677        verify_that!("A string", matcher.ignoring_leading_whitespace())
678    }
679
680    #[test]
681    fn ignores_leading_whitespace_in_actual_when_requested() -> TestResult<()> {
682        let matcher = StrMatcher::with_default_config("A string");
683        verify_that!(" \n\tA string", matcher.ignoring_leading_whitespace())
684    }
685
686    #[test]
687    fn does_not_match_unequal_remaining_string_when_ignoring_leading_whitespace() -> TestResult<()>
688    {
689        let matcher = StrMatcher::with_default_config(" \n\tAnother string");
690        verify_that!("A string", not(matcher.ignoring_leading_whitespace()))
691    }
692
693    #[test]
694    fn remains_sensitive_to_trailing_whitespace_when_ignoring_leading_whitespace() -> TestResult<()>
695    {
696        let matcher = StrMatcher::with_default_config("A string \n\t");
697        verify_that!("A string", not(matcher.ignoring_leading_whitespace()))
698    }
699
700    #[test]
701    fn ignores_trailing_whitespace_in_expected_when_requested() -> TestResult<()> {
702        let matcher = StrMatcher::with_default_config("A string \n\t");
703        verify_that!("A string", matcher.ignoring_trailing_whitespace())
704    }
705
706    #[test]
707    fn ignores_trailing_whitespace_in_actual_when_requested() -> TestResult<()> {
708        let matcher = StrMatcher::with_default_config("A string");
709        verify_that!("A string \n\t", matcher.ignoring_trailing_whitespace())
710    }
711
712    #[test]
713    fn does_not_match_unequal_remaining_string_when_ignoring_trailing_whitespace() -> TestResult<()>
714    {
715        let matcher = StrMatcher::with_default_config("Another string \n\t");
716        verify_that!("A string", not(matcher.ignoring_trailing_whitespace()))
717    }
718
719    #[test]
720    fn remains_sensitive_to_leading_whitespace_when_ignoring_trailing_whitespace() -> TestResult<()>
721    {
722        let matcher = StrMatcher::with_default_config(" \n\tA string");
723        verify_that!("A string", not(matcher.ignoring_trailing_whitespace()))
724    }
725
726    #[test]
727    fn ignores_leading_and_trailing_whitespace_in_expected_when_requested() -> TestResult<()> {
728        let matcher = StrMatcher::with_default_config(" \n\tA string \n\t");
729        verify_that!("A string", matcher.ignoring_outer_whitespace())
730    }
731
732    #[test]
733    fn ignores_leading_and_trailing_whitespace_in_actual_when_requested() -> TestResult<()> {
734        let matcher = StrMatcher::with_default_config("A string");
735        verify_that!(" \n\tA string \n\t", matcher.ignoring_outer_whitespace())
736    }
737
738    #[test]
739    fn respects_ascii_case_by_default() -> TestResult<()> {
740        let matcher = StrMatcher::with_default_config("A string");
741        verify_that!("A STRING", not(matcher))
742    }
743
744    #[test]
745    fn ignores_ascii_case_when_requested() -> TestResult<()> {
746        let matcher = StrMatcher::with_default_config("A string");
747        verify_that!("A STRING", matcher.ignoring_ascii_case())
748    }
749
750    #[test]
751    fn allows_ignoring_leading_whitespace_from_eq() -> TestResult<()> {
752        verify_that!("A string", eq(" \n\tA string").ignoring_leading_whitespace())
753    }
754
755    #[test]
756    fn allows_ignoring_trailing_whitespace_from_eq() -> TestResult<()> {
757        verify_that!("A string", eq("A string \n\t").ignoring_trailing_whitespace())
758    }
759
760    #[test]
761    fn allows_ignoring_outer_whitespace_from_eq() -> TestResult<()> {
762        verify_that!("A string", eq(" \n\tA string \n\t").ignoring_outer_whitespace())
763    }
764
765    #[test]
766    fn allows_ignoring_ascii_case_from_eq() -> TestResult<()> {
767        verify_that!("A string", eq("A STRING").ignoring_ascii_case())
768    }
769
770    #[test]
771    fn allows_ignoring_ascii_case_from_eq_deref_of_str_slice() -> TestResult<()> {
772        verify_that!("A string", eq_deref_of("A STRING").ignoring_ascii_case())
773    }
774
775    #[test]
776    fn allows_ignoring_ascii_case_from_eq_deref_of_owned_string() -> TestResult<()> {
777        verify_that!("A string", eq_deref_of("A STRING".to_string()).ignoring_ascii_case())
778    }
779
780    #[test]
781    fn matches_string_containing_expected_value_in_contains_mode() -> TestResult<()> {
782        verify_that!("Some string", contains_substring("str"))
783    }
784
785    #[test]
786    fn matches_string_containing_expected_value_in_contains_mode_while_ignoring_ascii_case()
787    -> TestResult<()> {
788        verify_that!("Some string", contains_substring("STR").ignoring_ascii_case())
789    }
790
791    #[test]
792    fn contains_substring_matches_correct_number_of_substrings() -> TestResult<()> {
793        verify_that!("Some string", contains_substring("str").times(eq(1)))
794    }
795
796    #[test]
797    fn contains_substring_does_not_match_incorrect_number_of_substrings() -> TestResult<()> {
798        verify_that!("Some string\nSome string", not(contains_substring("string").times(eq(1))))
799    }
800
801    #[test]
802    fn contains_substring_does_not_match_when_substrings_overlap() -> TestResult<()> {
803        verify_that!("ababab", not(contains_substring("abab").times(eq(2))))
804    }
805
806    #[test]
807    fn starts_with_matches_string_reference_with_prefix() -> TestResult<()> {
808        verify_that!("Some value", starts_with("Some"))
809    }
810
811    #[test]
812    fn starts_with_matches_string_reference_with_prefix_ignoring_ascii_case() -> TestResult<()> {
813        verify_that!("Some value", starts_with("SOME").ignoring_ascii_case())
814    }
815
816    #[test]
817    fn starts_with_does_not_match_wrong_prefix_ignoring_ascii_case() -> TestResult<()> {
818        verify_that!("Some value", not(starts_with("OTHER").ignoring_ascii_case()))
819    }
820
821    #[test]
822    fn ends_with_does_not_match_short_string_ignoring_ascii_case() -> TestResult<()> {
823        verify_that!("Some", not(starts_with("OTHER").ignoring_ascii_case()))
824    }
825
826    #[test]
827    fn starts_with_does_not_match_string_without_prefix() -> TestResult<()> {
828        verify_that!("Some value", not(starts_with("Another")))
829    }
830
831    #[test]
832    fn starts_with_does_not_match_string_with_substring_not_at_beginning() -> TestResult<()> {
833        verify_that!("Some value", not(starts_with("value")))
834    }
835
836    #[test]
837    fn ends_with_matches_string_reference_with_suffix() -> TestResult<()> {
838        verify_that!("Some value", ends_with("value"))
839    }
840
841    #[test]
842    fn ends_with_matches_string_reference_with_suffix_ignoring_ascii_case() -> TestResult<()> {
843        verify_that!("Some value", ends_with("VALUE").ignoring_ascii_case())
844    }
845
846    #[test]
847    fn ends_with_does_not_match_wrong_suffix_ignoring_ascii_case() -> TestResult<()> {
848        verify_that!("Some value", not(ends_with("OTHER").ignoring_ascii_case()))
849    }
850
851    #[test]
852    fn ends_with_does_not_match_too_short_string_ignoring_ascii_case() -> TestResult<()> {
853        verify_that!("Some", not(ends_with("OTHER").ignoring_ascii_case()))
854    }
855
856    #[test]
857    fn ends_with_does_not_match_string_without_suffix() -> TestResult<()> {
858        verify_that!("Some value", not(ends_with("other value")))
859    }
860
861    #[test]
862    fn ends_with_does_not_match_string_with_substring_not_at_end() -> TestResult<()> {
863        verify_that!("Some value", not(ends_with("Some")))
864    }
865
866    #[test]
867    fn describes_itself_for_matching_result() -> TestResult<()> {
868        let matcher = StrMatcher::with_default_config("A string");
869        verify_that!(
870            matcher.describe(MatcherResult::Match),
871            displays_as(eq("is equal to \"A string\""))
872        )
873    }
874
875    #[test]
876    fn describes_itself_for_non_matching_result() -> TestResult<()> {
877        let matcher = StrMatcher::with_default_config("A string");
878        verify_that!(
879            matcher.describe(MatcherResult::NoMatch),
880            displays_as(eq("isn't equal to \"A string\""))
881        )
882    }
883
884    #[test]
885    fn describes_itself_for_matching_result_ignoring_leading_whitespace() -> TestResult<()> {
886        let matcher = StrMatcher::with_default_config("A string").ignoring_leading_whitespace();
887        verify_that!(
888            matcher.describe(MatcherResult::Match),
889            displays_as(eq("is equal to \"A string\" (ignoring leading whitespace)"))
890        )
891    }
892
893    #[test]
894    fn describes_itself_for_non_matching_result_ignoring_leading_whitespace() -> TestResult<()> {
895        let matcher = StrMatcher::with_default_config("A string").ignoring_leading_whitespace();
896        verify_that!(
897            matcher.describe(MatcherResult::NoMatch),
898            displays_as(eq("isn't equal to \"A string\" (ignoring leading whitespace)"))
899        )
900    }
901
902    #[test]
903    fn describes_itself_for_matching_result_ignoring_trailing_whitespace() -> TestResult<()> {
904        let matcher = StrMatcher::with_default_config("A string").ignoring_trailing_whitespace();
905        verify_that!(
906            matcher.describe(MatcherResult::Match),
907            displays_as(eq("is equal to \"A string\" (ignoring trailing whitespace)"))
908        )
909    }
910
911    #[test]
912    fn describes_itself_for_matching_result_ignoring_leading_and_trailing_whitespace()
913    -> TestResult<()> {
914        let matcher = StrMatcher::with_default_config("A string").ignoring_outer_whitespace();
915        verify_that!(
916            matcher.describe(MatcherResult::Match),
917            displays_as(eq("is equal to \"A string\" (ignoring leading and trailing whitespace)"))
918        )
919    }
920
921    #[test]
922    fn describes_itself_for_matching_result_ignoring_ascii_case() -> TestResult<()> {
923        let matcher = StrMatcher::with_default_config("A string").ignoring_ascii_case();
924        verify_that!(
925            matcher.describe(MatcherResult::Match),
926            displays_as(eq("is equal to \"A string\" (ignoring ASCII case)"))
927        )
928    }
929
930    #[test]
931    fn describes_itself_for_matching_result_ignoring_ascii_case_and_leading_whitespace()
932    -> TestResult<()> {
933        let matcher = StrMatcher::with_default_config("A string")
934            .ignoring_leading_whitespace()
935            .ignoring_ascii_case();
936        verify_that!(
937            matcher.describe(MatcherResult::Match),
938            displays_as(eq(
939                "is equal to \"A string\" (ignoring leading whitespace, ignoring ASCII case)"
940            ))
941        )
942    }
943
944    #[test]
945    fn describes_itself_for_matching_result_in_contains_mode() -> TestResult<()> {
946        let matcher = contains_substring("A string");
947        verify_that!(
948            matcher.describe(MatcherResult::Match),
949            displays_as(eq("contains a substring \"A string\""))
950        )
951    }
952
953    #[test]
954    fn describes_itself_for_non_matching_result_in_contains_mode() -> TestResult<()> {
955        let matcher = contains_substring("A string");
956        verify_that!(
957            matcher.describe(MatcherResult::NoMatch),
958            displays_as(eq("does not contain a substring \"A string\""))
959        )
960    }
961
962    #[test]
963    fn describes_itself_with_count_number() -> TestResult<()> {
964        let matcher = contains_substring("A string").times(gt(2));
965        verify_that!(
966            matcher.describe(MatcherResult::Match),
967            displays_as(eq("contains a substring \"A string\" (count is greater than 2)"))
968        )
969    }
970
971    #[test]
972    fn describes_itself_for_matching_result_in_starts_with_mode() -> TestResult<()> {
973        let matcher = starts_with("A string");
974        verify_that!(
975            matcher.describe(MatcherResult::Match),
976            displays_as(eq("starts with prefix \"A string\""))
977        )
978    }
979
980    #[test]
981    fn describes_itself_for_non_matching_result_in_starts_with_mode() -> TestResult<()> {
982        let matcher = starts_with("A string");
983        verify_that!(
984            matcher.describe(MatcherResult::NoMatch),
985            displays_as(eq("does not start with \"A string\""))
986        )
987    }
988
989    #[test]
990    fn describes_itself_for_matching_result_in_ends_with_mode() -> TestResult<()> {
991        let matcher = ends_with("A string");
992        verify_that!(
993            matcher.describe(MatcherResult::Match),
994            displays_as(eq("ends with suffix \"A string\""))
995        )
996    }
997
998    #[test]
999    fn describes_itself_for_non_matching_result_in_ends_with_mode() -> TestResult<()> {
1000        let matcher = ends_with("A string");
1001        verify_that!(
1002            matcher.describe(MatcherResult::NoMatch),
1003            displays_as(eq("does not end with \"A string\""))
1004        )
1005    }
1006
1007    #[test]
1008    fn match_explanation_contains_diff_of_strings_if_more_than_one_line() -> TestResult<()> {
1009        let result = verify_that!(
1010            indoc!(
1011                "
1012                    First line
1013                    Second line
1014                    Third line
1015                "
1016            ),
1017            starts_with(indoc!(
1018                "
1019                    First line
1020                    Second lines
1021                    Third line
1022                "
1023            ))
1024        );
1025
1026        verify_that!(
1027            result,
1028            err(displays_as(contains_substring(
1029                "\
1030   First line
1031  -Second line
1032  +Second lines
1033   Third line"
1034            )))
1035        )
1036    }
1037
1038    #[test]
1039    fn match_explanation_for_starts_with_ignores_trailing_lines_in_actual_string() -> TestResult<()>
1040    {
1041        let result = verify_that!(
1042            indoc!(
1043                "
1044                    First line
1045                    Second line
1046                    Third line
1047                    Fourth line
1048                "
1049            ),
1050            starts_with(indoc!(
1051                "
1052                    First line
1053                    Second lines
1054                    Third line
1055                "
1056            ))
1057        );
1058
1059        verify_that!(
1060            result,
1061            err(displays_as(contains_substring(
1062                "
1063   First line
1064  -Second line
1065  +Second lines
1066   Third line
1067   <---- remaining lines omitted ---->"
1068            )))
1069        )
1070    }
1071
1072    #[test]
1073    fn match_explanation_for_starts_with_includes_both_versions_of_differing_last_line()
1074    -> TestResult<()> {
1075        let result = verify_that!(
1076            indoc!(
1077                "
1078                    First line
1079                    Second line
1080                    Third line
1081                "
1082            ),
1083            starts_with(indoc!(
1084                "
1085                    First line
1086                    Second lines
1087                "
1088            ))
1089        );
1090
1091        verify_that!(
1092            result,
1093            err(displays_as(contains_substring(
1094                "\
1095   First line
1096  -Second line
1097  +Second lines
1098   <---- remaining lines omitted ---->"
1099            )))
1100        )
1101    }
1102
1103    #[test]
1104    fn match_explanation_for_ends_with_ignores_leading_lines_in_actual_string() -> TestResult<()> {
1105        let result = verify_that!(
1106            indoc!(
1107                "
1108                    First line
1109                    Second line
1110                    Third line
1111                    Fourth line
1112                "
1113            ),
1114            ends_with(indoc!(
1115                "
1116                    Second line
1117                    Third lines
1118                    Fourth line
1119                "
1120            ))
1121        );
1122
1123        verify_that!(
1124            result,
1125            err(displays_as(contains_substring(
1126                "
1127  Difference(-actual / +expected):
1128   <---- remaining lines omitted ---->
1129   Second line
1130  -Third line
1131  +Third lines
1132   Fourth line"
1133            )))
1134        )
1135    }
1136
1137    #[test]
1138    fn match_explanation_for_contains_substring_ignores_outer_lines_in_actual_string()
1139    -> TestResult<()> {
1140        let result = verify_that!(
1141            indoc!(
1142                "
1143                    First line
1144                    Second line
1145                    Third line
1146                    Fourth line
1147                    Fifth line
1148                "
1149            ),
1150            contains_substring(indoc!(
1151                "
1152                    Second line
1153                    Third lines
1154                    Fourth line
1155                "
1156            ))
1157        );
1158
1159        verify_that!(
1160            result,
1161            err(displays_as(contains_substring(
1162                "
1163  Difference(-actual / +expected):
1164   <---- remaining lines omitted ---->
1165   Second line
1166  -Third line
1167  +Third lines
1168   Fourth line
1169   <---- remaining lines omitted ---->"
1170            )))
1171        )
1172    }
1173
1174    #[test]
1175    fn match_explanation_for_contains_substring_shows_diff_when_first_and_last_line_are_incomplete()
1176    -> TestResult<()> {
1177        let result = verify_that!(
1178            indoc!(
1179                "
1180                    First line
1181                    Second line
1182                    Third line
1183                    Fourth line
1184                    Fifth line
1185                "
1186            ),
1187            contains_substring(indoc!(
1188                "
1189                    line
1190                    Third line
1191                    Foorth line
1192                    Fifth"
1193            ))
1194        );
1195
1196        verify_that!(
1197            result,
1198            err(displays_as(contains_substring(
1199                "
1200  Difference(-actual / +expected):
1201   <---- remaining lines omitted ---->
1202  -Second line
1203  +line
1204   Third line
1205  -Fourth line
1206  +Foorth line
1207  -Fifth line
1208  +Fifth
1209   <---- remaining lines omitted ---->"
1210            )))
1211        )
1212    }
1213
1214    #[test]
1215    fn match_explanation_for_eq_does_not_ignore_trailing_lines_in_actual_string() -> TestResult<()>
1216    {
1217        let result = verify_that!(
1218            indoc!(
1219                "
1220                    First line
1221                    Second line
1222                    Third line
1223                    Fourth line
1224                "
1225            ),
1226            eq(indoc!(
1227                "
1228                    First line
1229                    Second lines
1230                    Third line
1231                "
1232            ))
1233        );
1234
1235        verify_that!(
1236            result,
1237            err(displays_as(contains_substring(
1238                "\
1239   First line
1240  -Second line
1241  +Second lines
1242   Third line
1243  -Fourth line"
1244            )))
1245        )
1246    }
1247
1248    #[test]
1249    fn match_explanation_does_not_show_diff_if_actual_value_is_single_line() -> TestResult<()> {
1250        let result = verify_that!(
1251            "First line",
1252            starts_with(indoc!(
1253                "
1254                    Second line
1255                    Third line
1256                "
1257            ))
1258        );
1259
1260        verify_that!(
1261            result,
1262            err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
1263        )
1264    }
1265
1266    #[test]
1267    fn match_explanation_does_not_show_diff_if_expected_value_is_single_line() -> TestResult<()> {
1268        let result = verify_that!(
1269            indoc!(
1270                "
1271                    First line
1272                    Second line
1273                    Third line
1274                "
1275            ),
1276            starts_with("Second line")
1277        );
1278
1279        verify_that!(
1280            result,
1281            err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
1282        )
1283    }
1284
1285    #[test]
1286    fn eq_ignoring_unicode_case_matches_string_with_non_unicode_different_case() -> TestResult<()> {
1287        verify_that!("Κάποια τιμή", eq("ΚΆΠΟΙΑ ΤΙΜΉ").ignoring_unicode_case())
1288    }
1289
1290    #[test]
1291    fn eq_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1292        verify_that!("Κάποια τιμή", not(eq("Some Value").ignoring_unicode_case()))
1293    }
1294
1295    #[test]
1296    fn starts_with_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1297    -> TestResult<()> {
1298        verify_that!("Κάποια τιμή", starts_with("ΚΆΠΟΙΑ").ignoring_unicode_case())
1299    }
1300
1301    #[test]
1302    fn starts_with_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1303        verify_that!("Κάποια τιμή", not(starts_with("Some Value").ignoring_unicode_case()))
1304    }
1305
1306    #[test]
1307    fn ends_with_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1308    -> TestResult<()> {
1309        verify_that!("Κάποια τιμή", ends_with("ΤΙΜΉ").ignoring_unicode_case())
1310    }
1311
1312    #[test]
1313    fn ends_with_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1314        verify_that!("Κάποια τιμή", not(ends_with("Some Value").ignoring_unicode_case()))
1315    }
1316
1317    #[test]
1318    fn contains_substring_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1319    -> TestResult<()> {
1320        verify_that!("Κάποια τιμή", contains_substring("ΠΟΙΑ ").ignoring_unicode_case())
1321    }
1322
1323    #[test]
1324    fn contains_substring_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()>
1325    {
1326        verify_that!("Κάποια τιμή", not(contains_substring("me Val").ignoring_unicode_case()))
1327    }
1328}