Skip to content

Commit 9a16405

Browse files
hovinenbcopybara-github
authored andcommitted
Fix the use of the #[google_test] attribute macro in combination with other test attributes such as #[rstest].
Previously, there were some problems when using `#[google_test]` together with `#[rstest]`: * Placing `#[rstest]` after `#[google_test]` would result in a compiler warning. * Placing `#[rstest]` before `#[google_test]` would result in the test being executed twice. * Parameterised tests would only work if `#[rstest]` appeared first; otherwise they would result in a compiler error. This makes two changes: * First, it changes the way the test method produced by `#[google_test]` is generated. Previously, it used a nested function using the same signature as the original test function. Now, it makes the *signature* of the *outer* test function that of the the *original* test function and places the *body* of the original test function in a `move` closure, which is then invoked in the body of the new test function. This should allow parameters being passed to tests to work transparently. * Second, it make the googletest procedural macro detect whether there are any other test attributes present and emit the `#[test]` attribute only if there aren't. Previously, it would emit `#[test]` in the generated code unconditionally, which sometimes resulted in multiple tests being generated or in annotated tests being generated on inner functions. Now, it avoids emitting `#[test]` when it sees that another attribute is present which would emit `#[test]`, so the test is generated correctly. Unfortunately, these changes are insufficient to fix the problem that the test runs twice if `#[rstest]` appears *before* `#[google_test]`. That does not appear to be feasible within GoogleTest Rust alone, since the `#[google_test]` macro cannot see in that case that it was run with `#[rstest]`. For that reason, this also updates the documentation. Fixes #77 PiperOrigin-RevId: 517924751
1 parent 3284d74 commit 9a16405

File tree

6 files changed

+133
-13
lines changed

6 files changed

