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::{
16     description::Description,
17     matcher::{Matcher, MatcherResult},
18 };
19 use std::{fmt::Debug, marker::PhantomData};
20 
21 /// Matches a string whose number of Unicode scalars matches `expected`.
22 ///
23 /// In other words, the argument must match the output of
24 /// [`actual_string.chars().count()`][std::str::Chars].
25 ///
26 /// This can have surprising effects when what appears to be a single character
27 /// is composed of multiple Unicode scalars. See [Rust documentation on
28 /// character
29 /// representation](https://doc.rust-lang.org/std/primitive.char.html#representation)
30 /// for more information.
31 ///
32 /// This matches against owned strings and string slices.
33 ///
34 /// ```
35 /// # use googletest::prelude::*;
36 /// # fn should_pass() -> Result<()> {
37 /// let string_slice = "A string";
38 /// verify_that!(string_slice, char_count(eq(8)))?;
39 /// let non_ascii_string_slice = "Ä ſtřiɲğ";
40 /// verify_that!(non_ascii_string_slice, char_count(eq(8)))?;
41 /// let owned_string = String::from("A string");
42 /// verify_that!(owned_string, char_count(eq(8)))?;
43 /// #     Ok(())
44 /// # }
45 /// # should_pass().unwrap();
46 /// ```
47 ///
48 /// The parameter `expected` can be any integer numeric matcher.
49 ///
50 /// ```
51 /// # use googletest::prelude::*;
52 /// # fn should_pass() -> Result<()> {
53 /// let string_slice = "A string";
54 /// verify_that!(string_slice, char_count(gt(4)))?;
55 /// #     Ok(())
56 /// # }
57 /// # should_pass().unwrap();
58 /// ```
char_count<T: Debug + ?Sized + AsRef<str>, E: Matcher<ActualT = usize>>( expected: E, ) -> impl Matcher<ActualT = T>59 pub fn char_count<T: Debug + ?Sized + AsRef<str>, E: Matcher<ActualT = usize>>(
60     expected: E,
61 ) -> impl Matcher<ActualT = T> {
62     CharLenMatcher { expected, phantom: Default::default() }
63 }
64 
65 struct CharLenMatcher<T: ?Sized, E> {
66     expected: E,
67     phantom: PhantomData<T>,
68 }
69 
70 impl<T: Debug + ?Sized + AsRef<str>, E: Matcher<ActualT = usize>> Matcher for CharLenMatcher<T, E> {
71     type ActualT = T;
72 
matches(&self, actual: &T) -> MatcherResult73     fn matches(&self, actual: &T) -> MatcherResult {
74         self.expected.matches(&actual.as_ref().chars().count())
75     }
76 
describe(&self, matcher_result: MatcherResult) -> Description77     fn describe(&self, matcher_result: MatcherResult) -> Description {
78         match matcher_result {
79             MatcherResult::Match => format!(
80                 "has character count, which {}",
81                 self.expected.describe(MatcherResult::Match)
82             )
83             .into(),
84             MatcherResult::NoMatch => format!(
85                 "has character count, which {}",
86                 self.expected.describe(MatcherResult::NoMatch)
87             )
88             .into(),
89         }
90     }
91 
explain_match(&self, actual: &T) -> Description92     fn explain_match(&self, actual: &T) -> Description {
93         let actual_size = actual.as_ref().chars().count();
94         format!(
95             "which has character count {}, {}",
96             actual_size,
97             self.expected.explain_match(&actual_size)
98         )
99         .into()
100     }
101 }
102 
103 #[cfg(test)]
104 mod tests {
105     use super::char_count;
106     use crate::description::Description;
107     use crate::matcher::{Matcher, MatcherResult};
108     use crate::prelude::*;
109     use indoc::indoc;
110     use std::fmt::Debug;
111     use std::marker::PhantomData;
112 
113     #[test]
char_count_matches_string_slice() -> Result<()>114     fn char_count_matches_string_slice() -> Result<()> {
115         let value = "abcd";
116         verify_that!(value, char_count(eq(4)))
117     }
118 
119     #[test]
char_count_matches_owned_string() -> Result<()>120     fn char_count_matches_owned_string() -> Result<()> {
121         let value = String::from("abcd");
122         verify_that!(value, char_count(eq(4)))
123     }
124 
125     #[test]
char_count_counts_non_ascii_characters_correctly() -> Result<()>126     fn char_count_counts_non_ascii_characters_correctly() -> Result<()> {
127         let value = "äöüß";
128         verify_that!(value, char_count(eq(4)))
129     }
130 
131     #[test]
char_count_explains_match() -> Result<()>132     fn char_count_explains_match() -> Result<()> {
133         struct TestMatcher<T>(PhantomData<T>);
134         impl<T: Debug> Matcher for TestMatcher<T> {
135             type ActualT = T;
136 
137             fn matches(&self, _: &T) -> MatcherResult {
138                 false.into()
139             }
140 
141             fn describe(&self, _: MatcherResult) -> Description {
142                 "called described".into()
143             }
144 
145             fn explain_match(&self, _: &T) -> Description {
146                 "called explain_match".into()
147             }
148         }
149         verify_that!(
150             char_count(TestMatcher(Default::default())).explain_match(&"A string"),
151             displays_as(eq("which has character count 8, called explain_match"))
152         )
153     }
154 
155     #[test]
char_count_has_correct_failure_message() -> Result<()>156     fn char_count_has_correct_failure_message() -> Result<()> {
157         let result = verify_that!("äöüß", char_count(eq(3)));
158         verify_that!(
159             result,
160             err(displays_as(contains_substring(indoc!(
161                 r#"
162                 Value of: "äöüß"
163                 Expected: has character count, which is equal to 3
164                 Actual: "äöüß",
165                   which has character count 4, which isn't equal to 3"#
166             ))))
167         )
168     }
169 }
170