build(rust): vendor googletest crate

This commit was merged in pull request #76.
This commit is contained in:
alexion committed 2026-09-19 14:53:22 -04:00
1 parent a6fe84190e
commit 6887b104d6
576 files changed
+284290 -2

No files matched your search

+415
View File
@@ -0,0 +1,415 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use quote::quote;
use syn::{
parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, Attribute,
DeriveInput, Expr, ExprLit, FnArg, ItemFn, Lit, MetaNameValue, PatType, ReturnType, Signature,
Type,
};
/// Marks a test to be run by the Google Rust test runner.
///
/// Annotate tests the same way ordinary Rust tests are annotated:
///
/// ```ignore
/// #[gtest]
/// fn should_work() {
/// ...
/// }
/// ```
///
/// The test function is not required to have a return type. If it does have a
/// return type, that type must be [`googletest::Result`]. One may do this if
/// one wishes to use both fatal and non-fatal assertions in the same test. For
/// example:
///
/// ```ignore
/// #[gtest]
/// fn should_work() -> googletest::Result<()> {
/// let value = 2;
/// expect_that!(value, gt(0));
/// verify_that!(value, eq(2))
/// }
/// ```
///
/// This macro can be used with `#[should_panic]` to indicate that the test is
/// expected to panic. For example:
///
/// ```ignore
/// #[gtest]
/// #[should_panic]
/// fn passes_due_to_should_panic() {
/// let value = 2;
/// expect_that!(value, gt(0));
/// panic!("This panics");
/// }
/// ```
///
/// Using `#[should_panic]` modifies the behaviour of `#[gtest]` so
/// that the test panics (and passes) if any non-fatal assertion occurs.
/// For example, the following test passes:
///
/// ```ignore
/// #[gtest]
/// #[should_panic]
/// fn passes_due_to_should_panic_and_failing_assertion() {
/// let value = 2;
/// expect_that!(value, eq(0));
/// }
/// ```
///
/// [`googletest::Result`]: type.Result.html
#[proc_macro_attribute]
pub fn gtest(
_args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let ItemFn { mut attrs, sig, block, .. } = parse_macro_input!(input as ItemFn);
let sig_ident = &sig.ident;
let test_case_hash: u64 = {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
// Only consider attrs and name for stability. Changing the function body should
// not affect the test case distribution.
attrs.hash(&mut h);
sig_ident.hash(&mut h);
h.finish()
};
let (skipped_test_result, outer_return_type, trailer) = attrs
.iter()
.find(|attr| attr.path().is_ident("should_panic"))
.map(|attr| {
let error_message = extract_should_panic_expected(attr).unwrap_or("".to_string());
(
quote! {
{
panic!("{}", #error_message);
}
},
quote! { () },
quote! { .unwrap(); }
)})
.unwrap_or_else(||
(
quote! {Ok(())},
quote! { ::core::result::Result<(), googletest::internal::test_outcome::TestFailure> },
quote! {},
));
let is_rstest_enabled = is_rstest_enabled(&attrs);
let outer_sig = {
let mut outer_sig = sig.clone();
outer_sig.output = ReturnType::Default;
if !is_rstest_enabled {
outer_sig.inputs = Punctuated::new();
}
outer_sig
};
let output_type = match sig.output {
ReturnType::Default => None,
ReturnType::Type(_, ref ty) => Some(quote! {#ty}),
};
let is_async = sig.asyncness.is_some();
let maybe_async = is_async.then(|| quote! { async });
let maybe_await = is_async.then(|| quote! { .await });
let invocation = if is_rstest_enabled {
// Rstest may refer in block to its fixtures. Hence, we only wrap it in a
// closure to capture them.
let result_type = output_type.clone().unwrap_or_else(|| quote! {()});
let mut invocation = quote! {
(#maybe_async move || -> #result_type {
#block
})() #maybe_await
};
if output_type.is_none() {
invocation = quote! { { let () = #invocation; googletest::Result::<()>::Ok(()) } };
}
invocation
} else {
let closure_body = match closure_body(&sig) {
Ok(body) => body,
Err(e) => return e.into_compile_error().into(),
};
// In the sync case, the ? operator returns from the surrounding function. So
// we redeclare the original test function internally.
let result_type = output_type.unwrap_or_else(|| quote! {googletest::Result<()>});
quote! {
(#maybe_async move || -> #result_type {
#sig { #block }
#closure_body
})() #maybe_await
}
};
if !attrs.iter().any(is_test_attribute) && !is_rstest_enabled {
let test_attr: Attribute = parse_quote! {
#[::core::prelude::v1::test]
};
attrs.push(test_attr);
};
let function = quote! {
#(#attrs)*
#outer_sig -> #outer_return_type {
if !googletest::internal::test_filter::test_should_run(concat!(module_path!(), "::", stringify!(#sig_ident))) {
#skipped_test_result
} else if googletest::internal::test_sharding::test_should_run(#test_case_hash) {
use googletest::internal::test_outcome::TestOutcome;
TestOutcome::init_current_test_outcome();
TestOutcome::close_current_test_outcome(#invocation)
} else {
#skipped_test_result
}
#trailer
}
};
function.into()
}
/// Extract the optional "expected" string literal from a `should_panic`
/// attribute.
fn extract_should_panic_expected(attr: &Attribute) -> Option<String> {
let Ok(name_value) = attr.parse_args::<MetaNameValue>() else {
return None;
};
match name_value.value {
Expr::Lit(ExprLit { lit: Lit::Str(expected), .. })
if name_value.path.is_ident("expected") =>
{
Some(expected.value())
}
_ => None,
}
}
/// Alias for [`googletest::gtest`].
///
/// Generally, prefer using `#[gtest]` to mark googletest-based tests.
///
/// Use `#[test]` instead of `#[gtest]` to satisfy compatibility
/// requirements. For example, the rstest crate can be composed with other test
/// attributes but it requires the attribute to be named `test`.
///
/// ```ignore
/// #[rstest]
/// #[googletest::test]
/// fn rstest_with_googletest() -> Result<()> {
/// verify_that!(1, eq(1))
/// }
/// ```
///
/// [`googletest::gtest`]: attr.gtest.html
#[proc_macro_attribute]
pub fn test(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
gtest(args, input)
}
fn is_test_attribute(attr: &Attribute) -> bool {
match attr.path().segments.last() {
Some(last_segment) => last_segment.ident == "test",
None => false,
}
}
fn is_rstest_enabled(attributes: &[Attribute]) -> bool {
attributes.iter().any(|attr| matches!(attr.path().segments.last(), Some(last_segment) if last_segment.ident == "rstest"))
}
enum FixtureKind {
Consumable,
MutableRef,
SharedRef,
}
struct Fixture {
identifier: syn::Ident,
ty: Box<syn::Type>,
kind: FixtureKind,
}
impl Fixture {
fn new(index: usize, ty: Box<syn::Type>) -> syn::Result<Self> {
let identifier = syn::Ident::new(&format!("__googletest__fixture__{index}"), ty.span());
match &*ty {
Type::Reference(reference) => Ok(Self {
identifier,
ty: reference.elem.clone(),
kind: if reference.mutability.is_some() {
FixtureKind::MutableRef
} else {
FixtureKind::SharedRef
},
}),
Type::Path(..) => Ok(Self { identifier, ty, kind: FixtureKind::Consumable }),
_ => Err(syn::Error::new(
ty.span(),
"Unexpected fixture type. Only references (&T or &mut T) and paths (T) are supported.",
)),
}
}
fn wrap_call(&self, inner_call: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let Self { identifier, ty, kind } = self;
let ref_method = match kind {
FixtureKind::Consumable => {
return quote! {
#[allow(non_snake_case)]
let #identifier = <#ty as googletest::fixtures::ConsumableFixture>::set_up()?;
{ #inner_call }
};
}
FixtureKind::MutableRef => quote! { .as_mut() },
FixtureKind::SharedRef => quote! { .as_ref() },
};
quote! { {
#[allow(non_snake_case, unused_mut)]
let mut #identifier =
googletest::__internal_macro_support::FixtureTearDownOnDrop::new(
<#ty as googletest::fixtures::Fixture>::set_up()?);
let result = {
#[allow(non_snake_case)]
let #identifier = #identifier #ref_method;
#inner_call
};
#identifier.tear_down()?;
result
} }
}
}
fn closure_body(signature: &Signature) -> syn::Result<proc_macro2::TokenStream> {
let input_types = signature
.inputs
.iter()
.enumerate()
.map(|(index, typed)| {
let FnArg::Typed(PatType { ty, .. }) = typed else {
return Err(syn::Error::new(
typed.span(),
"`self` receiver is not accepted as test argument",
));
};
Fixture::new(index, ty.clone())
})
.collect::<syn::Result<Vec<Fixture>>>()?;
let mut block = {
let parameters = input_types.iter().map(|fixture| &fixture.identifier);
let test_name = &signature.ident;
let mut invocation = quote! {
#test_name(#(#parameters, )*)
};
if signature.asyncness.is_some() {
invocation = quote! { #invocation.await };
}
if let ReturnType::Default = signature.output {
invocation = quote! { { let () = #invocation; googletest::Result::Ok(()) } };
}
invocation
};
for fixture in input_types.iter().rev() {
block = fixture.wrap_call(block);
}
Ok(block)
}
#[proc_macro_derive(MatcherBase)]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let DeriveInput { ident, generics, .. } = ast;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #impl_generics MatcherBase for #ident #ty_generics #where_clause {}
}
.into()
}
mod matches_pattern;
/// This is an implementation detail of `googletest::matches_pattern!`.
///
/// It's not intended to be used directly.
#[doc(hidden)]
#[proc_macro]
pub fn __googletest_macro_matches_pattern(
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
matches_pattern::matches_pattern_impl(input)
}
mod verify_pred;
/// This is an implementation detail of `googletest::verify_pred!`.
///
/// It's not intended to be used directly.
#[doc(hidden)]
#[proc_macro]
pub fn __googletest_macro_verify_pred(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
verify_pred::verify_pred_impl(input)
}
/// Stringifies its argument (like `stringify!()` from the standard library) but
/// limits the output to a provided maximum length.
///
/// The input is a tuple of `target` and `max_length` seprated by a comma.
/// The `max_length` is the maximum number of characters to include in the
/// abbreviated string. For example:
///
/// ```ignore
/// #[rstest]
/// #[gtest]
/// fn test_abbreviated_string() -> Result<()> {
/// verifiy_eq!(__abbreviated_stringify!(|x| x + 1, 6), "|x|...")?;
/// Ok(())
/// }
/// ```
#[doc(hidden)]
#[proc_macro]
pub fn __abbreviated_stringify(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = input.to_string();
let abbreviated = abbreviated_string(&input).unwrap();
quote! {
#abbreviated
}
.into()
}
fn abbreviated_string(target: &str) -> Result<std::borrow::Cow<'_, str>, &'static str> {
use std::borrow::Cow;
match target.rsplit_once(',') {
None => Err("Expect a `max_length` argument, but got none"),
Some((expr, limit)) => match limit.trim().parse::<usize>() {
Ok(limit) if expr.len() > limit => {
if limit >= 4 {
Ok(Cow::Owned(format!("{}...", &expr[..limit - 3])))
} else {
Err("The `max_length` argument is too small. It must be at least 4.")
}
}
Ok(_) => Ok(Cow::Borrowed(expr)),
Err(_) => Err("The `max_length` argument is not a number."),
},
}
}
@@ -0,0 +1,415 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use proc_macro2::{Delimiter, Group, Ident, Span, TokenStream, TokenTree};
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream, Parser as _},
parse_macro_input,
token::DotDot,
Expr, ExprCall, Pat, Token,
};
/// This is an implementation detail of `googletest::matches_pattern!`. It
/// assumes that a few symbols from `googletest::matchers` have been imported
/// and that `$crate` has been aliased to `googletest` (which might otherwise
/// have been imported as a different alias), both of which are done
/// by `googletest::matches_pattern!` before calling this proc macro.
pub(crate) fn matches_pattern_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let result = parse_macro_input!(input as ParsedMatchPattern).into_matcher_expr();
quote! { #result }.into()
}
/// Represents one of the root-level pseudo-patterns supported by
/// `match_pattern!`.
///
/// Examples with `struct_name` set and `group` being `None`:
/// * `true`
/// * `Enum::Variant`
/// * `&Enum::Variant`
///
/// Examples with `group` being non-`None`.
/// * `Struct { a: eq(1), b: ends_with("foo") }`
/// * `&Struct(ref eq(&1), ends_with("foo"))`
#[derive(Debug)]
struct ParsedMatchPattern {
struct_name: TokenStream,
group: Option<Group>,
}
impl Parse for ParsedMatchPattern {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut struct_name: Vec<TokenTree> = vec![];
let mut group: Option<Group> = None;
// Use a TT-muncher (as opposed to parsing `Expr` or `Pat` or such) since:
// - The `struct_name` can be a struct/enum type or literal value (`true`, `1`,
// `&"test"`).
// - The braced part supports syntax that is not valid rust like `&Foo { a: ref
// eq(1) }`.
input.step(|cursor| {
let mut rest = *cursor;
while let Some((tt, next)) = rest.token_tree() {
// If we reached a group (`{...}` or `(...)`), that should be the end of the
// pattern. Other groups (`[...]`) are not supported in struct names, but that's
// checked later.
if let TokenTree::Group(g) = tt {
group = Some(g);
return Ok(((), next));
}
// Everything before the group is the struct/enum name, possibly prefixed with
// `&`.
struct_name.push(tt);
rest = next;
}
// If no group found, remove any trailing comma that might have been
// incorporated into the struct/enum name.
if matches!(struct_name.last(), Some(TokenTree::Punct(p)) if p.as_char() == ',') {
struct_name.pop();
}
Ok(((), rest))
})?;
input.parse::<Option<Token![,]>>()?;
let struct_name = struct_name.into_iter().collect();
Ok(ParsedMatchPattern { struct_name, group })
}
}
impl ParsedMatchPattern {
fn into_matcher_expr(self) -> TokenStream {
let Self { struct_name, group } = self;
// `matcher_pattern` supports both its custom (not necessarily valid rust)
// syntax and also native match pattern (like `Struct { .. }` and
// `Struct(_)`). So we need to speculatively attempt the first and then
// fall back to the latter if the first fails. If both fail, we return
// the error from the first attempt.
let mut first_err = None;
// `matcher_pattern` special syntax.
if let Some(ref g) = group {
let res = match g.delimiter() {
Delimiter::Parenthesis => parse_tuple_pattern_args(struct_name.clone(), g.stream()),
Delimiter::Brace => parse_braced_pattern_args(struct_name.clone(), g.stream()),
Delimiter::Bracket => compile_err(g.span(), "[...] syntax is not meaningful"),
Delimiter::None => compile_err(g.span(), "undelimited group not supported"),
};
match res {
Ok(res) => return res,
Err(e) => first_err = Some(e),
}
}
// Standard `match` pattern (prioritize `first_err` if both fail).
into_match_pattern_expr(quote! { #struct_name #group })
.map_err(|e| first_err.unwrap_or(e))
.unwrap_or_else(syn::Error::into_compile_error)
}
}
/// Returns a pattern match expression as long as `stream` is a valid pattern.
/// Otherwise, returns failure.
fn into_match_pattern_expr(stream: TokenStream) -> syn::Result<TokenStream> {
// Only produce if stream successfully parses as a pattern. Otherwise, return
// failure so that we can instead return the error due to failing to parse
// the `matcher_pattern` custom syntax.
Pat::parse_multi.parse2(stream.clone())?;
Ok(quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::pattern_only(
|v| matches!(v, #stream),
concat!("is ", stringify!(#stream)),
concat!("is not ", stringify!(#stream)))
})
}
////////////////////////////////////////////////////////////////////////////////
// Parse tuple struct patterns
/// Each part in a tuple matcher pattern that's between the commas. When `None`,
/// it represents `_` which matches anything.
struct MaybeTupleFieldPattern(Option<TupleFieldPattern>);
struct TupleFieldPattern {
ref_token: Option<Token![ref]>,
matcher: Expr,
}
impl Parse for MaybeTupleFieldPattern {
fn parse(input: ParseStream) -> syn::Result<Self> {
let pattern = match input.parse::<Option<Token![_]>>()? {
Some(_) => None,
None => Some(TupleFieldPattern { ref_token: input.parse()?, matcher: input.parse()? }),
};
Ok(MaybeTupleFieldPattern(pattern))
}
}
/// Parses a tuple struct's fields into a match expression.
fn parse_tuple_pattern_args(
struct_name: TokenStream,
group_content: TokenStream,
) -> syn::Result<TokenStream> {
let (patterns, dot_dot) =
parse_list_terminated_pattern::<MaybeTupleFieldPattern>.parse2(group_content)?;
let field_count = patterns.len();
let field_patterns: Vec<_> = patterns
.into_iter()
.enumerate()
.filter_map(|(index, maybe_pattern)| maybe_pattern.0.map(|pattern| (index, pattern)))
.map(|(index, TupleFieldPattern { ref_token, matcher })| {
let index = syn::Index::from(index);
quote! { googletest::matchers::field!(#struct_name.#index, #ref_token #matcher) }
})
.collect();
if field_patterns.is_empty() {
// It is possible that the logic above didn't generate any field matchers
// (e.g., for patterns like `Some(_)`).
// In this case we verify that the enum has the correct case, but don't
// verify the payload.
#[allow(clippy::manual_repeat_n)]
// `repeat_n` is not available on the Rust MSRV that we support in OSS
let ignored_fields = std::iter::repeat(quote! { _ })
.take(field_count)
.chain(dot_dot.map(ToTokens::into_token_stream));
let full_pattern = quote! { #struct_name ( #(#ignored_fields),* ) };
Ok(quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::pattern_only(
|actual| { matches!(actual, #full_pattern) },
concat!("is ", stringify!(#full_pattern)),
concat!("is not ", stringify!(#full_pattern))
)
})
} else {
// We have created at least one field matcher. Each field matcher will verify
// not only its part of the payload, but also that the enum has the
// correct case.
let matcher = quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::is(
stringify!(#struct_name),
all!( #(#field_patterns),* )
)
};
// Do a match to ensure:
// - Fields are exhaustively listed unless the pattern ended with `..`.
// - `UNDEFINED_SYMBOL(..)` fails to compile.
#[allow(clippy::manual_repeat_n)]
// `repeat_n` is not available on the Rust MSRV that we support in OSS
let empty_fields = std::iter::repeat(quote! { _ })
.take(field_count)
.chain(dot_dot.map(ToTokens::into_token_stream));
Ok(quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::compile_assert_and_match(
|actual| {
// Exhaustively check that all field names are specified.
match actual {
#struct_name ( #(#empty_fields),* ) => (),
// The pattern below is unreachable if the type is a struct (as opposed to
// an enum). Since the macro can't know which it is, we always include it
// and just tell the compiler not to complain.
#[allow(unreachable_patterns)]
_ => {},
}
},
#matcher)
})
}
}
////////////////////////////////////////////////////////////////////////////////
// Parse braced structs patterns
enum FieldOrMethod {
Field(Ident),
Method(ExprCall),
}
impl Parse for FieldOrMethod {
/// Parses the field name or method call along with the `:` that follows it.
fn parse(input: ParseStream) -> syn::Result<Self> {
// If the ident is followed by a single `:` and not a `::`, then it's a field
// match and not a method call match.
let value = if input.peek2(Token![:]) && !input.peek2(Token![::]) {
input.parse().map(FieldOrMethod::Field)
} else {
input.parse().map(FieldOrMethod::Method)
}?;
input.parse::<Token![:]>()?;
Ok(value)
}
}
/// Either field or method call matcher. E.g.:
/// * `field: starts_with("something")` or `field: _`
/// * `property(arg1, arg2): starts_with("something")
struct FieldOrMethodPattern {
ref_token: Option<Token![ref]>,
field_or_method: FieldOrMethod,
/// When `None`, it represents `_` which matches anything, meaning we should
/// ignore it.
matcher: Option<Expr>,
}
impl Parse for FieldOrMethodPattern {
fn parse(input: ParseStream) -> syn::Result<Self> {
let field_or_method: FieldOrMethod = input.parse()?;
let underscore = input.parse::<Option<Token![_]>>()?;
match underscore {
Some(underscore) if matches!(field_or_method, FieldOrMethod::Method(_)) => compile_err(
underscore.spans[0],
"Don't match a method call against `_`. Just omit it instead.",
),
Some(_) => Ok(FieldOrMethodPattern { field_or_method, ref_token: None, matcher: None }),
None => Ok(FieldOrMethodPattern {
field_or_method,
ref_token: input.parse()?,
matcher: Some(input.parse()?),
}),
}
}
}
/// Parses a struct's fields or method calls into a match expression.
fn parse_braced_pattern_args(
struct_name: TokenStream,
group_content: TokenStream,
) -> syn::Result<TokenStream> {
let (patterns, dot_dot) = parse_list_terminated_pattern.parse2(group_content)?;
let mut field_names = vec![];
let field_patterns: Vec<TokenStream> = patterns
.into_iter()
.filter_map(|FieldOrMethodPattern { ref_token, field_or_method, matcher }| {
match field_or_method {
FieldOrMethod::Field(ident) => {
field_names.push(ident.clone());
matcher.map(|matcher| {
quote! { field!(#struct_name . #ident, #ref_token #matcher) }
})
}
FieldOrMethod::Method(call) => {
Some(quote! { property!(#struct_name . #call, #ref_token #matcher) })
}
}
})
.collect();
if field_patterns.is_empty() {
// It is possible that the logic above didn't generate any field matchers
// (e.g., for patterns like `AnEnum::Foo { a_field : _ }`).
// In this case we verify that the enum has the correct case, but don't
// verify the payload.
let full_pattern = quote! { #struct_name { #(#field_names: _,)* #dot_dot}};
Ok(quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::pattern_only(
|actual| { matches!(actual, #full_pattern) },
concat!("is ", stringify!(#full_pattern)),
concat!("is not ", stringify!(#full_pattern))
)
})
} else {
// We have created at least one field matcher. Each field matcher will verify
// not only its part of the payload, but also that the enum has the
// correct case.
let matcher = quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::is(
stringify!(#struct_name),
all!(#(#field_patterns),* )
)
};
// Do a match to ensure:
// - Fields are exhaustively listed unless the pattern ended with `..` and has
// any fields in the pattern.
// - `UNDEFINED_SYMBOL { .. }` fails to compile.
//
// The requisite that some fields are in the pattern is there because
// `matches_pattern!` also uses the brace notation for tuple structs when
// asserting on method calls on tuple structs. i.e.
//
// ```
// struct Struct(u32);
// ...
// matches_pattern!(foo, Struct { bar(): eq(1) })
// ```
// and we can't emit an exhaustiveness check based on the `matches_pattern!`.
if field_names.is_empty() && dot_dot.is_none() &&
// If there are no fields, then this check means that there are method patterns, and we can
// no longer be confident that this is a braced struct rather than a tuple struct.
!field_patterns.is_empty()
{
Ok(matcher)
} else {
Ok(quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::compile_assert_and_match(
|actual| {
// Exhaustively check that all field names are specified.
match actual {
#struct_name { #(#field_names: _,)* #dot_dot } => {},
// The pattern below is unreachable if the type is a struct (as opposed to
// an enum). Since the macro can't know which it is, we always include it
// and just tell the compiler not to complain.
#[allow(unreachable_patterns)]
_ => {},
}
},
#matcher)
})
}
}
}
////////////////////////////////////////////////////////////////////////////////
// General-purpose helpers
/// Returns the parsed struct pattern body along with a `..` if it appears at
/// the end of the body.
///
/// This is like `Punctuated::parse_terminated`, but additionally allows for an
/// optional `..`, which cannot be followed by a comma.
fn parse_list_terminated_pattern<T: Parse>(
input: ParseStream<'_>,
) -> syn::Result<(Vec<T>, Option<DotDot>)> {
let mut patterns = vec![];
while !input.is_empty() {
// Check for trailing `..`.
let dot_dot = input.parse::<Option<Token![..]>>()?;
if dot_dot.is_some() {
// Must be at the end of the group content.
return if input.is_empty() {
Ok((patterns, dot_dot))
} else {
compile_err(input.span(), "`..` must be at the end of the struct pattern")
};
}
// Otherwise, parse the field/method patterns.
patterns.push(input.parse::<T>()?);
if input.is_empty() {
break;
}
input.parse::<Token![,]>()?;
}
Ok((patterns, None))
}
fn compile_err<T>(span: Span, message: &str) -> syn::Result<T> {
Err(syn::Error::new(span, message))
}
@@ -0,0 +1,183 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use quote::quote;
use syn::{parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, Expr, Ident};
struct AccumulatePartsState {
var_num: usize,
error_message_ident: Ident,
statements: Vec<proc_macro2::TokenStream>,
}
fn expr_to_string(expr: &Expr) -> String {
quote!(#expr).to_string()
}
impl AccumulatePartsState {
fn new() -> Self {
Self {
var_num: 0,
error_message_ident: Ident::new(
"__googletest__verify_pred__error_message",
::proc_macro2::Span::call_site(),
),
statements: vec![],
}
}
/// Takes an expression with chained field accesses and method calls and
/// accumulates intermediate expressions used for computing `verify_pred!`'s
/// expression, including intermediate variable assignments to evaluate
/// parts of the expression exactly once, and the format string used to
/// output intermediate values on condition failure. It returns the new form
/// of the input expression with parts of it potentially replaced by the
/// intermediate variables.
fn accumulate_parts(&mut self, expr: Expr) -> Expr {
// Literals don't need to be printed or stored in intermediate variables.
if is_literal(&expr) {
return expr;
}
let expr_string = expr_to_string(&expr);
let new_expr = match expr {
Expr::Group(mut group) => {
// This is an invisible group added for correct precedence in the AST. Just pass
// through without having a separate printing result.
*group.expr = self.accumulate_parts(*group.expr);
return Expr::Group(group);
}
Expr::Field(mut field) => {
// Don't assign field access to an intermediate variable to avoid moving out of
// non-`Copy` fields.
*field.base = self.accumulate_parts(*field.base);
Expr::Field(field)
}
Expr::Call(mut call) => {
// Cache args into intermediate variables.
call.args = self.define_variables_for_args(call.args);
// Cache function value into an intermediate variable.
self.define_variable(&Expr::Call(call))
}
Expr::MethodCall(mut method_call) => {
*method_call.receiver = self.accumulate_parts(*method_call.receiver);
// Cache args into intermediate variables.
method_call.args = self.define_variables_for_args(method_call.args);
// Cache method value into an intermediate variable.
self.define_variable(&Expr::MethodCall(method_call))
}
Expr::Binary(mut binary) => {
*binary.left = self.accumulate_parts(*binary.left);
*binary.right = self.accumulate_parts(*binary.right);
Expr::Binary(binary)
}
Expr::Unary(mut unary) => {
*unary.expr = self.accumulate_parts(*unary.expr);
Expr::Unary(unary)
}
// A path expression doesn't need to be stored in an intermediate variable.
// This avoids moving out of an existing variable.
Expr::Path(_) => expr,
// By default, assume it's some expression that needs to be cached to avoid
// double-evaluation.
_ => self.define_variable(&expr),
};
let error_message_ident = &self.error_message_ident;
self.statements.push(quote! {
::googletest::fmt::internal::__googletest__write_expr_value!(
&mut #error_message_ident,
#expr_string,
#new_expr,
);
});
new_expr
}
// Defines a variable for each argument expression so that it's evaluated
// exactly once.
fn define_variables_for_args(
&mut self,
args: Punctuated<Expr, Comma>,
) -> Punctuated<Expr, Comma> {
args.into_pairs()
.map(|mut pair| {
// Don't need to assign literals to intermediate variables.
if is_literal(pair.value()) {
return pair;
}
let var_expr = self.define_variable(pair.value());
let error_message_ident = &self.error_message_ident;
let expr_string = expr_to_string(pair.value());
self.statements.push(quote! {
::googletest::fmt::internal::__googletest__write_expr_value!(
&mut #error_message_ident,
#expr_string,
#var_expr,
);
});
*pair.value_mut() = var_expr;
pair
})
.collect()
}
/// Defines a new variable assigned to the expression and returns the
/// variable as an expression to be used in place of the passed-in
/// expression.
fn define_variable(&mut self, value: &Expr) -> Expr {
let var_name =
Ident::new(&format!("__googletest__verify_pred__var{}", self.var_num), value.span());
self.var_num += 1;
self.statements.push(quote! {
#[allow(non_snake_case)]
let mut #var_name = #value;
});
syn::parse::<Expr>(quote!(#var_name).into()).unwrap()
}
}
// Whether it's a literal or unary operator applied to a literal (1, -1).
fn is_literal(expr: &Expr) -> bool {
match expr {
Expr::Lit(_) => true,
Expr::Unary(unary) => matches!(&*unary.expr, Expr::Lit(_)),
_ => false,
}
}
pub fn verify_pred_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let parsed = parse_macro_input!(input as Expr);
let error_message = quote!(#parsed).to_string() + " was false with";
let mut state = AccumulatePartsState::new();
let pred_value = state.accumulate_parts(parsed);
let AccumulatePartsState { error_message_ident, mut statements, .. } = state;
let _ = statements.pop(); // The last statement prints the full expression itself.
quote! {
{
let mut #error_message_ident = #error_message.to_string();
#(#statements)*
if (#pred_value) {
Ok(())
} else {
::core::result::Result::Err(
::googletest::internal::test_outcome::TestAssertionFailure::create(
#error_message_ident))
}
}
}
.into()
}