1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
|
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{parse_macro_input, Error, Ident, ItemFn, ItemImpl, LitStr, Token, Type};
/// The prefix attached to a Gtest factory function by the
/// RUST_GTEST_TEST_SUITE_FACTORY() macro.
const RUST_GTEST_FACTORY_PREFIX: &str = "RustGtestFactory_";
struct GtestArgs {
suite_name: String,
test_name: String,
}
impl Parse for GtestArgs {
fn parse(input: ParseStream) -> Result<Self, Error> {
let suite_name = input.parse::<Ident>()?.to_string();
input.parse::<Token![,]>()?;
let test_name = input.parse::<Ident>()?.to_string();
Ok(GtestArgs { suite_name, test_name })
}
}
struct GtestSuiteArgs {
rust_type: Type,
}
impl Parse for GtestSuiteArgs {
fn parse(input: ParseStream) -> Result<Self, Error> {
let rust_type = input.parse::<Type>()?;
Ok(GtestSuiteArgs { rust_type })
}
}
struct ExternTestSuiteArgs {
cpp_type: TokenStream,
}
impl Parse for ExternTestSuiteArgs {
fn parse(input: ParseStream) -> Result<Self, Error> {
// 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_as_lit_str = input.parse::<LitStr>()?;
// 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.
match cpp_type_as_lit_str.value().split("::").last() {
Some(name) => {
Ok(ExternTestSuiteArgs { cpp_type: format_ident!("{}", name).into_token_stream() })
}
None => Err(Error::new(cpp_type_as_lit_str.span(), "invalid C++ class name")),
}
}
}
struct CppPrefixArgs {
cpp_prefix: String,
}
impl Parse for CppPrefixArgs {
fn parse(input: ParseStream) -> Result<Self, Error> {
let cpp_prefix_as_lit_str = input.parse::<LitStr>()?;
Ok(CppPrefixArgs { cpp_prefix: cpp_prefix_as_lit_str.value() })
}
}
/// 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(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let GtestArgs { suite_name, test_name } = parse_macro_input!(args as GtestArgs);
let (input_fn, gtest_suite_attr) = {
let mut input_fn = parse_macro_input!(input as ItemFn);
if let Some(asyncness) = input_fn.sig.asyncness {
// TODO(crbug.com/40211749): 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();
}
// Filter out other gtest attributes on the test function and save them for
// later processing.
let mut gtest_suite_attr = None;
input_fn.attrs = input_fn
.attrs
.into_iter()
.filter_map(|attr| {
if attr.path().is_ident("gtest_suite") {
gtest_suite_attr = Some(attr);
None
} else {
Some(attr)
}
})
.collect::<Vec<_>>();
(input_fn, gtest_suite_attr)
};
// The identifier of the function which contains the body of the test.
let test_fn = &input_fn.sig.ident;
let (gtest_factory_fn, test_fn_call) = if let Some(attr) = gtest_suite_attr {
// If present, the gtest_suite attribute is expected to have the form
// `#[gtest_suite(path::to::RustType)]`. The Rust type wraps a C++
// `TestSuite` (subclass of `::testing::Test`) which should be created
// and returned by a C++ factory function.
let rust_type = match attr.parse_args::<GtestSuiteArgs>() {
Ok(x) => x.rust_type,
Err(x) => return x.to_compile_error().into(),
};
(
// Get the Gtest factory function pointer from the TestSuite trait.
quote! { <#rust_type as ::rust_gtest_interop::TestSuite>::gtest_factory_fn_ptr() },
// 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)
},
)
} else {
// Otherwise, 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 },
quote! { #test_fn() },
)
};
// The test function and all code generate by this proc macroa go into a
// submodule which is uniquely named for the super module based on the Gtest
// suite and test names. If two tests have the same suite + test name, this
// will result in a compiler error—this is OK because Gtest disallows
// dynamically registering multiple tests with the same suite + test name.
let test_mod = format_ident!("__test_{}_{}", suite_name, test_name);
// In the generated code, `run_test_fn` 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.
//
// TODO(dcheng): This probably can be simplified to not bother with anything
// other than the suite and test name, given Gtest's restrictions for a
// given suite + test name pair to be globally unique within a test binary.
let mangled_function_name = |f: &syn::ItemFn| -> syn::Ident {
let file_name = file!().replace(|c: char| !c.is_ascii_alphanumeric(), "_");
format_ident!("{}_{}_{}_{}", file_name, suite_name, test_name, f.sig.ident)
};
let run_test_fn = format_ident!("run_test_{}", mangled_function_name(&input_fn));
// 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/40215436): 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 suite_name_c_bytes = CStringLiteral(&suite_name);
let test_name_c_bytes = CStringLiteral(&test_name);
let file_c_bytes = CStringLiteral(file!());
let output = quote! {
#[cfg(not(is_gtest_unittests))]
compile_error!(
"#[gtest(...)] can only be used in targets where the GN \
variable `is_gtest_unittests` is set to `true`.");
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: #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/40214720): 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(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
// 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 ExternTestSuiteArgs { cpp_type } = parse_macro_input!(args as ExternTestSuiteArgs);
// Filter out other gtest attributes on the trait impl and save them for later
// processing.
let (trait_impl, cpp_prefix_attr) = {
let mut trait_impl = parse_macro_input!(input as 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_attr = None;
trait_impl.attrs = trait_impl
.attrs
.into_iter()
.filter_map(|attr| {
if attr.path().is_ident("cpp_prefix") {
cpp_prefix_attr = Some(attr);
None
} else {
Some(attr)
}
})
.collect::<Vec<_>>();
(trait_impl, cpp_prefix_attr)
};
let cpp_prefix = if let Some(attr) = cpp_prefix_attr {
// If present, the cpp_prefix attribute is expected to have the form
// `#[cpp_prefix("PREFIX_STRING_")]`.
match attr.parse_args::<CppPrefixArgs>() {
Ok(cpp_prefix_args) => cpp_prefix_args.cpp_prefix,
Err(x) => return x.to_compile_error().into(),
}
} else {
RUST_GTEST_FACTORY_PREFIX.to_string()
};
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 {
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.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()
}
|