Skip to content

Commit 4a1540b

Browse files
committed
Remove the need for allocation from is_utf8_string.
Previously, `IsEncodedStringMatcher` converted its actual value input to a `Vec` and constructed a `String` out of that in order to match it. This is because using a string slice caused problems with the borrow checker as described in #323. The fundamental problem is that the type against which the inner matcher is matching is a reference whose lifetime must be named in the trait bound: ``` impl<ActualT: AsRef<[u8]> + Debug, InnerMatcherT> Matcher for IsEncodedStringMatcher<ActualT, InnerMatcherT> where InnerMatcherT: Matcher<ActualT = &'what str>, ^^^^ What goes here??? ``` One option is just to remove the reference: ``` impl<ActualT: AsRef<[u8]> + Debug, InnerMatcherT> Matcher for IsEncodedStringMatcher<ActualT, InnerMatcherT> where InnerMatcherT: Matcher<ActualT = str>, ``` This doesn't work when the inner matcher is `eq`, since one would then be comparing an `str` and a string slice: ``` verify_that!("A string".as_bytes(), is_utf8_string(eq("A string"))) // Compile error: cannot compare str and &str ``` However, this problem does not occur with `StrMatcher`: ``` verify_that!("A string".as_bytes(), is_utf8_string(constains_substring("A string"))) // Passes ``` So this also introduces an easy way to obtain a `StrMatcher` to check string equality: ``` verify_that!("A string".as_bytes(), is_utf8_string(eq_str("A string"))) // Passes ``` This is slightly inconvenient, since one must use a different matcher to compare string equality in this case. But it is well-documented and not too inconvenient to use. So it should be okay.
1 parent 1d6ec26 commit 4a1540b

File tree

4 files changed

+70
-23
lines changed

4 files changed

+70
-23
lines changed

googletest/crate_docs.md

+4
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,15 @@ The following matchers are provided in GoogleTest Rust:
127127
| [`ends_with`] | A string ending with the given suffix. |
128128
| [`eq`] | A value equal to the argument, in the sense of the [`PartialEq`] trait. |
129129
| [`eq_deref_of`] | A value equal to the dereferenced value of the argument. |
130+
| [`eq_str`] | A string equal to the argument. |
130131
| [`err`] | A [`Result`][std::result::Result] containing an `Err` variant the argument matches. |
131132
| [`field!`] | A struct or enum with a given field whose value the argument matches. |
132133
| [`ge`] | A [`PartialOrd`] value greater than or equal to the given value. |
133134
| [`gt`] | A [`PartialOrd`] value strictly greater than the given value. |
134135
| [`has_entry`] | A [`HashMap`] containing a given key whose value the argument matches. |
135136
| [`is_contained_in!`] | A container each of whose elements is matched by some given matcher. |
136137
| [`is_nan`] | A floating point number which is NaN. |
138+
| [`is_utf8_string`] | A byte sequence representing a UTF-8 encoded string which the argument matches. |
137139
| [`le`] | A [`PartialOrd`] value less than or equal to the given value. |
138140
| [`len`] | A container whose number of elements the argument matches. |
139141
| [`lt`] | A [`PartialOrd`] value strictly less than the given value. |
@@ -169,6 +171,7 @@ The following matchers are provided in GoogleTest Rust:
169171
[`empty`]: matchers::empty
170172
[`ends_with`]: matchers::ends_with
171173
[`eq`]: matchers::eq
174+
[`eq_str`]: matchers::eq_str
172175
[`eq_deref_of`]: matchers::eq_deref_of
173176
[`err`]: matchers::err
174177
[`field!`]: matchers::field
@@ -177,6 +180,7 @@ The following matchers are provided in GoogleTest Rust:
177180
[`has_entry`]: matchers::has_entry
178181
[`is_contained_in!`]: matchers::is_contained_in
179182
[`is_nan`]: matchers::is_nan
183+
[`is_utf8_string`]: matchers::is_utf8_string
180184
[`le`]: matchers::le
181185
[`len`]: matchers::len
182186
[`lt`]: matchers::lt

googletest/src/matchers/is_encoded_string_matcher.rs

