xref: /aosp_15_r20/external/cronet/testing/rust_gtest_interop/gtest_attribute.rs (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use proc_macro2::TokenStream;
6 use quote::{format_ident, quote, quote_spanned, ToTokens};
7 use syn::parse::{Parse, ParseStream};
8 use syn::spanned::Spanned;
9 use syn::{parse_macro_input, Error, Ident, ItemFn, ItemImpl, LitStr, Token, Type};
10 
11 /// The prefix attached to a Gtest factory function by the
12 /// RUST_GTEST_TEST_SUITE_FACTORY() macro.
13 const RUST_GTEST_FACTORY_PREFIX: &str = "RustGtestFactory_";
14 
15 struct GtestArgs {
16     suite_name: String,
17     test_name: String,
18 }
19 
20 impl Parse for GtestArgs {
parse(input: ParseStream) -> Result<Self, Error>21     fn parse(input: ParseStream) -> Result<Self, Error> {
22         let suite_name = input.parse::<Ident>()?.to_string();
23         input.parse::<Token![,]>()?;
24         let test_name = input.parse::<Ident>()?.to_string();
25         Ok(GtestArgs { suite_name, test_name })
26     }
27 }
28 
29 struct GtestSuiteArgs {
30     rust_type: Type,
31 }
32 
33 impl Parse for GtestSuiteArgs {
parse(input: ParseStream) -> Result<Self, Error>34     fn parse(input: ParseStream) -> Result<Self, Error> {
35         let rust_type = input.parse::<Type>()?;
36         Ok(GtestSuiteArgs { rust_type })
37     }
38 }
39 
40 struct ExternTestSuiteArgs {
41     cpp_type: TokenStream,
42 }
43 
44 impl Parse for ExternTestSuiteArgs {
parse(input: ParseStream) -> Result<Self, Error>45     fn parse(input: ParseStream) -> Result<Self, Error> {
46         // TODO(b/229791967): With CXX it is not possible to get the C++ typename and
47         // path from the Rust wrapper type, so we require specifying it by hand in
48         // the macro. It would be nice to remove this opportunity for mistakes.
49         let cpp_type_as_lit_str = input.parse::<LitStr>()?;
50 
51         // TODO(danakj): This code drops the C++ namespaces, because we can't produce a
52         // mangled name and can't generate bindings involving fn pointers, so we require
53         // the C++ function to be `extern "C"` which means it has no namespace.
54         // Eventually we should drop the `extern "C"` on the C++ side and use the
55         // full path here.
56         match cpp_type_as_lit_str.value().split("::").last() {
57             Some(name) => {
58                 Ok(ExternTestSuiteArgs { cpp_type: format_ident!("{}", name).into_token_stream() })
59             }
60             None => Err(Error::new(cpp_type_as_lit_str.span(), "invalid C++ class name")),
61         }
62     }
63 }
64 
65 struct CppPrefixArgs {
66     cpp_prefix: String,
67 }
68 
69 impl Parse for CppPrefixArgs {
parse(input: ParseStream) -> Result<Self, Error>70     fn parse(input: ParseStream) -> Result<Self, Error> {
71         let cpp_prefix_as_lit_str = input.parse::<LitStr>()?;
72         Ok(CppPrefixArgs { cpp_prefix: cpp_prefix_as_lit_str.value() })
73     }
74 }
75 
76 /// The `gtest` macro can be placed on a function to make it into a Gtest unit
77 /// test, when linked into a C++ binary that invokes Gtest.
78 ///
79 /// The `gtest` macro takes two arguments, which are Rust identifiers. The first
80 /// is the name of the test suite and the second is the name of the test, each
81 /// of which are converted to a string and given to Gtest. The name of the test
82 /// function itself does not matter, and need not be unique (it's placed into a
83 /// unique module based on the Gtest suite + test names.
84 ///
85 /// The test function must have no arguments. The return value must be either
86 /// `()` or `std::result::Result<(), E>`. If another return type is found, the
87 /// test will fail when run. If the return type is a `Result`, then an `Err` is
88 /// treated as a test failure.
89 ///
90 /// # Examples
91 /// ```
92 /// #[gtest(MathTest, Addition)]
93 /// fn my_test() {
94 ///   expect_eq!(1 + 1, 2);
95 /// }
96 /// ```
97 ///
98 /// The above adds the function to the Gtest binary as `MathTest.Addtition`:
99 /// ```
100 /// [ RUN      ] MathTest.Addition
101 /// [       OK ] MathTest.Addition (0 ms)
102 /// ```
103 ///
104 /// A test with a Result return type, and which uses the `?` operator. It will
105 /// fail if the test returns an `Err`, and print the resulting error string:
106 /// ```
107 /// #[gtest(ResultTest, CheckThingWithResult)]
108 /// fn my_test() -> std::result::Result<(), String> {
109 ///   call_thing_with_result()?;
110 /// }
111 /// ```
112 #[proc_macro_attribute]
gtest( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream113 pub fn gtest(
114     args: proc_macro::TokenStream,
115     input: proc_macro::TokenStream,
116 ) -> proc_macro::TokenStream {
117     let GtestArgs { suite_name, test_name } = parse_macro_input!(args as GtestArgs);
118 
119     let (input_fn, gtest_suite_attr) = {
120         let mut input_fn = parse_macro_input!(input as ItemFn);
121 
122         if let Some(asyncness) = input_fn.sig.asyncness {
123             // TODO(crbug.com/1288947): We can support async functions once we have
124             // block_on() support which will run a RunLoop until the async test
125             // completes. The run_test_fn just needs to be generated to `block_on(||
126             // #test_fn)` instead of calling `#test_fn` synchronously.
127             return quote_spanned! {
128                 asyncness.span =>
129                     compile_error!("async functions are not supported.");
130             }
131             .into();
132         }
133 
134         // Filter out other gtest attributes on the test function and save them for
135         // later processing.
136         let mut gtest_suite_attr = None;
137         input_fn.attrs = input_fn
138             .attrs
139             .into_iter()
140             .filter_map(|attr| {
141                 if attr.path().is_ident("gtest_suite") {
142                     gtest_suite_attr = Some(attr);
143                     None
144                 } else {
145                     Some(attr)
146                 }
147             })
148             .collect::<Vec<_>>();
149 
150         (input_fn, gtest_suite_attr)
151     };
152 
153     // The identifier of the function which contains the body of the test.
154     let test_fn = &input_fn.sig.ident;
155 
156     let (gtest_factory_fn, test_fn_call) = if let Some(attr) = gtest_suite_attr {
157         // If present, the gtest_suite attribute is expected to have the form
158         // `#[gtest_suite(path::to::RustType)]`. The Rust type wraps a C++
159         // `TestSuite` (subclass of `::testing::Test`) which should be created
160         // and returned by a C++ factory function.
161         let rust_type = match attr.parse_args::<GtestSuiteArgs>() {
162             Ok(x) => x.rust_type,
163             Err(x) => return x.to_compile_error().into(),
164         };
165 
166         (
167             // Get the Gtest factory function pointer from the TestSuite trait.
168             quote! { <#rust_type as ::rust_gtest_interop::TestSuite>::gtest_factory_fn_ptr() },
169             // SAFETY: Our lambda casts the `suite` reference and does not move from it, and
170             // the resulting type is not Unpin.
171             quote! {
172                 let p = unsafe {
173                     suite.map_unchecked_mut(|suite: &mut ::rust_gtest_interop::OpaqueTestingTest| {
174                         suite.as_mut()
175                     })
176                 };
177                 #test_fn(p)
178             },
179         )
180     } else {
181         // Otherwise, use `rust_gtest_interop::rust_gtest_default_factory()`
182         // which makes a `TestSuite` with `testing::Test` directly.
183         (
184             quote! { ::rust_gtest_interop::__private::rust_gtest_default_factory },
185             quote! { #test_fn() },
186         )
187     };
188 
189     // The test function and all code generate by this proc macroa go into a
190     // submodule which is uniquely named for the super module based on the Gtest
191     // suite and test names. If two tests have the same suite + test name, this
192     // will result in a compiler error—this is OK because Gtest disallows
193     // dynamically registering multiple tests with the same suite + test name.
194     let test_mod = format_ident!("__test_{}_{}", suite_name, test_name);
195 
196     // In the generated code, `run_test_fn` is marked #[no_mangle] to work around a
197     // codegen bug where the function is seen as dead and the compiler omits it
198     // from the object files. Since it's #[no_mangle], the identifier must be
199     // globally unique or we have an ODR violation. To produce a unique
200     // identifier, we roll our own name mangling by combining the file name and
201     // path from the source tree root with the Gtest suite and test names and the
202     // function itself.
203     //
204     // Note that an adversary could still produce a bug here by placing two equal
205     // Gtest suite and names in a single .rs file but in separate inline
206     // submodules.
207     //
208     // TODO(dcheng): This probably can be simplified to not bother with anything
209     // other than the suite and test name, given Gtest's restrictions for a
210     // given suite + test name pair to be globally unique within a test binary.
211     let mangled_function_name = |f: &syn::ItemFn| -> syn::Ident {
212         let file_name = file!().replace(|c: char| !c.is_ascii_alphanumeric(), "_");
213         format_ident!("{}_{}_{}_{}", file_name, suite_name, test_name, f.sig.ident)
214     };
215 
216     let run_test_fn = format_ident!("run_test_{}", mangled_function_name(&input_fn));
217 
218     // Implements ToTokens to generate a reference to a static-lifetime,
219     // null-terminated, C-String literal. It is represented as an array of type
220     // std::os::raw::c_char which can be either signed or unsigned depending on
221     // the platform, and it can be passed directly to C++. This differs from
222     // byte strings and CStr which work with `u8`.
223     //
224     // TODO(crbug.com/1298175): Would it make sense to write a c_str_literal!()
225     // macro that takes a Rust string literal and produces a null-terminated
226     // array of `c_char`? Then you could write `c_str_literal!(file!())` for
227     // example, or implement a `file_c_str!()` in this way. Explore using https://crates.io/crates/cstr.
228     //
229     // TODO(danakj): Write unit tests for this, and consider pulling this out into
230     // its own crate, if we don't replace it with c_str_literal!() or the "cstr"
231     // crate.
232     struct CStringLiteral<'a>(&'a str);
233     impl quote::ToTokens for CStringLiteral<'_> {
234         fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
235             let mut c_chars = self.0.chars().map(|c| c as std::os::raw::c_char).collect::<Vec<_>>();
236             c_chars.push(0);
237             // Verify there's no embedded nulls as that would be invalid if the literal were
238             // put in a std::ffi::CString.
239             assert_eq!(c_chars.iter().filter(|x| **x == 0).count(), 1);
240             let comment = format!("\"{}\" as [c_char]", self.0);
241             tokens.extend(quote! {
242                 {
243                     #[doc=#comment]
244                     &[#(#c_chars as std::os::raw::c_char),*]
245                 }
246             });
247         }
248     }
249 
250     // C-compatible string literals, that can be inserted into the quote! macro.
251     let suite_name_c_bytes = CStringLiteral(&suite_name);
252     let test_name_c_bytes = CStringLiteral(&test_name);
253     let file_c_bytes = CStringLiteral(file!());
254 
255     let output = quote! {
256         #[cfg(not(is_gtest_unittests))]
257         compile_error!(
258             "#[gtest(...)] can only be used in targets where the GN \
259             variable `is_gtest_unittests` is set to `true`.");
260 
261         mod #test_mod {
262             use super::*;
263 
264             #[::rust_gtest_interop::small_ctor::ctor]
265             unsafe fn register_test() {
266                 let r = ::rust_gtest_interop::__private::TestRegistration {
267                     func: #run_test_fn,
268                     test_suite_name: #suite_name_c_bytes,
269                     test_name: #test_name_c_bytes,
270                     file: #file_c_bytes,
271                     line: line!(),
272                     factory: #gtest_factory_fn,
273                 };
274                 ::rust_gtest_interop::__private::register_test(r);
275             }
276 
277             // The function is extern "C" so `register_test()` can pass this fn as a pointer to C++
278             // where it's registered with gtest.
279             //
280             // TODO(crbug.com/1296284): Removing #[no_mangle] makes rustc drop the symbol for the
281             // test function in the generated rlib which produces linker errors. If we resolve the
282             // linked bug and emit real object files from rustc for linking, then all the required
283             // symbols are present and `#[no_mangle]` should go away along with the custom-mangling
284             // of `run_test_fn`. We can not use `pub` to resolve this unfortunately. When `#[used]`
285             // is fixed in https://github.com/rust-lang/rust/issues/47384, this may also be
286             // resolved as well.
287             #[no_mangle]
288             extern "C" fn #run_test_fn(
289                 suite: std::pin::Pin<&mut ::rust_gtest_interop::OpaqueTestingTest>
290             ) {
291                 let catch_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
292                     #test_fn_call
293                 }));
294                 use ::rust_gtest_interop::TestResult;
295                 let err_message: Option<String> = match catch_result {
296                     Ok(fn_result) => TestResult::into_error_message(fn_result),
297                     Err(_) => Some("Test panicked".to_string()),
298                 };
299                 if let Some(m) = err_message.as_ref() {
300                     ::rust_gtest_interop::__private::add_failure_at(file!(), line!(), &m);
301                 }
302             }
303 
304             #input_fn
305         }
306     };
307 
308     output.into()
309 }
310 
311 /// The `#[extern_test_suite()]` macro is used to implement the unsafe
312 /// `TestSuite` trait.
313 ///
314 /// The `TestSuite` trait is used to mark a Rust type as being a wrapper of a
315 /// C++ subclass of `testing::Test`. This makes it valid to cast from a `*mut
316 /// testing::Test` to a pointer of the marked Rust type.
317 ///
318 /// It also marks a promise that on the C++, there exists an instantiation of
319 /// the RUST_GTEST_TEST_SUITE_FACTORY() macro for the C++ subclass type which
320 /// will be linked with the Rust crate.
321 ///
322 /// The macro takes a single parameter which is the fully specified C++ typename
323 /// of the C++ subclass for which the implementing Rust type is a wrapper. It
324 /// expects the body of the trait implementation to be empty, as it will fill in
325 /// the required implementation.
326 ///
327 /// # Example
328 /// If in C++ we have:
329 /// ```cpp
330 /// class GoatTestSuite : public testing::Test {}
331 /// RUST_GTEST_TEST_SUITE_FACTORY(GoatTestSuite);
332 /// ```
333 ///
334 /// And in Rust we have a `ffi::GoatTestSuite` type generated to wrap the C++
335 /// type. The the type can be marked as a valid TestSuite with the
336 /// `#[extern_test_suite]` macro: ```rs
337 /// #[extern_test_suite("GoatTestSuite")]
338 /// unsafe impl rust_gtest_interop::TestSuite for ffi::GoatTestSuite {}
339 /// ```
340 ///
341 /// # Internals
342 /// The #[cpp_prefix("STRING_")] attribute can follow `#[extern_test_suite()]`
343 /// to control the path to the C++ Gtest factory function. This is used for
344 /// connecting to different C++ macros than the usual
345 /// RUST_GTEST_TEST_SUITE_FACTORY().
346 #[proc_macro_attribute]
extern_test_suite( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream347 pub fn extern_test_suite(
348     args: proc_macro::TokenStream,
349     input: proc_macro::TokenStream,
350 ) -> proc_macro::TokenStream {
351     // TODO(b/229791967): With CXX it is not possible to get the C++ typename and
352     // path from the Rust wrapper type, so we require specifying it by hand in
353     // the macro. It would be nice to remove this opportunity for mistakes.
354     let ExternTestSuiteArgs { cpp_type } = parse_macro_input!(args as ExternTestSuiteArgs);
355 
356     // Filter out other gtest attributes on the trait impl and save them for later
357     // processing.
358     let (trait_impl, cpp_prefix_attr) = {
359         let mut trait_impl = parse_macro_input!(input as ItemImpl);
360 
361         if !trait_impl.items.is_empty() {
362             return quote_spanned! {trait_impl.items[0].span() => compile_error!(
363                 "expected empty trait impl"
364             )}
365             .into();
366         }
367 
368         let mut cpp_prefix_attr = None;
369         trait_impl.attrs = trait_impl
370             .attrs
371             .into_iter()
372             .filter_map(|attr| {
373                 if attr.path().is_ident("cpp_prefix") {
374                     cpp_prefix_attr = Some(attr);
375                     None
376                 } else {
377                     Some(attr)
378                 }
379             })
380             .collect::<Vec<_>>();
381 
382         (trait_impl, cpp_prefix_attr)
383     };
384 
385     let cpp_prefix = if let Some(attr) = cpp_prefix_attr {
386         // If present, the cpp_prefix attribute is expected to have the form
387         // `#[cpp_prefix("PREFIX_STRING_")]`.
388         match attr.parse_args::<CppPrefixArgs>() {
389             Ok(cpp_prefix_args) => cpp_prefix_args.cpp_prefix,
390             Err(x) => return x.to_compile_error().into(),
391         }
392     } else {
393         RUST_GTEST_FACTORY_PREFIX.to_string()
394     };
395 
396     let trait_name = match &trait_impl.trait_ {
397         Some((_, path, _)) => path,
398         None => {
399             return quote! {compile_error!(
400                 "expected impl rust_gtest_interop::TestSuite trait"
401             )}
402             .into();
403         }
404     };
405 
406     let rust_type = match &*trait_impl.self_ty {
407         Type::Path(type_path) => type_path,
408         _ => {
409             return quote_spanned! {trait_impl.self_ty.span() => compile_error!(
410                 "expected type that wraps C++ subclass of `testing::Test`"
411             )}
412             .into();
413         }
414     };
415 
416     // TODO(danakj): We should generate a C++ mangled name here, then we don't
417     // require the function to be `extern "C"` (or have the author write the
418     // mangled name themselves).
419     let cpp_fn_name = format_ident!("{}{}", cpp_prefix, cpp_type.to_string());
420 
421     let output = quote! {
422         unsafe impl #trait_name for #rust_type {
423             fn gtest_factory_fn_ptr() -> rust_gtest_interop::GtestFactoryFunction {
424                 extern "C" {
425                     fn #cpp_fn_name(
426                         f: extern "C" fn(
427                             test_body: ::std::pin::Pin<&mut ::rust_gtest_interop::OpaqueTestingTest>
428                         )
429                     ) -> ::std::pin::Pin<&'static mut ::rust_gtest_interop::OpaqueTestingTest>;
430                 }
431                 #cpp_fn_name
432             }
433         }
434     };
435 
436     output.into()
437 }
438