Skip to content

Commit bdb26d3

Browse files
committed
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.
1 parent c3ba28f commit bdb26d3

File tree

4 files changed

+169
-4
lines changed

4 files changed

+169
-4
lines changed

googletest/crate_docs.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ The following matchers are provided in GoogleTest Rust:
8181
| [`all!`] | Anything matched by all given matchers. |
8282
| [`anything`] | Any input. |
8383
| [`approx_eq`] | A floating point number within a standard tolerance of the argument. |
84+
| [`char_count`] | A string which a Unicode scalar count matching the argument. |
8485
| [`container_eq`] | Same as [`eq`], but for containers (with a better mismatch description). |
8586
| [`contains`] | A container containing an element matched by the given matcher. |
8687
| [`contains_each!`] | A container containing distinct elements each of the arguments match. |
@@ -122,6 +123,7 @@ The following matchers are provided in GoogleTest Rust:
122123

123124
[`anything`]: matchers::anything
124125
[`approx_eq`]: matchers::approx_eq
126+
[`char_count`]: matchers::char_count
125127
[`container_eq`]: matchers::container_eq
126128
[`contains`]: matchers::contains
127129
[`contains_regex`]: matchers::contains_regex
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use crate::matcher::{Matcher, MatcherResult};
16+
use std::{fmt::Debug, marker::PhantomData};
17+
18+
/// Matches a string whose number of Unicode scalars matches `expected`.
19+
///
20+
/// In other words, the argument must match the output of [`actual_string.chars().count()`][std::str::Chars].
21+
///
22+
/// 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.
23+
///
24+
/// This matches against owned strings and string slices.
25+
///
26+
/// ```
27+
/// # use googletest::prelude::*;
28+
/// # fn should_pass() -> Result<()> {
29+
/// let string_slice = "A string";
30+
/// verify_that!(string_slice, char_count(eq(8)))?;
31+
/// let non_ascii_string_slice = "Ä ſtřiɲğ";
32+
/// verify_that!(non_ascii_string_slice, char_count(eq(8)))?;
33+
/// let owned_string = String::from("A string");
34+
/// verify_that!(owned_string, char_count(eq(8)))?;
35+
/// # Ok(())
36+
/// # }
37+
/// # should_pass().unwrap();
38+
/// ```
39+
///
40+
/// The parameter `expected` can be any integer numeric matcher.
41+
///
42+
/// ```
43+
/// # use googletest::prelude::*;
44+
/// # fn should_pass() -> Result<()> {
45+
/// let string_slice = "A string";
46+
/// verify_that!(string_slice, char_count(gt(4)))?;
47+
/// # Ok(())
48+
/// # }
49+
/// # should_pass().unwrap();
50+
/// ```
51+
pub fn char_count<T: Debug + ?Sized + AsRef<str>, E: Matcher<ActualT = usize>>(
52+
expected: E,
53+
) -> impl Matcher<ActualT = T> {
54+
CharLenMatcher { expected, phantom: Default::default() }
55+
}
56+
57+
struct CharLenMatcher<T: ?Sized, E> {
58+
expected: E,
59+
phantom: PhantomData<T>,
60+
}
61+
62+
impl<T: Debug + ?Sized + AsRef<str>, E: Matcher<ActualT = usize>> Matcher for CharLenMatcher<T, E> {
63+
type ActualT = T;
64+
65+
fn matches(&self, actual: &T) -> MatcherResult {
66+
self.expected.matches(&actual.as_ref().chars().count())
67+
}
68+
69+
fn describe(&self, matcher_result: MatcherResult) -> String {
70+
match matcher_result {
71+
MatcherResult::Matches => {
72+
format!(
73+
"has character count, which {}",
74+
self.expected.describe(MatcherResult::Matches)
75+
)
76+
}
77+
MatcherResult::DoesNotMatch => {
78+
format!(
79+
"has character count, which {}",
80+
self.expected.describe(MatcherResult::DoesNotMatch)
81+
)
82+
}
83+
}
84+
}
85+
86+
fn explain_match(&self, actual: &T) -> String {
87+
let actual_size = actual.as_ref().chars().count();
88+
format!(
89+
"which has character count {}, {}",
90+
actual_size,
91+
self.expected.explain_match(&actual_size)
92+
)
93+
}
94+
}
95+
96+
#[cfg(test)]
97+
mod tests {
98+
use super::char_count;
99+
use crate::matcher::{Matcher, MatcherResult};
100+
use crate::prelude::*;
101+
use indoc::indoc;
102+
use std::fmt::Debug;
103+
use std::marker::PhantomData;
104+
105+
#[test]
106+
fn char_count_matches_string_slice() -> Result<()> {
107+
let value = "abcd";
108+
verify_that!(value, char_count(eq(4)))
109+
}
110+
111+
#[test]
112+
fn char_count_matches_owned_string() -> Result<()> {
113+
let value = String::from("abcd");
114+
verify_that!(value, char_count(eq(4)))
115+
}
116+
117+
#[test]
118+
fn char_count_counts_non_ascii_characters_correctly() -> Result<()> {
119+
let value = "äöüß";
120+
verify_that!(value, char_count(eq(4)))
121+
}
122+
123+
#[test]
124+
fn char_count_explains_match() -> Result<()> {
125+
struct TestMatcher<T>(PhantomData<T>);
126+
impl<T: Debug> Matcher for TestMatcher<T> {
127+
type ActualT = T;
128+
129+
fn matches(&self, _: &T) -> MatcherResult {
130+
false.into()
131+
}
132+
133+
fn describe(&self, _: MatcherResult) -> String {
134+
"called described".into()
135+
}
136+
137+
fn explain_match(&self, _: &T) -> String {
138+
"called explain_match".into()
139+
}
140+
}
141+
verify_that!(
142+
char_count(TestMatcher(Default::default())).explain_match(&"A string"),
143+
displays_as(eq("which has character count 8, called explain_match"))
144+
)
145+
}
146+
147+
#[test]
148+
fn char_count_has_correct_failure_message() -> Result<()> {
149+
let result = verify_that!("äöüß", char_count(eq(3)));
150+
verify_that!(
151+
result,
152+
err(displays_as(contains_substring(indoc!(
153+
r#"
154+
Value of: "äöüß"
155+
Expected: has character count, which is equal to 3
156+
Actual: "äöüß",
157+
which has character count 4, which isn't equal to 3"#
158+
))))
159+
)
160+
}
161+
}

