File: gtest_attribute.rs

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (437 lines) | stat: -rw-r--r-- 17,856 bytes parent folder | download | duplicates (9)
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()
}