+133
-13
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,23 +206,27 @@ as [rstest](https://crates.io/crates/rstest). Just apply both attribute macros
206206
to the test:
207207

208208
```rust
209+
#[google_test]
209210
#[rstest]
210211
#[case(1)]
211212
#[case(2)]
212213
#[case(3)]
213-
#[google_test]
214214
fn rstest_works_with_google_test(#[case] value: u32) -> Result<()> {
215215
verify_that!(value, gt(0))
216216
}
217217
```
218218

219+
Make sure to put `#[google_test]` *before* `#[rstest]`. Otherwise the annotated
220+
test will run twice, since both macros will attempt to register a test with the
221+
Rust test harness.
222+
219223
The macro also works together with
220224
[async tests with Tokio](https://docs.rs/tokio/latest/tokio/attr.test.html) in
221225
the same way:
222226

223227
```rust
224-
#[tokio::test]
225228
#[google_test]
229+
#[tokio::test]
226230
async fn should_work_with_tokio() -> Result<()> {
227231
verify_that!(3, gt(0))
228232
}

googletest/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ googletest_macro = { path = "../googletest_macro", version = "0.4.1" }
3838
num-traits = "0.2.15"
3939
regex = "1.6.0"
4040
indoc = { version = "2", optional = true }
41+
rstest = { version = "0.16.0", optional = true }
4142

4243
[dev-dependencies]
4344
indoc = "1"
@@ -98,6 +99,12 @@ name = "first_failure_aborts"
9899
path = "integration_tests/first_failure_aborts.rs"
99100
test = false
100101

102+
[[bin]]
103+
name = "google_test_with_rstest"
104+
path = "integration_tests/google_test_with_rstest.rs"
105+
test = false
106+
required-feature = ["rstest"]
107+
101108
[[bin]]
102109
name = "non_fatal_failure_in_subroutine"
103110
path = "integration_tests/non_fatal_failure_in_subroutine.rs"
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
fn main() {}
16+
17+
// Mixing rstest and google_test should not result in any compiler warnings. The
18+
// fact that this successfully compiles is part of the test.
19+
#[deny(warnings)]
20+
#[cfg(test)]
21+
mod tests {
22+
#[cfg(not(google3))]
23+
use googletest::matchers;
24+
use googletest::{google_test, verify_that, Result};
25+
use matchers::eq;
26+
use rstest::rstest;
27+
28+
#[rstest]
29+
#[google_test]
30+
fn test_should_work_with_rstest_first() -> Result<()> {
31+
verify_that!(1, eq(1))
32+
}
33+
34+
#[rstest::rstest]
35+
#[google_test]
36+
fn test_should_work_with_qualified_rstest_first() -> Result<()> {
37+
verify_that!(1, eq(1))
38+
}
39+
40+
#[google_test]
41+
#[rstest]
42+
fn test_should_work_with_rstest_second() -> Result<()> {
43+
verify_that!(1, eq(1))
44+
}
45+
46+
#[google_test]
47+
#[rstest::rstest]
48+
fn test_should_work_with_qualified_rstest_second() -> Result<()> {
49+
verify_that!(1, eq(1))
50+
}
51+
52+
#[rstest]
53+
#[case(1)]
54+
#[google_test]
55+
fn paramterised_test_should_work_with_rstest_first(#[case] value: u32) -> Result<()> {
56+
verify_that!(value, eq(value))
57+
}
58+
59+
#[google_test]
60+
#[rstest]
61+
#[case(1)]
62+
fn paramterised_test_should_work_with_rstest_second(#[case] value: u32) -> Result<()> {
63+
verify_that!(value, eq(value))
64+
}
65+
}

googletest/integration_tests/integration_tests.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fn main() {}
1717
#[cfg(test)]
1818
mod tests {
1919
#[cfg(not(google3))]
20-
use googletest::{all, matchers};
20+
use googletest::{all, matchers, matchers::str_matcher};
2121
use googletest::{
2222
assert_that, expect_pred, expect_that, google_test, verify_pred, verify_that,
2323
GoogleTestSupport, Result,
@@ -27,6 +27,7 @@ mod tests {
2727
use matchers::all;
2828
use matchers::{anything, contains_regex, contains_substring, displays_as, eq, err, not};
2929
use std::process::Command;
30+
use str_matcher::StrMatcherConfigurator;
3031

3132
#[google_test]
3233
fn should_pass() -> Result<()> {
@@ -457,6 +458,20 @@ mod tests {
457458
)
458459
}
459460

461+
#[google_test]
462+
fn test_with_google_test_and_rstest_runs_only_once() -> Result<()> {
463+
let output = run_external_process_in_tests_directory("google_test_with_rstest")?;
464+
465+
expect_that!(
466+
output,
467+
contains_substring("tests::test_should_work_with_rstest_second").times(eq(1))
468+
);
469+
verify_that!(
470+
output,
471+
contains_substring("tests::test_should_work_with_qualified_rstest_second").times(eq(1))
472+
)
473+
}
474+
460475
fn run_external_process_in_tests_directory(name: &'static str) -> Result<String> {
461476
let mut command = run_external_process(name);
462477
let std::process::Output { stdout, .. } = command.output()?;

googletest_macro/src/lib.rs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414

1515
use quote::quote;
16-
use syn::{parse_macro_input, ItemFn};
16+
use syn::{parse_macro_input, Attribute, ItemFn, ReturnType};
1717

1818
/// Marks a test to be run by the Google Rust test runner.
1919
///
@@ -53,18 +53,46 @@ pub fn google_test(
5353
_args: proc_macro::TokenStream,
5454
input: proc_macro::TokenStream,
5555
) -> proc_macro::TokenStream {
56-
let parsed_fn = parse_macro_input!(input as ItemFn);
57-
let fn_name = parsed_fn.sig.ident.clone();
58-
let output = quote! {
59-
#[test]
60-
fn #fn_name() -> std::result::Result<(), ()> {
61-
#parsed_fn
62-
56+
let mut parsed_fn = parse_macro_input!(input as ItemFn);
57+
let attrs = parsed_fn.attrs.drain(..).collect::<Vec<_>>();
58+
let (mut sig, block) = (parsed_fn.sig, parsed_fn.block);
59+
let ReturnType::Type(_, output_type) = sig.output.clone() else {
60+
return quote! {
61+
compile_error!(
62+
"Test function with the #[google_test] attribute must return googletest::Result<()>"
63+
);
64+
}.into();
65+
};
66+
sig.output = ReturnType::Default;
67+
let function = quote! {
68+
#(#attrs)*
69+
#sig -> std::result::Result<(), ()> {
70+
let test = move || #block;
6371
use googletest::internal::test_outcome::TestOutcome;
6472
TestOutcome::init_current_test_outcome();
65-
let result = #fn_name();
73+
let result: #output_type = test();
6674
TestOutcome::close_current_test_outcome(result)
6775
}
6876
};
77+
let output = if attrs.iter().any(is_test_attribute) {
78+
function
79+
} else {
80+
quote! {
81+
#[test]
82+
#function
83+
}
84+
};
6985
output.into()
7086
}
87+
88+
fn is_test_attribute(attr: &Attribute) -> bool {
89+
let Some(first_segment) = attr.path.segments.first() else {
90+
return false;
91+
};
92+
let Some(last_segment) = attr.path.segments.last() else {
93+
return false;
94+
};
95+
first_segment.ident == "rstest"
96+
&& last_segment.ident == "rstest"
97+
&& attr.path.segments.len() <= 2
98+
}

run_integration_tests.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ INTEGRATION_TEST_BINARIES=(
3434
"failure_due_to_fail_macro_with_format_arguments"
3535
"failure_due_to_returned_error"
3636
"first_failure_aborts"
37+
"google_test_with_rstest"
3738
"non_fatal_failure_in_subroutine"
3839
"simple_assertion_failure"
3940
"simple_assertion_failure_with_assert_that"
@@ -46,6 +47,6 @@ INTEGRATION_TEST_BINARIES=(
4647

4748
cargo build
4849
for binary in ${INTEGRATION_TEST_BINARIES[@]}; do
49-
cargo rustc -p googletest --bin $binary --features indoc -- --test
50+
cargo rustc -p googletest --bin $binary --features indoc,rstest -- --test
5051
done
5152
./target/debug/integration_tests

0 commit comments

Comments
 (0)