From bdb26d35575801a311ab6f4331c475aca1c8097f Mon Sep 17 00:00:00 2001 From: Bradford Hovinen Date: Fri, 30 Jun 2023 17:53:52 +0200 Subject: [PATCH] Introduce a matcher `char_count` to match string slices and owned strings with a particular character count. Previously, there was no matcher on just the length of a string. The existing matcher `len` only works on containers, not strings. The concept of "length of a string" is itself slightly ambiguous -- it could mean byte length, character count, or number of grapheme clusters. This matcher unambiguously counts characters, which should be correct in most cases. --- googletest/crate_docs.md | 2 + googletest/src/matchers/char_count_matcher.rs | 161 ++++++++++++++++++ googletest/src/matchers/mod.rs | 2 + googletest/src/matchers/str_matcher.rs | 8 +- 4 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 googletest/src/matchers/char_count_matcher.rs diff --git a/googletest/crate_docs.md b/googletest/crate_docs.md index 619adfc5..80678c81 100644 --- a/googletest/crate_docs.md +++ b/googletest/crate_docs.md @@ -81,6 +81,7 @@ The following matchers are provided in GoogleTest Rust: | [`all!`] | Anything matched by all given matchers. | | [`anything`] | Any input. | | [`approx_eq`] | A floating point number within a standard tolerance of the argument. | +| [`char_count`] | A string which a Unicode scalar count matching the argument. | | [`container_eq`] | Same as [`eq`], but for containers (with a better mismatch description). | | [`contains`] | A container containing an element matched by the given matcher. | | [`contains_each!`] | A container containing distinct elements each of the arguments match. | @@ -122,6 +123,7 @@ The following matchers are provided in GoogleTest Rust: [`anything`]: matchers::anything [`approx_eq`]: matchers::approx_eq +[`char_count`]: matchers::char_count [`container_eq`]: matchers::container_eq [`contains`]: matchers::contains [`contains_regex`]: matchers::contains_regex diff --git a/googletest/src/matchers/char_count_matcher.rs b/googletest/src/matchers/char_count_matcher.rs new file mode 100644 index 00000000..047843a1 --- /dev/null +++ b/googletest/src/matchers/char_count_matcher.rs @@ -0,0 +1,161 @@ +// Copyright 2023 Google LLC +// +// 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. + +use crate::matcher::{Matcher, MatcherResult}; +use std::{fmt::Debug, marker::PhantomData}; + +/// 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 googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// 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 googletest::prelude::*; +/// # fn should_pass() -> Result<()> { +/// let string_slice = "A string"; +/// verify_that!(string_slice, char_count(gt(4)))?; +/// # Ok(()) +/// # } +/// # should_pass().unwrap(); +/// ``` +pub fn char_count, E: Matcher>( + expected: E, +) -> impl Matcher { + CharLenMatcher { expected, phantom: Default::default() } +} + +struct CharLenMatcher { + expected: E, + phantom: PhantomData, +} + +impl, E: Matcher> Matcher for CharLenMatcher { + type ActualT = T; + + fn matches(&self, actual: &T) -> MatcherResult { + self.expected.matches(&actual.as_ref().chars().count()) + } + + fn describe(&self, matcher_result: MatcherResult) -> String { + match matcher_result { + MatcherResult::Matches => { + format!( + "has character count, which {}", + self.expected.describe(MatcherResult::Matches) + ) + } + MatcherResult::DoesNotMatch => { + format!( + "has character count, which {}", + self.expected.describe(MatcherResult::DoesNotMatch) + ) + } + } + } + + fn explain_match(&self, actual: &T) -> String { + let actual_size = actual.as_ref().chars().count(); + format!( + "which has character count {}, {}", + actual_size, + self.expected.explain_match(&actual_size) + ) + } +} + +#[cfg(test)] +mod tests { + use super::char_count; + use crate::matcher::{Matcher, MatcherResult}; + use crate::prelude::*; + use indoc::indoc; + use std::fmt::Debug; + use std::marker::PhantomData; + + #[test] + fn char_count_matches_string_slice() -> Result<()> { + let value = "abcd"; + verify_that!(value, char_count(eq(4))) + } + + #[test] + fn char_count_matches_owned_string() -> Result<()> { + let value = String::from("abcd"); + verify_that!(value, char_count(eq(4))) + } + + #[test] + fn char_count_counts_non_ascii_characters_correctly() -> Result<()> { + let value = "äöüß"; + verify_that!(value, char_count(eq(4))) + } + + #[test] + fn char_count_explains_match() -> Result<()> { + struct TestMatcher(PhantomData); + impl Matcher for TestMatcher { + type ActualT = T; + + fn matches(&self, _: &T) -> MatcherResult { + false.into() + } + + fn describe(&self, _: MatcherResult) -> String { + "called described".into() + } + + fn explain_match(&self, _: &T) -> String { + "called explain_match".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() -> Result<()> { + 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"# + )))) + ) + } +} diff --git a/googletest/src/matchers/mod.rs b/googletest/src/matchers/mod.rs index 12bdbb09..a46c29ca 100644 --- a/googletest/src/matchers/mod.rs +++ b/googletest/src/matchers/mod.rs @@ -16,6 +16,7 @@ pub mod all_matcher; pub mod anything_matcher; +pub mod char_count_matcher; pub mod conjunction_matcher; pub mod container_eq_matcher; pub mod contains_matcher; @@ -55,6 +56,7 @@ pub mod tuple_matcher; pub mod unordered_elements_are_matcher; pub use anything_matcher::anything; +pub use char_count_matcher::char_count; pub use container_eq_matcher::container_eq; pub use contains_matcher::contains; pub use contains_regex_matcher::contains_regex; diff --git a/googletest/src/matchers/str_matcher.rs b/googletest/src/matchers/str_matcher.rs index d5165fdd..adaaddf4 100644 --- a/googletest/src/matchers/str_matcher.rs +++ b/googletest/src/matchers/str_matcher.rs @@ -14,11 +14,11 @@ use crate::{ matcher::{Matcher, MatcherResult}, - matcher_support::{edit_distance, summarize_diff::{create_diff_reversed, create_diff}}, - matchers::{ - eq_deref_of_matcher::EqDerefOfMatcher, - eq_matcher::{ EqMatcher}, + matcher_support::{ + edit_distance, + summarize_diff::{create_diff, create_diff_reversed}, }, + matchers::{eq_deref_of_matcher::EqDerefOfMatcher, eq_matcher::EqMatcher}, }; use std::borrow::Cow; use std::fmt::Debug;