test-that 0.5.0

A rich assertion and matcher library based on GoogleTest
Documentation
// Copyright 2023 Google LLC
// Copyright 2026 Bradford Hovinen <bradford@hovinen.me>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Matches a string whose number of Unicode scalars matches `expected`.
///
/// In other words, the argument must match the output of
/// [`actual_string.chars().count()`][std::str::Chars].
///
/// This can have surprising effects when what appears to be a single character
/// is composed of multiple Unicode scalars. See [Rust documentation on
/// character
/// representation](https://doc.rust-lang.org/std/primitive.char.html#representation)
/// for more information.
///
/// This matches against owned strings and string slices.
///
/// ```
/// # use test_that::prelude::*;
/// # fn should_pass() -> TestResult<()> {
/// let string_slice = "A string";
/// verify_that!(string_slice, char_count(eq(8)))?;
/// let non_ascii_string_slice = "Ä ſtřiɲğ";
/// verify_that!(non_ascii_string_slice, char_count(eq(8)))?;
/// let owned_string = String::from("A string");
/// verify_that!(owned_string, char_count(eq(8)))?;
/// #     Ok(())
/// # }
/// # should_pass().unwrap();
/// ```
///
/// The parameter `expected` can be any integer numeric matcher.
///
/// ```
/// # use test_that::prelude::*;
/// # fn should_pass() -> TestResult<()> {
/// let string_slice = "A string";
/// verify_that!(string_slice, char_count(gt(4)))?;
/// #     Ok(())
/// # }
/// # should_pass().unwrap();
/// ```
pub fn char_count<E>(expected: E) -> __internal::CharCountMatcher<E> {
    __internal::CharCountMatcher { expected }
}

pub mod __internal {
    use crate::{
        description::Description,
        matcher::{Describable, Matcher, MatcherResult},
    };
    use core::fmt::Debug;

    #[doc(hidden)]
    pub struct CharCountMatcher<E> {
        pub(super) expected: E,
    }

    impl<T: Debug + ?Sized + AsRef<str>, E: Matcher<usize>> Matcher<T> for CharCountMatcher<E> {
        fn matches(&self, actual: &T) -> MatcherResult {
            self.expected.matches(&actual.as_ref().chars().count())
        }

        fn explain_match(&self, actual: &T) -> Description {
            let actual_size = actual.as_ref().chars().count();
            format!(
                "which has character count {}, {}",
                actual_size,
                self.expected.explain_match(&actual_size)
            )
            .into()
        }
    }

    impl<E: Matcher<usize>> Describable for CharCountMatcher<E> {
        fn describe(&self, matcher_result: MatcherResult) -> Description {
            match matcher_result {
                MatcherResult::Match => format!(
                    "has character count, which {}",
                    self.expected.describe(MatcherResult::Match)
                )
                .into(),
                MatcherResult::NoMatch => format!(
                    "has character count, which {}",
                    self.expected.describe(MatcherResult::NoMatch)
                )
                .into(),
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::char_count;
    use crate::description::Description;
    use crate::matcher::{Describable, Matcher, MatcherResult};
    use crate::prelude::*;
    use alloc::string::String;
    use core::fmt::Debug;
    use core::marker::PhantomData;
    use indoc::indoc;

    #[test]
    fn char_count_matches_string_slice() -> TestResult<()> {
        let value = "abcd";
        verify_that!(value, char_count(eq(4)))
    }

    #[test]
    fn char_count_matches_owned_string() -> TestResult<()> {
        let value = String::from("abcd");
        verify_that!(value, char_count(eq(4)))
    }

    #[test]
    fn char_count_counts_non_ascii_characters_correctly() -> TestResult<()> {
        let value = "äöüß";
        verify_that!(value, char_count(eq(4)))
    }

    #[test]
    fn char_count_explains_match() -> TestResult<()> {
        struct TestMatcher<T>(PhantomData<T>);
        impl<T: Debug> Matcher<T> for TestMatcher<T> {
            fn matches(&self, _: &T) -> MatcherResult {
                false.into()
            }

            fn explain_match(&self, _: &T) -> Description {
                "called explain_match".into()
            }
        }
        impl<T: Debug> Describable for TestMatcher<T> {
            fn describe(&self, _: MatcherResult) -> Description {
                "called described".into()
            }
        }
        verify_that!(
            char_count(TestMatcher(Default::default())).explain_match(&"A string"),
            displays_as(eq("which has character count 8, called explain_match"))
        )
    }

    #[test]
    fn char_count_has_correct_failure_message() -> TestResult<()> {
        let result = verify_that!("äöüß", char_count(eq(3)));
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc!(
                r#"
                Value of: "äöüß"
                Expected: has character count, which is equal to 3
                Actual: "äöüß",
                  which has character count 4, which isn't equal to 3"#
            ))))
        )
    }
}