+28-22
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,21 @@ use std::{fmt::Debug, marker::PhantomData};
2525
///
2626
/// The input may be a slice `&[u8]` or a `Vec` of bytes.
2727
///
28+
/// When asserting the equality of the actual value with a given string, use
29+
/// [`eq_str`] rather than [`eq`].
30+
///
2831
/// ```
2932
/// # use googletest::prelude::*;
3033
/// # fn should_pass() -> Result<()> {
3134
/// let bytes: &[u8] = "A string".as_bytes();
32-
/// verify_that!(bytes, is_utf8_string(eq("A string")))?; // Passes
35+
/// verify_that!(bytes, is_utf8_string(eq_str("A string")))?; // Passes
3336
/// let bytes: Vec<u8> = "A string".as_bytes().to_vec();
34-
/// verify_that!(bytes, is_utf8_string(eq("A string")))?; // Passes
37+
/// verify_that!(bytes, is_utf8_string(eq_str("A string")))?; // Passes
3538
/// # Ok(())
3639
/// # }
3740
/// # fn should_fail_1() -> Result<()> {
3841
/// # let bytes: &[u8] = "A string".as_bytes();
39-
/// verify_that!(bytes, is_utf8_string(eq("Another string")))?; // Fails (inner matcher does not match)
42+
/// verify_that!(bytes, is_utf8_string(eq_str("Another string")))?; // Fails (inner matcher does not match)
4043
/// # Ok(())
4144
/// # }
4245
/// # fn should_fail_2() -> Result<()> {
@@ -48,11 +51,14 @@ use std::{fmt::Debug, marker::PhantomData};
4851
/// # should_fail_1().unwrap_err();
4952
/// # should_fail_2().unwrap_err();
5053
/// ```
51-
pub fn is_utf8_string<'a, ActualT: AsRef<[u8]> + Debug + 'a, InnerMatcherT>(
54+
///
55+
/// [`eq`]: crate::matchers::eq
56+
/// [`eq_str`]: crate::matchers::eq_str
57+
pub fn is_utf8_string<ActualT: AsRef<[u8]> + Debug, InnerMatcherT>(
5258
inner: InnerMatcherT,
5359
) -> impl Matcher<ActualT = ActualT>
5460
where
55-
InnerMatcherT: Matcher<ActualT = String>,
61+
InnerMatcherT: Matcher<ActualT = str>,
5662
{
5763
IsEncodedStringMatcher { inner, phantom: Default::default() }
5864
}
@@ -62,15 +68,15 @@ struct IsEncodedStringMatcher<ActualT, InnerMatcherT> {
6268
phantom: PhantomData<ActualT>,
6369
}
6470

65-
impl<'a, ActualT: AsRef<[u8]> + Debug + 'a, InnerMatcherT> Matcher
71+
impl<ActualT: AsRef<[u8]> + Debug, InnerMatcherT> Matcher
6672
for IsEncodedStringMatcher<ActualT, InnerMatcherT>
6773
where
68-
InnerMatcherT: Matcher<ActualT = String>,
74+
InnerMatcherT: Matcher<ActualT = str>,
6975
{
7076
type ActualT = ActualT;
7177

7278
fn matches(&self, actual: &Self::ActualT) -> MatcherResult {
73-
String::from_utf8(actual.as_ref().to_vec())
79+
std::str::from_utf8(actual.as_ref())
7480
.map(|s| self.inner.matches(&s))
7581
.unwrap_or(MatcherResult::NoMatch)
7682
}
@@ -91,7 +97,7 @@ where
9197
}
9298

9399
fn explain_match(&self, actual: &Self::ActualT) -> Description {
94-
match String::from_utf8(actual.as_ref().to_vec()) {
100+
match std::str::from_utf8(actual.as_ref()) {
95101
Ok(s) => {
96102
format!("which is a UTF-8 encoded string {}", self.inner.explain_match(&s)).into()
97103
}
@@ -107,70 +113,70 @@ mod tests {
107113

108114
#[test]
109115
fn matches_string_as_byte_slice() -> Result<()> {
110-
verify_that!("A string".as_bytes(), is_utf8_string(eq("A string")))
116+
verify_that!("A string".as_bytes(), is_utf8_string(eq_str("A string")))
111117
}
112118

113119
#[test]
114120
fn matches_string_as_byte_vec() -> Result<()> {
115-
verify_that!("A string".as_bytes().to_vec(), is_utf8_string(eq("A string")))
121+
verify_that!("A string".as_bytes().to_vec(), is_utf8_string(eq_str("A string")))
116122
}
117123

118124
#[test]
119125
fn matches_string_with_utf_8_encoded_sequences() -> Result<()> {
120-
verify_that!("äöüÄÖÜ".as_bytes().to_vec(), is_utf8_string(eq("äöüÄÖÜ")))
126+
verify_that!("äöüÄÖÜ".as_bytes().to_vec(), is_utf8_string(eq_str("äöüÄÖÜ")))
121127
}
122128

123129
#[test]
124130
fn does_not_match_non_equal_string() -> Result<()> {
125-
verify_that!("äöüÄÖÜ".as_bytes().to_vec(), not(is_utf8_string(eq("A string"))))
131+
verify_that!("äöüÄÖÜ".as_bytes().to_vec(), not(is_utf8_string(eq_str("A string"))))
126132
}
127133

128134
#[test]
129135
fn does_not_match_non_utf_8_encoded_byte_sequence() -> Result<()> {
130-
verify_that!(&[192, 64, 255, 32], not(is_utf8_string(eq("A string"))))
136+
verify_that!(&[192, 64, 255, 32], not(is_utf8_string(eq_str("A string"))))
131137
}
132138

133139
#[test]
134140
fn has_correct_description_in_matched_case() -> Result<()> {
135-
let matcher = is_utf8_string::<&[u8], _>(eq("A string"));
141+
let matcher = is_utf8_string::<&[u8], _>(eq_str("A string"));
136142

137143
verify_that!(
138144
matcher.describe(MatcherResult::Match),
139-
displays_as(eq("is a UTF-8 encoded string which is equal to \"A string\""))
145+
displays_as(eq_str("is a UTF-8 encoded string which is equal to \"A string\""))
140146
)
141147
}
142148

143149
#[test]
144150
fn has_correct_description_in_not_matched_case() -> Result<()> {
145-
let matcher = is_utf8_string::<&[u8], _>(eq("A string"));
151+
let matcher = is_utf8_string::<&[u8], _>(eq_str("A string"));
146152

147153
verify_that!(
148154
matcher.describe(MatcherResult::NoMatch),
149-
displays_as(eq("is not a UTF-8 encoded string which is equal to \"A string\""))
155+
displays_as(eq_str("is not a UTF-8 encoded string which is equal to \"A string\""))
150156
)
151157
}
152158

153159
#[test]
154160
fn has_correct_explanation_in_matched_case() -> Result<()> {
155-
let explanation = is_utf8_string(eq("A string")).explain_match(&"A string".as_bytes());
161+
let explanation = is_utf8_string(eq_str("A string")).explain_match(&"A string".as_bytes());
156162

157163
verify_that!(
158164
explanation,
159-
displays_as(eq("which is a UTF-8 encoded string which is equal to \"A string\""))
165+
displays_as(eq_str("which is a UTF-8 encoded string which is equal to \"A string\""))
160166
)
161167
}
162168

163169
#[test]
164170
fn has_correct_explanation_when_byte_array_is_not_utf8_encoded() -> Result<()> {
165-
let explanation = is_utf8_string(eq("A string")).explain_match(&&[192, 128, 0, 64]);
171+
let explanation = is_utf8_string(eq_str("A string")).explain_match(&&[192, 128, 0, 64]);
166172

167173
verify_that!(explanation, displays_as(starts_with("which is not a UTF-8 encoded string: ")))
168174
}
169175

170176
#[test]
171177
fn has_correct_explanation_when_inner_matcher_does_not_match() -> Result<()> {
172178
let explanation =
173-
is_utf8_string(eq("A string")).explain_match(&"Another string".as_bytes());
179+
is_utf8_string(eq_str("A string")).explain_match(&"Another string".as_bytes());
174180

175181
verify_that!(
176182
explanation,

googletest/src/matchers/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub use points_to_matcher::points_to;
8585
pub use predicate_matcher::{predicate, PredicateMatcher};
8686
pub use some_matcher::some;
8787
pub use str_matcher::{
88-
contains_substring, ends_with, starts_with, StrMatcher, StrMatcherConfigurator,
88+
contains_substring, ends_with, eq_str, starts_with, StrMatcher, StrMatcherConfigurator,
8989
};
9090
pub use subset_of_matcher::subset_of;
9191
pub use superset_of_matcher::superset_of;

googletest/src/matchers/str_matcher.rs

+37
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,43 @@ use std::fmt::Debug;
2626
use std::marker::PhantomData;
2727
use std::ops::Deref;
2828

29+
/// Matches a string equal to the given string.
30+
///
31+
/// This has the same effect as [`eq`]. Unlike [`eq`], [`eq_str`] can be used
32+
/// in cases where the actual type being matched is `str`. In paticular, one
33+
/// can use [`eq_str`] together with [`is_utf8_string`].
34+
///
35+
/// ```
36+
/// # use googletest::prelude::*;
37+
/// # fn should_pass_1() -> Result<()> {
38+
/// verify_that!("Some value", eq_str("Some value"))?; // Passes
39+
/// verify_that!("Some value".to_string(), eq_str("Some value"))?; // Passes
40+
/// # Ok(())
41+
/// # }
42+
/// # fn should_fail() -> Result<()> {
43+
/// verify_that!("Another value", eq_str("Some value"))?; // Fails
44+
/// # Ok(())
45+
/// # }
46+
/// # fn should_pass_2() -> Result<()> {
47+
/// let value_as_bytes = "Some value".as_bytes();
48+
/// verify_that!(value_as_bytes, is_utf8_string(eq_str("Some value")))?; // Passes
49+
/// # Ok(())
50+
/// # }
51+
/// # should_pass_1().unwrap();
52+
/// # should_fail().unwrap_err();
53+
/// # should_pass_2().unwrap();
54+
/// ```
55+
///
56+
/// [`eq`]: crate::matchers::eq
57+
/// [`is_utf8_string`]: crate::matchers::is_utf8_string
58+
pub fn eq_str<A: ?Sized, T>(expected: T) -> StrMatcher<A, T> {
59+
StrMatcher {
60+
configuration: Configuration { mode: MatchMode::Equals, ..Default::default() },
61+
expected,
62+
phantom: Default::default(),
63+
}
64+
}
65+
2966
/// Matches a string containing a given substring.
3067
///
3168
/// Both the actual value and the expected substring may be either a `String` or

0 commit comments

Comments
 (0)