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
507            // the substring to be matched, of which there is one more than the
508            // number of 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
567            // ignore_leading_whitespace.
568            return default_explanation;
569        }
570
571        if self.ignore_trailing_whitespace {
572            // TODO - b/283448414 : Support StrMatcher with
573            // ignore_trailing_whitespace.
574            return default_explanation;
575        }
576
577        if self.times.is_some() {
578            // TODO - b/283448414 : Support StrMatcher with times.
579            return default_explanation;
580        }
581        if matches!(self.case_policy, CasePolicy::IgnoreAscii) {
582            // TODO - b/283448414 : Support StrMatcher with ignore ascii case
583            // policy.
584            return default_explanation;
585        }
586        if self.do_strings_match(expected, actual) {
587            // TODO - b/283448414 : Consider supporting debug difference if the
588            // strings match. This can be useful when a small contains is found
589            // in a long string.
590            return default_explanation;
591        }
592
593        let diff = match self.mode {
594            MatchMode::Equals | MatchMode::StartsWith | MatchMode::Contains => {
595                // TODO(b/287632452): Also consider improving the output in
596                // MatchMode::Contains when the substring begins
597                // or ends in the middle of a line of the actual
598                // value.
599                create_diff(actual, expected, self.mode.to_diff_mode())
600            }
601            MatchMode::EndsWith => create_diff_reversed(actual, expected, self.mode.to_diff_mode()),
602        };
603
604        format!("{default_explanation}\n{diff}").into()
605    }
606
607    fn ignoring_leading_whitespace(self) -> Self {
608        Self { ignore_leading_whitespace: true, ..self }
609    }
610
611    fn ignoring_trailing_whitespace(self) -> Self {
612        Self { ignore_trailing_whitespace: true, ..self }
613    }
614
615    fn ignoring_outer_whitespace(self) -> Self {
616        Self { ignore_leading_whitespace: true, ignore_trailing_whitespace: true, ..self }
617    }
618
619    fn ignoring_ascii_case(self) -> Self {
620        Self { case_policy: CasePolicy::IgnoreAscii, ..self }
621    }
622
623    fn ignoring_unicode_case(self) -> Self {
624        Self { case_policy: CasePolicy::IgnoreUnicode, ..self }
625    }
626
627    fn times(self, times: impl Matcher<usize> + 'static) -> Self {
628        Self { times: Some(Box::new(times)), ..self }
629    }
630}
631
632impl Default for Configuration {
633    fn default() -> Self {
634        Self {
635            mode: MatchMode::Equals,
636            ignore_leading_whitespace: false,
637            ignore_trailing_whitespace: false,
638            case_policy: CasePolicy::Respect,
639            times: None,
640        }
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use super::{StrMatcher, StrMatcherConfigurator, contains_substring, ends_with, starts_with};
647    use crate::matcher::{Describable as _, MatcherResult};
648    use crate::prelude::*;
649    use alloc::string::ToString;
650    use indoc::indoc;
651
652    #[test]
653    fn matches_string_reference_with_equal_string_reference() -> TestResult<()> {
654        let matcher = StrMatcher::with_default_config("A string");
655        verify_that!("A string", matcher)
656    }
657
658    #[test]
659    fn does_not_match_string_reference_with_non_equal_string_reference() -> TestResult<()> {
660        let matcher = StrMatcher::with_default_config("Another string");
661        verify_that!("A string", not(matcher))
662    }
663
664    #[test]
665    fn matches_owned_string_with_string_reference() -> TestResult<()> {
666        let matcher = StrMatcher::with_default_config("A string");
667        let value = "A string".to_string();
668        verify_that!(value, matcher)
669    }
670
671    #[test]
672    fn matches_owned_string_reference_with_string_reference() -> TestResult<()> {
673        let matcher = StrMatcher::with_default_config("A string");
674        let value = "A string".to_string();
675        verify_that!(&value, matcher)
676    }
677
678    #[test]
679    fn ignores_leading_whitespace_in_expected_when_requested() -> TestResult<()> {
680        let matcher = StrMatcher::with_default_config(" \n\tA string");
681        verify_that!("A string", matcher.ignoring_leading_whitespace())
682    }
683
684    #[test]
685    fn ignores_leading_whitespace_in_actual_when_requested() -> TestResult<()> {
686        let matcher = StrMatcher::with_default_config("A string");
687        verify_that!(" \n\tA string", matcher.ignoring_leading_whitespace())
688    }
689
690    #[test]
691    fn does_not_match_unequal_remaining_string_when_ignoring_leading_whitespace() -> TestResult<()>
692    {
693        let matcher = StrMatcher::with_default_config(" \n\tAnother string");
694        verify_that!("A string", not(matcher.ignoring_leading_whitespace()))
695    }
696
697    #[test]
698    fn remains_sensitive_to_trailing_whitespace_when_ignoring_leading_whitespace() -> TestResult<()>
699    {
700        let matcher = StrMatcher::with_default_config("A string \n\t");
701        verify_that!("A string", not(matcher.ignoring_leading_whitespace()))
702    }
703
704    #[test]
705    fn ignores_trailing_whitespace_in_expected_when_requested() -> TestResult<()> {
706        let matcher = StrMatcher::with_default_config("A string \n\t");
707        verify_that!("A string", matcher.ignoring_trailing_whitespace())
708    }
709
710    #[test]
711    fn ignores_trailing_whitespace_in_actual_when_requested() -> TestResult<()> {
712        let matcher = StrMatcher::with_default_config("A string");
713        verify_that!("A string \n\t", matcher.ignoring_trailing_whitespace())
714    }
715
716    #[test]
717    fn does_not_match_unequal_remaining_string_when_ignoring_trailing_whitespace() -> TestResult<()>
718    {
719        let matcher = StrMatcher::with_default_config("Another string \n\t");
720        verify_that!("A string", not(matcher.ignoring_trailing_whitespace()))
721    }
722
723    #[test]
724    fn remains_sensitive_to_leading_whitespace_when_ignoring_trailing_whitespace() -> TestResult<()>
725    {
726        let matcher = StrMatcher::with_default_config(" \n\tA string");
727        verify_that!("A string", not(matcher.ignoring_trailing_whitespace()))
728    }
729
730    #[test]
731    fn ignores_leading_and_trailing_whitespace_in_expected_when_requested() -> TestResult<()> {
732        let matcher = StrMatcher::with_default_config(" \n\tA string \n\t");
733        verify_that!("A string", matcher.ignoring_outer_whitespace())
734    }
735
736    #[test]
737    fn ignores_leading_and_trailing_whitespace_in_actual_when_requested() -> TestResult<()> {
738        let matcher = StrMatcher::with_default_config("A string");
739        verify_that!(" \n\tA string \n\t", matcher.ignoring_outer_whitespace())
740    }
741
742    #[test]
743    fn respects_ascii_case_by_default() -> TestResult<()> {
744        let matcher = StrMatcher::with_default_config("A string");
745        verify_that!("A STRING", not(matcher))
746    }
747
748    #[test]
749    fn ignores_ascii_case_when_requested() -> TestResult<()> {
750        let matcher = StrMatcher::with_default_config("A string");
751        verify_that!("A STRING", matcher.ignoring_ascii_case())
752    }
753
754    #[test]
755    fn allows_ignoring_leading_whitespace_from_eq() -> TestResult<()> {
756        verify_that!("A string", eq(" \n\tA string").ignoring_leading_whitespace())
757    }
758
759    #[test]
760    fn allows_ignoring_trailing_whitespace_from_eq() -> TestResult<()> {
761        verify_that!("A string", eq("A string \n\t").ignoring_trailing_whitespace())
762    }
763
764    #[test]
765    fn allows_ignoring_outer_whitespace_from_eq() -> TestResult<()> {
766        verify_that!("A string", eq(" \n\tA string \n\t").ignoring_outer_whitespace())
767    }
768
769    #[test]
770    fn allows_ignoring_ascii_case_from_eq() -> TestResult<()> {
771        verify_that!("A string", eq("A STRING").ignoring_ascii_case())
772    }
773
774    #[test]
775    fn allows_ignoring_ascii_case_from_eq_deref_of_str_slice() -> TestResult<()> {
776        verify_that!("A string", eq_deref_of("A STRING").ignoring_ascii_case())
777    }
778
779    #[test]
780    fn allows_ignoring_ascii_case_from_eq_deref_of_owned_string() -> TestResult<()> {
781        verify_that!("A string", eq_deref_of("A STRING".to_string()).ignoring_ascii_case())
782    }
783
784    #[test]
785    fn matches_string_containing_expected_value_in_contains_mode() -> TestResult<()> {
786        verify_that!("Some string", contains_substring("str"))
787    }
788
789    #[test]
790    fn matches_string_containing_expected_value_in_contains_mode_while_ignoring_ascii_case()
791    -> TestResult<()> {
792        verify_that!("Some string", contains_substring("STR").ignoring_ascii_case())
793    }
794
795    #[test]
796    fn contains_substring_matches_correct_number_of_substrings() -> TestResult<()> {
797        verify_that!("Some string", contains_substring("str").times(eq(1)))
798    }
799
800    #[test]
801    fn contains_substring_does_not_match_incorrect_number_of_substrings() -> TestResult<()> {
802        verify_that!("Some string\nSome string", not(contains_substring("string").times(eq(1))))
803    }
804
805    #[test]
806    fn contains_substring_does_not_match_when_substrings_overlap() -> TestResult<()> {
807        verify_that!("ababab", not(contains_substring("abab").times(eq(2))))
808    }
809
810    #[test]
811    fn starts_with_matches_string_reference_with_prefix() -> TestResult<()> {
812        verify_that!("Some value", starts_with("Some"))
813    }
814
815    #[test]
816    fn starts_with_matches_string_reference_with_prefix_ignoring_ascii_case() -> TestResult<()> {
817        verify_that!("Some value", starts_with("SOME").ignoring_ascii_case())
818    }
819
820    #[test]
821    fn starts_with_does_not_match_wrong_prefix_ignoring_ascii_case() -> TestResult<()> {
822        verify_that!("Some value", not(starts_with("OTHER").ignoring_ascii_case()))
823    }
824
825    #[test]
826    fn ends_with_does_not_match_short_string_ignoring_ascii_case() -> TestResult<()> {
827        verify_that!("Some", not(starts_with("OTHER").ignoring_ascii_case()))
828    }
829
830    #[test]
831    fn starts_with_does_not_match_string_without_prefix() -> TestResult<()> {
832        verify_that!("Some value", not(starts_with("Another")))
833    }
834
835    #[test]
836    fn starts_with_does_not_match_string_with_substring_not_at_beginning() -> TestResult<()> {
837        verify_that!("Some value", not(starts_with("value")))
838    }
839
840    #[test]
841    fn ends_with_matches_string_reference_with_suffix() -> TestResult<()> {
842        verify_that!("Some value", ends_with("value"))
843    }
844
845    #[test]
846    fn ends_with_matches_string_reference_with_suffix_ignoring_ascii_case() -> TestResult<()> {
847        verify_that!("Some value", ends_with("VALUE").ignoring_ascii_case())
848    }
849
850    #[test]
851    fn ends_with_does_not_match_wrong_suffix_ignoring_ascii_case() -> TestResult<()> {
852        verify_that!("Some value", not(ends_with("OTHER").ignoring_ascii_case()))
853    }
854
855    #[test]
856    fn ends_with_does_not_match_too_short_string_ignoring_ascii_case() -> TestResult<()> {
857        verify_that!("Some", not(ends_with("OTHER").ignoring_ascii_case()))
858    }
859
860    #[test]
861    fn ends_with_does_not_match_string_without_suffix() -> TestResult<()> {
862        verify_that!("Some value", not(ends_with("other value")))
863    }
864
865    #[test]
866    fn ends_with_does_not_match_string_with_substring_not_at_end() -> TestResult<()> {
867        verify_that!("Some value", not(ends_with("Some")))
868    }
869
870    #[test]
871    fn describes_itself_for_matching_result() -> TestResult<()> {
872        let matcher = StrMatcher::with_default_config("A string");
873        verify_that!(
874            matcher.describe(MatcherResult::Match),
875            displays_as(eq("is equal to \"A string\""))
876        )
877    }
878
879    #[test]
880    fn describes_itself_for_non_matching_result() -> TestResult<()> {
881        let matcher = StrMatcher::with_default_config("A string");
882        verify_that!(
883            matcher.describe(MatcherResult::NoMatch),
884            displays_as(eq("isn't equal to \"A string\""))
885        )
886    }
887
888    #[test]
889    fn describes_itself_for_matching_result_ignoring_leading_whitespace() -> TestResult<()> {
890        let matcher = StrMatcher::with_default_config("A string").ignoring_leading_whitespace();
891        verify_that!(
892            matcher.describe(MatcherResult::Match),
893            displays_as(eq("is equal to \"A string\" (ignoring leading whitespace)"))
894        )
895    }
896
897    #[test]
898    fn describes_itself_for_non_matching_result_ignoring_leading_whitespace() -> TestResult<()> {
899        let matcher = StrMatcher::with_default_config("A string").ignoring_leading_whitespace();
900        verify_that!(
901            matcher.describe(MatcherResult::NoMatch),
902            displays_as(eq("isn't equal to \"A string\" (ignoring leading whitespace)"))
903        )
904    }
905
906    #[test]
907    fn describes_itself_for_matching_result_ignoring_trailing_whitespace() -> TestResult<()> {
908        let matcher = StrMatcher::with_default_config("A string").ignoring_trailing_whitespace();
909        verify_that!(
910            matcher.describe(MatcherResult::Match),
911            displays_as(eq("is equal to \"A string\" (ignoring trailing whitespace)"))
912        )
913    }
914
915    #[test]
916    fn describes_itself_for_matching_result_ignoring_leading_and_trailing_whitespace()
917    -> TestResult<()> {
918        let matcher = StrMatcher::with_default_config("A string").ignoring_outer_whitespace();
919        verify_that!(
920            matcher.describe(MatcherResult::Match),
921            displays_as(eq("is equal to \"A string\" (ignoring leading and trailing whitespace)"))
922        )
923    }
924
925    #[test]
926    fn describes_itself_for_matching_result_ignoring_ascii_case() -> TestResult<()> {
927        let matcher = StrMatcher::with_default_config("A string").ignoring_ascii_case();
928        verify_that!(
929            matcher.describe(MatcherResult::Match),
930            displays_as(eq("is equal to \"A string\" (ignoring ASCII case)"))
931        )
932    }
933
934    #[test]
935    fn describes_itself_for_matching_result_ignoring_ascii_case_and_leading_whitespace()
936    -> TestResult<()> {
937        let matcher = StrMatcher::with_default_config("A string")
938            .ignoring_leading_whitespace()
939            .ignoring_ascii_case();
940        verify_that!(
941            matcher.describe(MatcherResult::Match),
942            displays_as(eq(
943                "is equal to \"A string\" (ignoring leading whitespace, ignoring ASCII case)"
944            ))
945        )
946    }
947
948    #[test]
949    fn describes_itself_for_matching_result_in_contains_mode() -> TestResult<()> {
950        let matcher = contains_substring("A string");
951        verify_that!(
952            matcher.describe(MatcherResult::Match),
953            displays_as(eq("contains a substring \"A string\""))
954        )
955    }
956
957    #[test]
958    fn describes_itself_for_non_matching_result_in_contains_mode() -> TestResult<()> {
959        let matcher = contains_substring("A string");
960        verify_that!(
961            matcher.describe(MatcherResult::NoMatch),
962            displays_as(eq("does not contain a substring \"A string\""))
963        )
964    }
965
966    #[test]
967    fn describes_itself_with_count_number() -> TestResult<()> {
968        let matcher = contains_substring("A string").times(gt(2));
969        verify_that!(
970            matcher.describe(MatcherResult::Match),
971            displays_as(eq("contains a substring \"A string\" (count is greater than 2)"))
972        )
973    }
974
975    #[test]
976    fn describes_itself_for_matching_result_in_starts_with_mode() -> TestResult<()> {
977        let matcher = starts_with("A string");
978        verify_that!(
979            matcher.describe(MatcherResult::Match),
980            displays_as(eq("starts with prefix \"A string\""))
981        )
982    }
983
984    #[test]
985    fn describes_itself_for_non_matching_result_in_starts_with_mode() -> TestResult<()> {
986        let matcher = starts_with("A string");
987        verify_that!(
988            matcher.describe(MatcherResult::NoMatch),
989            displays_as(eq("does not start with \"A string\""))
990        )
991    }
992
993    #[test]
994    fn describes_itself_for_matching_result_in_ends_with_mode() -> TestResult<()> {
995        let matcher = ends_with("A string");
996        verify_that!(
997            matcher.describe(MatcherResult::Match),
998            displays_as(eq("ends with suffix \"A string\""))
999        )
1000    }
1001
1002    #[test]
1003    fn describes_itself_for_non_matching_result_in_ends_with_mode() -> TestResult<()> {
1004        let matcher = ends_with("A string");
1005        verify_that!(
1006            matcher.describe(MatcherResult::NoMatch),
1007            displays_as(eq("does not end with \"A string\""))
1008        )
1009    }
1010
1011    #[test]
1012    fn match_explanation_contains_diff_of_strings_if_more_than_one_line() -> TestResult<()> {
1013        let result = verify_that!(
1014            indoc!(
1015                "
1016                    First line
1017                    Second line
1018                    Third line
1019                "
1020            ),
1021            starts_with(indoc!(
1022                "
1023                    First line
1024                    Second lines
1025                    Third line
1026                "
1027            ))
1028        );
1029
1030        verify_that!(
1031            result,
1032            err(displays_as(contains_substring(
1033                "\
1034   First line
1035  -Second line
1036  +Second lines
1037   Third line"
1038            )))
1039        )
1040    }
1041
1042    #[test]
1043    fn match_explanation_for_starts_with_ignores_trailing_lines_in_actual_string() -> TestResult<()>
1044    {
1045        let result = verify_that!(
1046            indoc!(
1047                "
1048                    First line
1049                    Second line
1050                    Third line
1051                    Fourth line
1052                "
1053            ),
1054            starts_with(indoc!(
1055                "
1056                    First line
1057                    Second lines
1058                    Third line
1059                "
1060            ))
1061        );
1062
1063        verify_that!(
1064            result,
1065            err(displays_as(contains_substring(
1066                "
1067   First line
1068  -Second line
1069  +Second lines
1070   Third line
1071   <---- remaining lines omitted ---->"
1072            )))
1073        )
1074    }
1075
1076    #[test]
1077    fn match_explanation_for_starts_with_includes_both_versions_of_differing_last_line()
1078    -> TestResult<()> {
1079        let result = verify_that!(
1080            indoc!(
1081                "
1082                    First line
1083                    Second line
1084                    Third line
1085                "
1086            ),
1087            starts_with(indoc!(
1088                "
1089                    First line
1090                    Second lines
1091                "
1092            ))
1093        );
1094
1095        verify_that!(
1096            result,
1097            err(displays_as(contains_substring(
1098                "\
1099   First line
1100  -Second line
1101  +Second lines
1102   <---- remaining lines omitted ---->"
1103            )))
1104        )
1105    }
1106
1107    #[test]
1108    fn match_explanation_for_ends_with_ignores_leading_lines_in_actual_string() -> TestResult<()> {
1109        let result = verify_that!(
1110            indoc!(
1111                "
1112                    First line
1113                    Second line
1114                    Third line
1115                    Fourth line
1116                "
1117            ),
1118            ends_with(indoc!(
1119                "
1120                    Second line
1121                    Third lines
1122                    Fourth line
1123                "
1124            ))
1125        );
1126
1127        verify_that!(
1128            result,
1129            err(displays_as(contains_substring(
1130                "
1131  Difference(-actual / +expected):
1132   <---- remaining lines omitted ---->
1133   Second line
1134  -Third line
1135  +Third lines
1136   Fourth line"
1137            )))
1138        )
1139    }
1140
1141    #[test]
1142    fn match_explanation_for_contains_substring_ignores_outer_lines_in_actual_string()
1143    -> TestResult<()> {
1144        let result = verify_that!(
1145            indoc!(
1146                "
1147                    First line
1148                    Second line
1149                    Third line
1150                    Fourth line
1151                    Fifth line
1152                "
1153            ),
1154            contains_substring(indoc!(
1155                "
1156                    Second line
1157                    Third lines
1158                    Fourth line
1159                "
1160            ))
1161        );
1162
1163        verify_that!(
1164            result,
1165            err(displays_as(contains_substring(
1166                "
1167  Difference(-actual / +expected):
1168   <---- remaining lines omitted ---->
1169   Second line
1170  -Third line
1171  +Third lines
1172   Fourth line
1173   <---- remaining lines omitted ---->"
1174            )))
1175        )
1176    }
1177
1178    #[test]
1179    fn match_explanation_for_contains_substring_shows_diff_when_first_and_last_line_are_incomplete()
1180    -> TestResult<()> {
1181        let result = verify_that!(
1182            indoc!(
1183                "
1184                    First line
1185                    Second line
1186                    Third line
1187                    Fourth line
1188                    Fifth line
1189                "
1190            ),
1191            contains_substring(indoc!(
1192                "
1193                    line
1194                    Third line
1195                    Foorth line
1196                    Fifth"
1197            ))
1198        );
1199
1200        verify_that!(
1201            result,
1202            err(displays_as(contains_substring(
1203                "
1204  Difference(-actual / +expected):
1205   <---- remaining lines omitted ---->
1206  -Second line
1207  +line
1208   Third line
1209  -Fourth line
1210  +Foorth line
1211  -Fifth line
1212  +Fifth
1213   <---- remaining lines omitted ---->"
1214            )))
1215        )
1216    }
1217
1218    #[test]
1219    fn match_explanation_for_eq_does_not_ignore_trailing_lines_in_actual_string() -> TestResult<()>
1220    {
1221        let result = verify_that!(
1222            indoc!(
1223                "
1224                    First line
1225                    Second line
1226                    Third line
1227                    Fourth line
1228                "
1229            ),
1230            eq(indoc!(
1231                "
1232                    First line
1233                    Second lines
1234                    Third line
1235                "
1236            ))
1237        );
1238
1239        verify_that!(
1240            result,
1241            err(displays_as(contains_substring(
1242                "\
1243   First line
1244  -Second line
1245  +Second lines
1246   Third line
1247  -Fourth line"
1248            )))
1249        )
1250    }
1251
1252    #[test]
1253    fn match_explanation_does_not_show_diff_if_actual_value_is_single_line() -> TestResult<()> {
1254        let result = verify_that!(
1255            "First line",
1256            starts_with(indoc!(
1257                "
1258                    Second line
1259                    Third line
1260                "
1261            ))
1262        );
1263
1264        verify_that!(
1265            result,
1266            err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
1267        )
1268    }
1269
1270    #[test]
1271    fn match_explanation_does_not_show_diff_if_expected_value_is_single_line() -> TestResult<()> {
1272        let result = verify_that!(
1273            indoc!(
1274                "
1275                    First line
1276                    Second line
1277                    Third line
1278                "
1279            ),
1280            starts_with("Second line")
1281        );
1282
1283        verify_that!(
1284            result,
1285            err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
1286        )
1287    }
1288
1289    #[test]
1290    fn eq_ignoring_unicode_case_matches_string_with_non_unicode_different_case() -> TestResult<()> {
1291        verify_that!("Κάποια τιμή", eq("ΚΆΠΟΙΑ ΤΙΜΉ").ignoring_unicode_case())
1292    }
1293
1294    #[test]
1295    fn eq_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1296        verify_that!("Κάποια τιμή", not(eq("Some Value").ignoring_unicode_case()))
1297    }
1298
1299    #[test]
1300    fn starts_with_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1301    -> TestResult<()> {
1302        verify_that!("Κάποια τιμή", starts_with("ΚΆΠΟΙΑ").ignoring_unicode_case())
1303    }
1304
1305    #[test]
1306    fn starts_with_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1307        verify_that!("Κάποια τιμή", not(starts_with("Some Value").ignoring_unicode_case()))
1308    }
1309
1310    #[test]
1311    fn ends_with_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1312    -> TestResult<()> {
1313        verify_that!("Κάποια τιμή", ends_with("ΤΙΜΉ").ignoring_unicode_case())
1314    }
1315
1316    #[test]
1317    fn ends_with_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()> {
1318        verify_that!("Κάποια τιμή", not(ends_with("Some Value").ignoring_unicode_case()))
1319    }
1320
1321    #[test]
1322    fn contains_substring_ignoring_unicode_case_matches_string_with_non_unicode_different_case()
1323    -> TestResult<()> {
1324        verify_that!("Κάποια τιμή", contains_substring("ΠΟΙΑ ").ignoring_unicode_case())
1325    }
1326
1327    #[test]
1328    fn contains_substring_ignoring_unicode_case_does_not_match_different_string() -> TestResult<()>
1329    {
1330        verify_that!("Κάποια τιμή", not(contains_substring("me Val").ignoring_unicode_case()))
1331    }
1332}