| use proc_macro2::TokenStream; |
| use quote::{format_ident, quote, quote_spanned, ToTokens}; |
| use syn::spanned::Spanned; |
| |
| /// The prefix attached to a Gtest factory function by the |
| /// RUST_GTEST_TEST_SUITE_FACTORY() macro. |
| const RUST_GTEST_FACTORY_PREFIX: &str = "RustGtestFactory_"; |
| |
| /// The `gtest` macro can be placed on a function to make it into a Gtest unit |
| /// test, when linked into a C++ binary that invokes Gtest. |
| /// |
| /// The `gtest` macro takes two arguments, which are Rust identifiers. The first |
| /// is the name of the test suite and the second is the name of the test, each |
| /// of which are converted to a string and given to Gtest. The name of the test |
| /// function itself does not matter, and need not be unique (it's placed into a |
| /// unique module based on the Gtest suite + test names. |
| /// |
| /// The test function must have no arguments. The return value must be either |
| /// `()` or `std::result::Result<(), E>`. If another return type is found, the |
| /// test will fail when run. If the return type is a `Result`, then an `Err` is |
| /// treated as a test failure. |
| /// |
| /// # Examples |
| /// ``` |
| /// #[gtest(MathTest, Addition)] |
| /// fn my_test() { |
| /// expect_eq!(1 + 1, 2); |
| /// } |
| /// ``` |
| /// |
| /// The above adds the function to the Gtest binary as `MathTest.Addtition`: |
| /// ``` |
| /// [ RUN ] MathTest.Addition |
| /// [ OK ] MathTest.Addition (0 ms) |
| /// ``` |
| /// |
| /// A test with a Result return type, and which uses the `?` operator. It will |
| /// fail if the test returns an `Err`, and print the resulting error string: |
| /// ``` |
| /// #[gtest(ResultTest, CheckThingWithResult)] |
| /// fn my_test() -> std::result::Result<(), String> { |
| /// call_thing_with_result()?; |
| /// } |
| /// ``` |
| #[proc_macro_attribute] |
| pub fn gtest( |
| arg_stream: proc_macro::TokenStream, |
| input: proc_macro::TokenStream, |
| ) -> proc_macro::TokenStream { |
| enum GtestAttributeArgument { |
| TestSuite, |
| TestName, |
| } |
| // Returns a string representation of an identifier argument to the attribute. |
| // For example, for #[gtest(Foo, Bar)], this function would return "Foo" for |
| // position 0 and "Bar" for position 1. If the argument is not a Rust |
| // identifier or not present, it returns a compiler error as a TokenStream |
| // to be emitted. |
| fn get_arg_string( |
| args: &syn::AttributeArgs, |
| which: GtestAttributeArgument, |
| ) -> Result<String, TokenStream> { |
| let pos = match which { |
| GtestAttributeArgument::TestSuite => 0, |
| GtestAttributeArgument::TestName => 1, |
| }; |
| match &args[pos] { |
| syn::NestedMeta::Meta(syn::Meta::Path(path)) if path.segments.len() == 1 => { |
| Ok(path.segments[0].ident.to_string()) |
| } |
| _ => { |
| let error_stream = match which { |
| GtestAttributeArgument::TestSuite => { |
| quote_spanned! { |
| args[pos].span() => |
| compile_error!( |
| "Expected a test suite name, written as an identifier." |
| ); |
| } |
| } |
| GtestAttributeArgument::TestName => { |
| quote_spanned! { |
| args[pos].span() => |
| compile_error!( |
| "Expected a test name, written as an identifier." |
| ); |
| } |
| } |
| }; |
| Err(error_stream) |
| } |
| } |
| } |
| /// Parses `#[gtest_suite(path::to::RustType)]` and returns |
| /// `path::to::RustType`. |
| fn parse_gtest_suite(attr: &syn::Attribute) -> Result<TokenStream, TokenStream> { |
| let parsed = match attr.parse_meta() { |
| Ok(syn::Meta::List(list)) if list.nested.len() == 1 => match &list.nested[0] { |
| syn::NestedMeta::Meta(syn::Meta::Path(fn_path)) => Ok(fn_path.into_token_stream()), |
| x => Err(x.span()), |
| }, |
| Ok(x) => Err(x.span()), |
| Err(x) => Err(x.span()), |
| }; |
| parsed.or_else(|span| { |
| Err(quote_spanned! { span => |
| compile_error!( |
| "invalid syntax for gtest_suite macro, \ |
| expected `#[gtest_suite(path::to:RustType)]`"); |
| }) |
| }) |
| } |
| |
| let args = syn::parse_macro_input!(arg_stream as syn::AttributeArgs); |
| let mut input_fn = syn::parse_macro_input!(input as syn::ItemFn); |
| |
| // Populated data from the #[gtest_suite] macro arguments. |
| // |
| // The Rust type wrapping a C++ TestSuite (subclass of `testing::Test`), which |
| // is created and returned by a C++ factory function. If no type is |
| // specified, then this is left as None, and the default C++ factory |
| // function will be used to make a `testing::Test` directly. |
| let mut gtest_test_suite_wrapper_type: Option<TokenStream> = None; |
| |
| // Look through other attributes on the test function, parse the ones related to |
| // Gtests, and put the rest back into `attrs`. |
| input_fn.attrs = { |
| let mut keep = Vec::new(); |
| for attr in std::mem::take(&mut input_fn.attrs) { |
| if attr.path.is_ident("gtest_suite") { |
| let rust_type_name = match parse_gtest_suite(&attr) { |
| Ok(tokens) => tokens, |
| Err(error_tokens) => return error_tokens.into(), |
| }; |
| gtest_test_suite_wrapper_type = Some(rust_type_name); |
| } else { |
| keep.push(attr) |
| } |
| } |
| keep |
| }; |
| |
| // No longer mut. |
| let input_fn = input_fn; |
| let gtest_test_suite_wrapper_type = gtest_test_suite_wrapper_type; |
| |
| if let Some(asyncness) = input_fn.sig.asyncness { |
| // TODO(crbug.com/1288947): We can support async functions once we have |
| // block_on() support which will run a RunLoop until the async test |
| // completes. The run_test_fn just needs to be generated to `block_on(|| |
| // #test_fn)` instead of calling `#test_fn` synchronously. |
| return quote_spanned! { |
| asyncness.span => |
| compile_error!("async functions are not supported."); |
| } |
| .into(); |
| } |
| |
| let (test_suite_name, test_name) = match args.len() { |
| 2 => { |
| let suite = match get_arg_string(&args, GtestAttributeArgument::TestSuite) { |
| Ok(ok) => ok, |
| Err(error_stream) => return error_stream.into(), |
| }; |
| let test = match get_arg_string(&args, GtestAttributeArgument::TestName) { |
| Ok(ok) => ok, |
| Err(error_stream) => return error_stream.into(), |
| }; |
| (suite, test) |
| } |
| 0 | 1 => { |
| return quote! { |
| compile_error!( |
| "Expected two arguments. For example: #[gtest(TestSuite, TestName)]."); |
| } |
| .into(); |
| } |
| x => { |
| return quote_spanned! { |
| args[x.min(2)].span() => |
| compile_error!( |
| "Expected two arguments. For example: #[gtest(TestSuite, TestName)]."); |
| } |
| .into(); |
| } |
| }; |
| |
| // We put the test function and all the code we generate around it into a |
| // submodule which is uniquely named for the super module based on the Gtest |
| // suite and test names. A result of this is that if two tests have the same |
| // test suite + name, a compiler error would report the conflict. |
| let test_mod = format_ident!("__test_{}_{}", test_suite_name, test_name); |
| |
| // The run_test_fn identifier is marked #[no_mangle] to work around a codegen |
| // bug where the function is seen as dead and the compiler omits it from the |
| // object files. Since it's #[no_mangle], the identifier must be globally |
| // unique or we have an ODR violation. To produce a unique identifier, we |
| // roll our own name mangling by combining the file name and path from |
| // the source tree root with the Gtest suite and test names and the function |
| // itself. |
| // |
| // Note that an adversary could still produce a bug here by placing two equal |
| // Gtest suite and names in a single .rs file but in separate inline |
| // submodules. |
| let mangled_function_name = |f: &syn::ItemFn| -> syn::Ident { |
| let file_name = file!().replace(|c: char| !c.is_ascii_alphanumeric(), "_"); |
| format_ident!("{}_{}_{}_{}", file_name, test_suite_name, test_name, f.sig.ident) |
| }; |
| let run_test_fn = format_ident!("run_test_{}", mangled_function_name(&input_fn)); |
| |
| // The identifier of the function which contains the body of the test. |
| let test_fn = &input_fn.sig.ident; |
| |
| // Implements ToTokens to generate a reference to a static-lifetime, |
| // null-terminated, C-String literal. It is represented as an array of type |
| // std::os::raw::c_char which can be either signed or unsigned depending on |
| // the platform, and it can be passed directly to C++. This differs from |
| // byte strings and CStr which work with `u8`. |
| // |
| // TODO(crbug.com/1298175): Would it make sense to write a c_str_literal!() |
| // macro that takes a Rust string literal and produces a null-terminated |
| // array of `c_char`? Then you could write `c_str_literal!(file!())` for |
| // example, or implement a `file_c_str!()` in this way. Explore using https://crates.io/crates/cstr. |
| // |
| // TODO(danakj): Write unit tests for this, and consider pulling this out into |
| // its own crate, if we don't replace it with c_str_literal!() or the "cstr" |
| // crate. |
| struct CStringLiteral<'a>(&'a str); |
| impl quote::ToTokens for CStringLiteral<'_> { |
| fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { |
| let mut c_chars = self.0.chars().map(|c| c as std::os::raw::c_char).collect::<Vec<_>>(); |
| c_chars.push(0); |
| // Verify there's no embedded nulls as that would be invalid if the literal were |
| // put in a std::ffi::CString. |
| assert_eq!(c_chars.iter().filter(|x| **x == 0).count(), 1); |
| let comment = format!("\"{}\" as [c_char]", self.0); |
| tokens.extend(quote! { |
| { |
| #[doc=#comment] |
| &[#(#c_chars as std::os::raw::c_char),*] |
| } |
| }); |
| } |
| } |
| |
| // C-compatible string literals, that can be inserted into the quote! macro. |
| let test_suite_name_c_bytes = CStringLiteral(&test_suite_name); |
| let test_name_c_bytes = CStringLiteral(&test_name); |
| let file_c_bytes = CStringLiteral(file!()); |
| |
| let gtest_factory_fn = match >est_test_suite_wrapper_type { |
| Some(rust_type) => { |
| // Get the Gtest factory function pointer from the the TestSuite trait. |
| quote! { <#rust_type as ::rust_gtest_interop::TestSuite>::gtest_factory_fn_ptr() } |
| } |
| None => { |
| // If the #[gtest] macros didn't specify a test suite, then we use |
| // `rust_gtest_interop::rust_gtest_default_factory() which makes a TestSuite |
| // with `testing::Test` directly. |
| quote! { ::rust_gtest_interop::__private::rust_gtest_default_factory } |
| } |
| }; |
| let test_fn_call = match >est_test_suite_wrapper_type { |
| Some(_rust_type) => { |
| // SAFETY: Our lambda casts the `suite` reference and does not move from it, and |
| // the resulting type is not Unpin. |
| quote! { |
| let p = unsafe { |
| suite.map_unchecked_mut(|suite: &mut ::rust_gtest_interop::OpaqueTestingTest| { |
| suite.as_mut() |
| }) |
| }; |
| #test_fn(p) |
| } |
| } |
| None => quote! { #test_fn() }, |
| }; |
| |
| let output = quote! { |
| mod #test_mod { |
| use super::*; |
| |
| #[::rust_gtest_interop::small_ctor::ctor] |
| unsafe fn register_test() { |
| let r = ::rust_gtest_interop::__private::TestRegistration { |
| func: #run_test_fn, |
| test_suite_name: #test_suite_name_c_bytes, |
| test_name: #test_name_c_bytes, |
| file: #file_c_bytes, |
| line: line!(), |
| factory: #gtest_factory_fn, |
| }; |
| ::rust_gtest_interop::__private::register_test(r); |
| } |
| |
| // The function is extern "C" so `register_test()` can pass this fn as a pointer to C++ |
| // where it's registered with gtest. |
| // |
| // TODO(crbug.com/1296284): Removing #[no_mangle] makes rustc drop the symbol for the |
| // test function in the generated rlib which produces linker errors. If we resolve the |
| // linked bug and emit real object files from rustc for linking, then all the required |
| // symbols are present and `#[no_mangle]` should go away along with the custom-mangling |
| // of `run_test_fn`. We can not use `pub` to resolve this unfortunately. When `#[used]` |
| // is fixed in https://github.com/rust-lang/rust/issues/47384, this may also be |
| // resolved as well. |
| #[no_mangle] |
| extern "C" fn #run_test_fn( |
| suite: std::pin::Pin<&mut ::rust_gtest_interop::OpaqueTestingTest> |
| ) { |
| let catch_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { |
| #test_fn_call |
| })); |
| use ::rust_gtest_interop::TestResult; |
| let err_message: Option<String> = match catch_result { |
| Ok(fn_result) => TestResult::into_error_message(fn_result), |
| Err(_) => Some("Test panicked".to_string()), |
| }; |
| if let Some(m) = err_message.as_ref() { |
| ::rust_gtest_interop::__private::add_failure_at(file!(), line!(), &m); |
| } |
| } |
| |
| #input_fn |
| } |
| }; |
| output.into() |
| } |
| |
| /// The `#[extern_test_suite()]` macro is used to implement the unsafe |
| /// `TestSuite` trait. |
| /// |
| /// The `TestSuite` trait is used to mark a Rust type as being a wrapper of a |
| /// C++ subclass of `testing::Test`. This makes it valid to cast from a `*mut |
| /// testing::Test` to a pointer of the marked Rust type. |
| /// |
| /// It also marks a promise that on the C++, there exists an instantiation of |
| /// the RUST_GTEST_TEST_SUITE_FACTORY() macro for the C++ subclass type which |
| /// will be linked with the Rust crate. |
| /// |
| /// The macro takes a single parameter which is the fully specified C++ typename |
| /// of the C++ subclass for which the implementing Rust type is a wrapper. It |
| /// expects the body of the trait implementation to be empty, as it will fill in |
| /// the required implementation. |
| /// |
| /// # Example |
| /// If in C++ we have: |
| /// ```cpp |
| /// class GoatTestSuite : public testing::Test {} |
| /// RUST_GTEST_TEST_SUITE_FACTORY(GoatTestSuite); |
| /// ``` |
| /// |
| /// And in Rust we have a `ffi::GoatTestSuite` type generated to wrap the C++ |
| /// type. The the type can be marked as a valid TestSuite with the |
| /// `#[extern_test_suite]` macro: ```rs |
| /// #[extern_test_suite("GoatTestSuite")] |
| /// unsafe impl rust_gtest_interop::TestSuite for ffi::GoatTestSuite {} |
| /// ``` |
| /// |
| /// # Internals |
| /// The #[cpp_prefix("STRING_")] attribute can follow `#[extern_test_suite()]` |
| /// to control the path to the C++ Gtest factory function. This is used for |
| /// connecting to different C++ macros than the usual |
| /// RUST_GTEST_TEST_SUITE_FACTORY(). |
| #[proc_macro_attribute] |
| pub fn extern_test_suite( |
| arg_stream: proc_macro::TokenStream, |
| input: proc_macro::TokenStream, |
| ) -> proc_macro::TokenStream { |
| let args = syn::parse_macro_input!(arg_stream as syn::AttributeArgs); |
| |
| // TODO(b/229791967): With CXX it is not possible to get the C++ typename and |
| // path from the Rust wrapper type, so we require specifying it by hand in |
| // the macro. It would be nice to remove this opportunity for mistakes. |
| let cpp_type = match if args.len() == 1 { Some(&args[0]) } else { None } { |
| Some(syn::NestedMeta::Lit(syn::Lit::Str(lit_str))) => { |
| // TODO(danakj): This code drops the C++ namespaces, because we can't produce a |
| // mangled name and can't generate bindings involving fn pointers, |
| // so we require the C++ function to be `extern "C"` which means it |
| // has no namespace. Eventually we should drop the `extern "C"` on |
| // the C++ side and use the full path here. |
| let string = lit_str.value(); |
| let class_name = string.split("::").last(); |
| match class_name { |
| Some(name) => format_ident!("{}", name).into_token_stream(), |
| None => { |
| return quote_spanned! {lit_str.span() => compile_error!( |
| "invalid C++ class name" |
| )} |
| .into(); |
| } |
| } |
| } |
| _ => { |
| return quote! {compiler_error!( |
| "expected C++ type as argument to extern_test_suite" |
| )} |
| .into(); |
| } |
| }; |
| |
| /// Parses `#[cpp_prefix("PREFIX_STRING_")]` and returns `"PREFIX_STRING_"`. |
| fn parse_cpp_prefix(attr: &syn::Attribute) -> Result<String, TokenStream> { |
| let parsed = match attr.parse_meta() { |
| Ok(syn::Meta::List(list)) if list.nested.len() == 1 => match &list.nested[0] { |
| syn::NestedMeta::Lit(syn::Lit::Str(lit_str)) => Ok(lit_str.value()), |
| x => Err(x.span()), |
| }, |
| Ok(x) => Err(x.span()), |
| Err(x) => Err(x.span()), |
| }; |
| parsed.map_err(|span| { |
| quote_spanned! { span => |
| compile_error!( |
| "invalid syntax for extern_test_suite macro, \ |
| expected `#[cpp_prefix("PREFIX_STRING_")]`"); |
| } |
| }) |
| } |
| |
| let mut trait_impl = syn::parse_macro_input!(input as syn::ItemImpl); |
| if !trait_impl.items.is_empty() { |
| return quote_spanned! {trait_impl.items[0].span() => compile_error!( |
| "expected empty trait impl" |
| )} |
| .into(); |
| } |
| |
| let mut cpp_prefix = RUST_GTEST_FACTORY_PREFIX.to_owned(); |
| |
| // Look through other attributes on `trait_impl`, parse the ones related to |
| // Gtests, and put the rest back into `attrs`. |
| trait_impl.attrs = { |
| let mut keep = Vec::new(); |
| for attr in std::mem::take(&mut trait_impl.attrs) { |
| if attr.path.is_ident("cpp_prefix") { |
| cpp_prefix = match parse_cpp_prefix(&attr) { |
| Ok(tokens) => tokens, |
| Err(error_tokens) => return error_tokens.into(), |
| }; |
| } else { |
| keep.push(attr) |
| } |
| } |
| keep |
| }; |
| |
| // No longer mut. |
| let trait_impl = trait_impl; |
| let cpp_prefix = cpp_prefix; |
| |
| let trait_name = match &trait_impl.trait_ { |
| Some((_, path, _)) => path, |
| None => { |
| return quote! {compile_error!( |
| "expected impl rust_gtest_interop::TestSuite trait" |
| )} |
| .into(); |
| } |
| }; |
| let rust_type = match &*trait_impl.self_ty { |
| syn::Type::Path(type_path) => type_path, |
| _ => { |
| return quote_spanned! {trait_impl.self_ty.span() => compile_error!( |
| "expected type that wraps C++ subclass of `testing::Test`" |
| )} |
| .into(); |
| } |
| }; |
| |
| // TODO(danakj): We should generate a C++ mangled name here, then we don't |
| // require the function to be `extern "C"` (or have the author write the |
| // mangled name themselves). |
| let cpp_fn_name = format_ident!("{}{}", cpp_prefix, cpp_type.into_token_stream().to_string()); |
| |
| let output = quote! { |
| unsafe impl #trait_name for #rust_type { |
| fn gtest_factory_fn_ptr() -> rust_gtest_interop::GtestFactoryFunction { |
| extern "C" { |
| fn #cpp_fn_name( |
| f: extern "C" fn( |
| test_body: ::std::pin::Pin<&mut ::rust_gtest_interop::OpaqueTestingTest> |
| ) |
| ) -> ::std::pin::Pin<&'static mut ::rust_gtest_interop::OpaqueTestingTest>; |
| } |
| #cpp_fn_name |
| } |
| } |
| }; |
| output.into() |
| } |