Skip to content

Commit 3c2022d

Browse files
hovinenbcopybara-github
authored andcommitted
Introduce a matcher eq_deref_of.
This is similar to `eq` but accepts a reference to the expected value rather than consuming it. This is useful in cases where one wants to check equality, has only a reference to the expected value, and either: * the expected value is neither Copy nor Clone, or * cloning the expected value is perceived to be too expensive. This change also refactors the implementation of `explain_match` in `EqMatcher` to allow it to be reused by `EqDerefOfMatcher`. PiperOrigin-RevId: 529731930
1 parent a4e3377 commit 3c2022d

File tree

4 files changed

+195
-24
lines changed

4 files changed

+195
-24
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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::{MatchExplanation, Matcher, MatcherResult};
16+
use crate::matchers::eq_matcher::create_diff;
17+
use std::{fmt::Debug, marker::PhantomData, ops::Deref};
18+
19+
/// Matches a value equal (in the sense of `==`) to the dereferenced value of
20+
/// `expected`.
21+
///
22+
/// This is similar to [`eq`][crate::matchers::eq] but takes a reference or
23+
/// smart pointer to the expected value rather than consuming it. This is useful
24+
/// when:
25+
///
26+
/// * one has only a reference to the expected value, and
27+
/// * the expected value cannot or should not be copied or cloned to create an
28+
/// owned value from it.
29+
///
30+
/// ```
31+
/// # use googletest::{matchers::eq_deref_of, verify_that};
32+
/// #[derive(Debug, PartialEq)]
33+
/// struct NonCloneableStruct(i32);
34+
/// let expected = NonCloneableStruct(123);
35+
/// verify_that!(NonCloneableStruct(123), eq_deref_of(&expected))
36+
/// # .unwrap()
37+
/// ```
38+
///
39+
/// **Note**: while one can use `eq_deref_of` with the configuration methods of
40+
/// [`StrMatcherConfigurator`][crate::matchers::str_matcher::StrMatcherConfigurator]
41+
/// to configure string equality, it is not possible to do so when the input is
42+
/// a smart pointer to a string.
43+
///
44+
/// ```compile_fail
45+
/// # use googletest::{matchers::{eq_deref_of, str_matcher::StrMatcherConfigurator}, verify_that};
46+
/// verify_that!("A string", eq_deref_of(Box::new("A STRING")).ignoring_ascii_case()) // Does not compile
47+
/// # .unwrap()
48+
/// ```
49+
///
50+
/// Otherwise, this has the same behaviour as [`eq`][crate::matchers::eq].
51+
pub fn eq_deref_of<ActualT: ?Sized, ExpectedRefT>(
52+
expected: ExpectedRefT,
53+
) -> EqDerefOfMatcher<ActualT, ExpectedRefT> {
54+
EqDerefOfMatcher { expected, phantom: Default::default() }
55+
}
56+
57+
/// A matcher which matches a value equal to the derefenced value of `expected`.
58+
///
59+
/// See [`eq_deref_of`].
60+
pub struct EqDerefOfMatcher<ActualT: ?Sized, ExpectedRefT> {
61+
pub(crate) expected: ExpectedRefT,
62+
phantom: PhantomData<ActualT>,
63+
}
64+
65+
impl<ActualT, ExpectedRefT, ExpectedT> Matcher for EqDerefOfMatcher<ActualT, ExpectedRefT>
66+
where
67+
ActualT: Debug + ?Sized,
68+
ExpectedRefT: Deref<Target = ExpectedT> + Debug,
69+
ExpectedT: PartialEq<ActualT> + Debug,
70+
{
71+
type ActualT = ActualT;
72+
73+
fn matches(&self, actual: &ActualT) -> MatcherResult {
74+
(self.expected.deref() == actual).into()
75+
}
76+
77+
fn describe(&self, matcher_result: MatcherResult) -> String {
78+
match matcher_result {
79+
MatcherResult::Matches => format!("is equal to {:?}", self.expected),
80+
MatcherResult::DoesNotMatch => format!("isn't equal to {:?}", self.expected),
81+
}
82+
}
83+
84+
fn explain_match(&self, actual: &ActualT) -> MatchExplanation {
85+
create_diff(
86+
&format!("{:#?}", self.expected.deref()),
87+
&format!("{:#?}", actual),
88+
&self.describe(self.matches(actual)),
89+
)
90+
}
91+
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use super::eq_deref_of;
96+
use crate::prelude::*;
97+
use indoc::indoc;
98+
99+
#[derive(Debug, PartialEq)]
100+
struct NonCloneNonCopyStruct(i32);
101+
102+
#[test]
103+
fn matches_value_with_ref_to_equal_value() -> Result<()> {
104+
verify_that!(NonCloneNonCopyStruct(123), eq_deref_of(&NonCloneNonCopyStruct(123)))
105+
}
106+
107+
#[test]
108+
fn matches_value_with_box_of_equal_value() -> Result<()> {
109+
verify_that!(NonCloneNonCopyStruct(123), eq_deref_of(Box::new(NonCloneNonCopyStruct(123))))
110+
}
111+
112+
#[test]
113+
fn does_not_match_value_with_non_equal_value() -> Result<()> {
114+
verify_that!(NonCloneNonCopyStruct(123), not(eq_deref_of(&NonCloneNonCopyStruct(234))))
115+
}
116+
117+
#[test]
118+
fn shows_structured_diff() -> Result<()> {
119+
#[derive(Debug, PartialEq)]
120+
struct Strukt {
121+
int: i32,
122+
string: String,
123+
}
124+
125+
let result = verify_that!(
126+
Strukt { int: 123, string: "something".into() },
127+
eq_deref_of(Box::new(Strukt { int: 321, string: "someone".into() }))
128+
);
129+
verify_that!(
130+
result,
131+
err(displays_as(contains_substring(indoc! {
132+
r#"
133+
Value of: Strukt { int: 123, string: "something".into() }
134+
Expected: is equal to Strukt { int: 321, string: "someone" }
135+
Actual: Strukt {
136+
int: 123,
137+
string: "something",
138+
}, which isn't equal to Strukt { int: 321, string: "someone" }
139+
Debug diff:
140+
Strukt {
141+
+ int: 123,
142+
- int: 321,
143+
+ string: "something",
144+
- string: "someone",
145+
}
146+
"#})))
147+
)
148+
}
149+
}

googletest/src/matchers/eq_matcher.rs

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -95,31 +95,36 @@ impl<A: Debug + ?Sized, T: PartialEq<A> + Debug> Matcher for EqMatcher<A, T> {
9595
}
9696

9797
fn explain_match(&self, actual: &A) -> MatchExplanation {
98-
let actual_debug = format!("{:#?}", actual);
99-
if actual_debug.lines().count() < 2 {
100-
// If the actual debug is only one line, then there is no point in doing a
101-
// line-by-line diff.
102-
return MatchExplanation::create(format!(
103-
"which {}",
104-
self.describe(self.matches(actual))
105-
));
106-
}
107-
let expected_debug = format!("{:#?}", self.expected);
108-
let edit_list = edit_distance::edit_list(actual_debug.lines(), expected_debug.lines());
98+
create_diff(
99+
&format!("{:#?}", self.expected),
100+
&format!("{:#?}", actual),
101+
&self.describe(self.matches(actual)),
102+
)
103+
}
104+
}
109105

110-
if edit_list.is_empty() {
111-
return MatchExplanation::create(format!(
112-
"which {}\nNo difference found between debug strings.",
113-
self.describe(self.matches(actual))
114-
));
115-
}
106+
pub(super) fn create_diff(
107+
expected_debug: &str,
108+
actual_debug: &str,
109+
description: &str,
110+
) -> MatchExplanation {
111+
if actual_debug.lines().count() < 2 {
112+
// If the actual debug is only one line, then there is no point in doing a
113+
// line-by-line diff.
114+
return MatchExplanation::create(format!("which {description}",));
115+
}
116+
let edit_list = edit_distance::edit_list(actual_debug.lines(), expected_debug.lines());
116117

117-
MatchExplanation::create(format!(
118-
"which {}\nDebug diff:{}",
119-
self.describe(self.matches(actual)),
120-
edit_list_summary(&edit_list)
121-
))
118+
if edit_list.is_empty() {
119+
return MatchExplanation::create(format!(
120+
"which {description}\nNo difference found between debug strings.",
121+
));
122122
}
123+
124+
MatchExplanation::create(format!(
125+
"which {description}\nDebug diff:{}",
126+
edit_list_summary(&edit_list)
127+
))
123128
}
124129

125130
fn edit_list_summary(edit_list: &[edit_distance::Edit<&str>]) -> String {

googletest/src/matchers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub mod display_matcher;
2323
pub mod each_matcher;
2424
pub mod elements_are_matcher;
2525
pub mod empty_matcher;
26+
pub mod eq_deref_of_matcher;
2627
pub mod eq_matcher;
2728
pub mod err_matcher;
2829
pub mod field_matcher;
@@ -59,6 +60,7 @@ pub use disjunction_matcher::OrMatcherExt;
5960
pub use display_matcher::displays_as;
6061
pub use each_matcher::each;
6162
pub use empty_matcher::empty;
63+
pub use eq_deref_of_matcher::eq_deref_of;
6264
pub use eq_matcher::eq;
6365
pub use err_matcher::err;
6466
pub use ge_matcher::ge;

googletest/src/matchers/str_matcher.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
// limitations under the License.
1414

1515
use crate::matcher::{Matcher, MatcherResult};
16-
use crate::matchers::eq_matcher;
17-
use eq_matcher::EqMatcher;
16+
use crate::matchers::{eq_deref_of_matcher::EqDerefOfMatcher, eq_matcher::EqMatcher};
1817
use std::borrow::Cow;
1918
use std::fmt::Debug;
2019
use std::marker::PhantomData;
@@ -350,6 +349,12 @@ impl<A: ?Sized, T: Deref<Target = str>> From<EqMatcher<A, T>> for StrMatcher<A,
350349
}
351350
}
352351

352+
impl<A: ?Sized, T: Deref<Target = str>> From<EqDerefOfMatcher<A, T>> for StrMatcher<A, T> {
353+
fn from(value: EqDerefOfMatcher<A, T>) -> Self {
354+
Self::with_default_config(value.expected)
355+
}
356+
}
357+
353358
impl<A: ?Sized, T> StrMatcher<A, T> {
354359
/// Returns a [`StrMatcher`] with a default configuration to match against
355360
/// the given expected value.
@@ -627,6 +632,16 @@ mod tests {
627632
verify_that!("A string", eq("A STRING").ignoring_ascii_case())
628633
}
629634

635+
#[test]
636+
fn allows_ignoring_ascii_case_from_eq_deref_of_str_slice() -> Result<()> {
637+
verify_that!("A string", eq_deref_of("A STRING").ignoring_ascii_case())
638+
}
639+
640+
#[test]
641+
fn allows_ignoring_ascii_case_from_eq_deref_of_owned_string() -> Result<()> {
642+
verify_that!("A string", eq_deref_of("A STRING".to_string()).ignoring_ascii_case())
643+
}
644+
630645
#[test]
631646
fn matches_string_containing_expected_value_in_contains_mode() -> Result<()> {
632647
verify_that!("Some string", contains_substring("str"))

0 commit comments

Comments
 (0)