Skip to content

Commit 4b86b0c

Browse files
hovinenbcopybara-github
authored andcommitted
Introduce a declarative macro to match properties of an object.
This is analogous to the existing macro `field!` which allows applying arbitrary matchers to fields of a struct or enum. This analogously allows matching on the output of method invocations. It is particularly useful in situations where one cannot access the fields directly (due to visibility), or direct access is not desired for other reasons. This includes two variants of the matcher, one for owned returned values (such as most primitive types) and one for references. The peculiarities of Rust's borrow checking unfortunately require keeping these matchers separate: otherwise the Rust borrow checker does not know how long the returned value will live and cannot prove that it doesn't outlive the data which it references in the outer struct. Unfortunately, this does not work properly with methods returning string references or slices due to complicated lifetime issues. PiperOrigin-RevId: 521438328
1 parent b2543ab commit 4b86b0c

File tree

5 files changed

+388
-1
lines changed

5 files changed

+388
-1
lines changed

googletest/src/matchers/field_matcher.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@
7070
/// }
7171
/// verify_that(value, field!(OuterStruct.inner.0, eq(32)))?; // Does not compile
7272
/// ```
73+
///
74+
/// See also the macro [`property`][crate::property] for an analogous mechanism
75+
/// to extract a datum by invoking a method.
7376
#[macro_export]
7477
macro_rules! field {
7578
($($t:tt)*) => { $crate::field_internal!($($t)*) }

googletest/src/matchers/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ pub mod pointwise_matcher;
7575
#[cfg(not(google3))]
7676
pub mod predicate_matcher;
7777
#[cfg(not(google3))]
78+
pub mod property_matcher;
79+
#[cfg(not(google3))]
7880
pub mod size_matcher;
7981
#[cfg(not(google3))]
8082
pub mod some_matcher;
@@ -124,8 +126,9 @@ pub use ok_matcher::ok;
124126
pub use points_to_matcher::points_to;
125127
#[cfg(google3)]
126128
pub use pointwise_matcher::pointwise;
127-
#[cfg(google3)]
128129
pub use predicate_matcher::{predicate, PredicateMatcher};
130+
#[cfg(google3)]
131+
pub use property_matcher::property;
129132
pub use size_matcher::size;
130133
pub use some_matcher::some;
131134
pub use str_matcher::{contains_substring, ends_with, starts_with};
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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+
/// Matches an object which, upon calling the given method on it with the given
16+
/// arguments, produces a value matched by the given inner matcher.
17+
///
18+
/// This is particularly useful as a nested matcher when the desired
19+
/// property cannot be accessed through a field and must instead be
20+
/// extracted through a method call. For example:
21+
///
22+
/// ```
23+
/// pub struct InnerStruct {
24+
/// // Some private fields
25+
/// }
26+
/// impl InnerStruct {
27+
/// pub fn get_foo(&self) -> u32 {...}
28+
/// }
29+
/// let value = vec![InnerStruct { ... }]
30+
/// verify_that!(value, contains(property!(InnerStruct.get_foo(), eq(100))))
31+
/// ```
32+
///
33+
/// If the method returns a *reference*, then it must be preceded by the keyword
34+
/// `ref`:
35+
///
36+
/// ```
37+
/// impl InnerStruct {
38+
/// pub fn get_foo(&self) -> &u32 {...}
39+
/// }
40+
/// verify_that!(value, contains(property!(ref InnerStruct.get_foo(), eq(100))))
41+
/// ```
42+
///
43+
/// > Note: At the moment, this does not work properly with methods returning
44+
/// > string references or slices.
45+
///
46+
/// The method may also take additional arguments:
47+
///
48+
/// ```
49+
/// impl InnerStruct {
50+
/// pub fn add_to_foo(&self, a: u32) -> u32 {...}
51+
/// }
52+
/// verify_that!(value, contains(property!(InnerStruct.add_to_foo(50), eq(150))))
53+
/// ```
54+
///
55+
/// > **Note**: The method should be pure function with a deterministic output
56+
/// > and no side effects. In particular, in the event of an assertion failure,
57+
/// > it will be invoked a second time, with the assertion failure output
58+
/// > reflecting the *second* invocation.
59+
///
60+
/// This macro is analogous to [`field`][crate::field], except that it extracts
61+
/// the datum to be matched from the given object by invoking a method rather
62+
/// than accessing a field.
63+
///
64+
/// The list of arguments may optionally have a trailing comma.
65+
#[macro_export]
66+
macro_rules! property {
67+
($($t:tt)*) => { $crate::property_internal!($($t)*) }
68+
}
69+
70+
// Internal-only macro created so that the macro definition does not appear in
71+
// generated documentation.
72+
#[doc(hidden)]
73+
#[macro_export]
74+
macro_rules! property_internal {
75+
($($t:ident)::+.$method:tt($($argument:tt),* $(,)?), $m:expr) => {{
76+
#[cfg(google3)]
77+
use $crate::internal::property_matcher;
78+
#[cfg(not(google3))]
79+
use $crate::matchers::property_matcher::internal::property_matcher;
80+
property_matcher(
81+
|o: &$($t)::+| o.$method($($argument),*),
82+
&stringify!($method($($argument),*)),
83+
$m)
84+
}};
85+
86+
(ref $($t:ident)::+.$method:tt($($argument:tt),* $(,)?), $m:expr) => {{
87+
#[cfg(google3)]
88+
use $crate::internal::property_ref_matcher;
89+
#[cfg(not(google3))]
90+
use $crate::matchers::property_matcher::internal::property_ref_matcher;
91+
property_ref_matcher(
92+
|o: &$($t)::+| o.$method($($argument),*),
93+
&stringify!($method($($argument),*)),
94+
$m)
95+
}};
96+
}
97+
98+
/// Functions for use only by the declarative macros in this module.
99+
///
100+
/// **For internal use only. API stablility is not guaranteed!**
101+
#[doc(hidden)]
102+
pub mod internal {
103+
#[cfg(not(google3))]
104+
use crate as googletest;
105+
use googletest::matcher::{MatchExplanation, Matcher, MatcherResult};
106+
use std::fmt::Debug;
107+
108+
/// **For internal use only. API stablility is not guaranteed!**
109+
#[doc(hidden)]
110+
pub fn property_matcher<OuterT: Debug, InnerT: Debug, MatcherT: Matcher<InnerT>>(
111+
extractor: impl Fn(&OuterT) -> InnerT,
112+
property_desc: &'static str,
113+
inner: MatcherT,
114+
) -> impl Matcher<OuterT> {
115+
PropertyMatcher { extractor, property_desc, inner }
116+
}
117+
118+
struct PropertyMatcher<ExtractorT, MatcherT> {
119+
extractor: ExtractorT,
120+
property_desc: &'static str,
121+
inner: MatcherT,
122+
}
123+
124+
impl<InnerT: Debug, OuterT: Debug, ExtractorT: Fn(&OuterT) -> InnerT, MatcherT: Matcher<InnerT>>
125+
Matcher<OuterT> for PropertyMatcher<ExtractorT, MatcherT>
126+
{
127+
fn matches(&self, actual: &OuterT) -> MatcherResult {
128+
self.inner.matches(&(self.extractor)(actual))
129+
}
130+
131+
fn describe(&self, matcher_result: MatcherResult) -> String {
132+
format!(
133+
"has property `{}`, which {}",
134+
self.property_desc,
135+
self.inner.describe(matcher_result)
136+
)
137+
}
138+
139+
fn explain_match(&self, actual: &OuterT) -> MatchExplanation {
140+
let actual_inner = (self.extractor)(actual);
141+
MatchExplanation::create(format!(
142+
"whose property `{}` is `{:#?}`, {}",
143+
self.property_desc,
144+
actual_inner,
145+
self.inner.explain_match(&actual_inner)
146+
))
147+
}
148+
}
149+
150+
/// **For internal use only. API stablility is not guaranteed!**
151+
#[doc(hidden)]
152+
pub fn property_ref_matcher<OuterT, InnerT, MatcherT>(
153+
extractor: fn(&OuterT) -> &InnerT,
154+
property_desc: &'static str,
155+
inner: MatcherT,
156+
) -> impl Matcher<OuterT>
157+
where
158+
OuterT: Debug,
159+
InnerT: Debug + ?Sized,
160+
MatcherT: Matcher<InnerT>,
161+
{
162+
PropertyRefMatcher { extractor, property_desc, inner }
163+
}
164+
165+
struct PropertyRefMatcher<InnerT: ?Sized, OuterT, MatcherT> {
166+
extractor: fn(&OuterT) -> &InnerT,
167+
property_desc: &'static str,
168+
inner: MatcherT,
169+
}
170+
171+
impl<InnerT: Debug + ?Sized, OuterT: Debug, MatcherT: Matcher<InnerT>> Matcher<OuterT>
172+
for PropertyRefMatcher<InnerT, OuterT, MatcherT>
173+
{
174+
fn matches(&self, actual: &OuterT) -> MatcherResult {
175+
self.inner.matches((self.extractor)(actual))
176+
}
177+
178+
fn describe(&self, matcher_result: MatcherResult) -> String {
179+
format!(
180+
"has property `{}`, which {}",
181+
self.property_desc,
182+
self.inner.describe(matcher_result)
183+
)
184+
}
185+
186+
fn explain_match(&self, actual: &OuterT) -> MatchExplanation {
187+
let actual_inner = (self.extractor)(actual);
188+
MatchExplanation::create(format!(
189+
"whose property `{}` is `{:#?}`, {}",
190+
self.property_desc,
191+
actual_inner,
192+
self.inner.explain_match(actual_inner)
193+
))
194+
}
195+
}
196+
}

googletest/tests/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ mod elements_are_matcher_test;
1717
mod field_matcher_test;
1818
mod matches_pattern_test;
1919
mod pointwise_matcher_test;
20+
mod property_matcher_test;
2021
mod tuple_matcher_test;
2122
mod unordered_elements_are_matcher_test;

0 commit comments

Comments
 (0)