googletest/src/matchers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
1717
pub mod all_matcher;
1818
pub mod anything_matcher;
19+
pub mod char_count_matcher;
1920
pub mod conjunction_matcher;
2021
pub mod container_eq_matcher;
2122
pub mod contains_matcher;
@@ -55,6 +56,7 @@ pub mod tuple_matcher;
5556
pub mod unordered_elements_are_matcher;
5657

5758
pub use anything_matcher::anything;
59+
pub use char_count_matcher::char_count;
5860
pub use container_eq_matcher::container_eq;
5961
pub use contains_matcher::contains;
6062
pub use contains_regex_matcher::contains_regex;

googletest/src/matchers/str_matcher.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
use crate::{
1616
matcher::{Matcher, MatcherResult},
17-
matcher_support::{edit_distance, summarize_diff::{create_diff_reversed, create_diff}},
18-
matchers::{
19-
eq_deref_of_matcher::EqDerefOfMatcher,
20-
eq_matcher::{ EqMatcher},
17+
matcher_support::{
18+
edit_distance,
19+
summarize_diff::{create_diff, create_diff_reversed},
2120
},
21+
matchers::{eq_deref_of_matcher::EqDerefOfMatcher, eq_matcher::EqMatcher},
2222
};
2323
use std::borrow::Cow;
2424
use std::fmt::Debug;

0 commit comments

Comments
 (0)