Skip to content

Commit cb8eaf9

Browse files
Googlercopybara-github
authored andcommitted
test_sharding: Emit panics when skipping should_panic tests in a sharding context.
When a test uses `#[should_panic]` and the test is being skipped since it's not running in its assigned shard, if we just return `Ok(())`, that will break the test. To fix this, we simulate the expected panic to make sure the test skips successfully. PiperOrigin-RevId: 723147758
1 parent ef06f3f commit cb8eaf9

File tree

7 files changed

+214
-27
lines changed

7 files changed

+214
-27
lines changed

googletest_macro/src/lib.rs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
use quote::quote;
1616
use syn::{
17-
parse_macro_input, punctuated::Punctuated, spanned::Spanned, Attribute, DeriveInput, FnArg,
18-
ItemFn, PatType, ReturnType, Signature, Type,
17+
parse_macro_input, punctuated::Punctuated, spanned::Spanned, Attribute, DeriveInput, Expr,
18+
ExprLit, FnArg, ItemFn, Lit, MetaNameValue, PatType, ReturnType, Signature, Type,
1919
};
2020

2121
/// Marks a test to be run by the Google Rust test runner.
@@ -88,17 +88,26 @@ pub fn gtest(
8888
h.finish()
8989
};
9090

91-
let (outer_return_type, trailer) = if attrs
91+
let (skipped_test_result, outer_return_type, trailer) = attrs
9292
.iter()
93-
.any(|attr| attr.path().is_ident("should_panic"))
94-
{
95-
(quote! { () }, quote! { .unwrap(); })
96-
} else {
93+
.find(|attr| attr.path().is_ident("should_panic"))
94+
.map(|attr| {
95+
let error_message = extract_should_panic_expected(attr).unwrap_or("".to_string());
96+
(
97+
quote! {
98+
{
99+
panic!("{}", #error_message);
100+
}
101+
},
102+
quote! { () },
103+
quote! { .unwrap(); }
104+
)})
105+
.unwrap_or_else(||
97106
(
107+
quote! {Ok(())},
98108
quote! { ::std::result::Result<(), googletest::internal::test_outcome::TestFailure> },
99109
quote! {},
100-
)
101-
};
110+
));
102111

103112
let is_rstest_enabled = is_rstest_enabled(&attrs);
104113
let outer_sig = {
@@ -186,7 +195,7 @@ pub fn gtest(
186195
let result: #invocation_result_type = #invocation;
187196
TestOutcome::close_current_test_outcome(#result)
188197
} else {
189-
Ok(())
198+
#skipped_test_result
190199
}
191200
#trailer
192201
}
@@ -204,6 +213,22 @@ pub fn gtest(
204213
output.into()
205214
}
206215

216+
/// Extract the optional "expected" string literal from a `should_panic`
217+
/// attribute.
218+
fn extract_should_panic_expected(attr: &Attribute) -> Option<String> {
219+
let Ok(name_value) = attr.parse_args::<MetaNameValue>() else {
220+
return None;
221+
};
222+
match name_value.value {
223+
Expr::Lit(ExprLit { lit: Lit::Str(expected), .. })
224+
if name_value.path.is_ident("expected") =>
225+
{
226+
Some(expected.value())
227+
}
228+
_ => None,
229+
}
230+
}
231+
207232
/// Alias for [`googletest::gtest`].
208233
///
209234
/// Generally, prefer using `#[gtest]` to mark googletest-based tests.

integration_tests/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ name = "always_fails"
4646
path = "src/always_fails.rs"
4747
test = false
4848

49+
[[bin]]
50+
name = "always_panics"
51+
path = "src/always_panics.rs"
52+
test = false
53+
54+
[[bin]]
55+
name = "expect_panic"
56+
path = "src/expect_panic.rs"
57+
test = false
58+
59+
[[bin]]
60+
name = "expect_panic_with_expected"
61+
path = "src/expect_panic_with_expected.rs"
62+
test = false
63+
4964
[[bin]]
5065
name = "assert_predicate_with_failure"
5166
path = "src/assert_predicate_with_failure.rs"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2025 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+
#[cfg(test)]
18+
mod tests {
19+
use googletest::prelude::*;
20+
21+
#[gtest]
22+
fn always_panics() -> Result<()> {
23+
panic!("panic");
24+
}
25+
}

integration_tests/src/expect_panic.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2025 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+
#[cfg(test)]
18+
mod tests {
19+
use googletest::prelude::*;
20+
21+
#[gtest]
22+
#[should_panic]
23+
fn should_panic() -> Result<()> {
24+
panic!("panic");
25+
}
26+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2025 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+
#[cfg(test)]
18+
mod tests {
19+
use googletest::prelude::*;
20+
21+
#[gtest]
22+
#[should_panic(expected = "<*)))‑{")]
23+
// This emoticon is meant to validate that we're not accidentally
24+
// macro-expanding the `expected` to a spot that is in format
25+
// string position. If we do, then the `{` character should cause
26+
// a format-string parsing error.
27+
28+
fn should_panic() -> Result<()> {
29+
panic!("{}", "This is a school of fish: <*)))‑{ <><");
30+
}
31+
}

integration_tests/src/integration_tests.rs

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -752,24 +752,35 @@ mod tests {
752752

753753
#[gtest]
754754
fn always_fails_test_respects_sharding() -> Result<()> {
755-
fn execute_test(shard: u64, total_shards: u64) -> Result<bool> {
756-
let status_file = tempfile::tempdir()?.path().join("shard_status_file");
757-
let gtest_total_shards = format!("{total_shards}");
758-
let gtest_shard_index = format!("{shard}");
759-
760-
let success = run_external_process("always_fails")
761-
.env("GTEST_TOTAL_SHARDS", gtest_total_shards)
762-
.env("GTEST_SHARD_INDEX", gtest_shard_index)
763-
.env("GTEST_SHARD_STATUS_FILE", &status_file)
764-
.status()?
765-
.success();
766-
767-
verify_that!(status_file.exists(), eq(true))?;
768-
Ok(success)
769-
}
755+
// The test case should only run and fail in one shard.
756+
let results = [
757+
execute_sharded_test("always_fails", 0, 3)?,
758+
execute_sharded_test("always_fails", 1, 3)?,
759+
execute_sharded_test("always_fails", 2, 3)?,
760+
];
761+
let successes = results.iter().filter(|b| **b).count();
762+
let failures = results.iter().filter(|b| !**b).count();
770763

771-
// The test case should only run in one shard.
772-
let results = [execute_test(0, 3)?, execute_test(1, 3)?, execute_test(2, 3)?];
764+
expect_that!(successes, eq(2));
765+
expect_that!(failures, eq(1));
766+
Ok(())
767+
}
768+
769+
#[gtest]
770+
fn always_panics_test_always_fails() -> Result<()> {
771+
let status = run_external_process("always_panics").status()?;
772+
773+
verify_that!(status.success(), eq(false))
774+
}
775+
776+
#[gtest]
777+
fn always_panics_test_respects_sharding() -> Result<()> {
778+
// The test case should only run and fail in one shard.
779+
let results = [
780+
execute_sharded_test("always_panics", 0, 3)?,
781+
execute_sharded_test("always_panics", 1, 3)?,
782+
execute_sharded_test("always_panics", 2, 3)?,
783+
];
773784
let successes = results.iter().filter(|b| **b).count();
774785
let failures = results.iter().filter(|b| !**b).count();
775786

@@ -778,6 +789,41 @@ mod tests {
778789
Ok(())
779790
}
780791

792+
#[gtest]
793+
fn expect_panic_test_always_succeeds() -> Result<()> {
794+
let status = run_external_process("expect_panic").status()?;
795+
796+
verify_that!(status.success(), is_true())
797+
}
798+
799+
#[gtest]
800+
fn expect_panic_test_respects_sharding() -> Result<()> {
801+
let results = [
802+
execute_sharded_test("expect_panic", 0, 3)?,
803+
execute_sharded_test("expect_panic", 1, 3)?,
804+
execute_sharded_test("expect_panic", 2, 3)?,
805+
];
806+
verify_that!(results.iter().all(|b| *b), is_true())
807+
}
808+
809+
#[gtest]
810+
fn expect_panic_with_expected_test_always_succeeds() -> Result<()> {
811+
let status = run_external_process("expect_panic_with_expected").status()?;
812+
813+
verify_that!(status.success(), is_true())
814+
}
815+
816+
#[gtest]
817+
fn expect_panic_with_expected_test_respects_sharding() -> Result<()> {
818+
let results = [
819+
execute_sharded_test("expect_panic_with_expected", 0, 3)?,
820+
execute_sharded_test("expect_panic_with_expected", 1, 3)?,
821+
execute_sharded_test("expect_panic_with_expected", 2, 3)?,
822+
];
823+
824+
verify_that!(results.iter().all(|b| *b), is_true())
825+
}
826+
781827
#[gtest]
782828
fn verify_true_when_true_returns_ok() {
783829
assert!(verify_true!("test" == "test").is_ok())
@@ -1968,4 +2014,20 @@ mod tests {
19682014
);
19692015
Command::new(command_path)
19702016
}
2017+
2018+
fn execute_sharded_test(process: &'static str, shard: u64, total_shards: u64) -> Result<bool> {
2019+
let status_file = tempfile::tempdir()?.path().join("shard_status_file");
2020+
let gtest_total_shards = format!("{total_shards}");
2021+
let gtest_shard_index = format!("{shard}");
2022+
2023+
let success = run_external_process(process)
2024+
.env("GTEST_TOTAL_SHARDS", gtest_total_shards)
2025+
.env("GTEST_SHARD_INDEX", gtest_shard_index)
2026+
.env("GTEST_SHARD_STATUS_FILE", &status_file)
2027+
.status()?
2028+
.success();
2029+
2030+
verify_that!(status_file.exists(), eq(true))?;
2031+
Ok(success)
2032+
}
19712033
}

run_integration_tests.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ set -e
2525
INTEGRATION_TEST_BINARIES=(
2626
"integration_tests"
2727
"always_fails"
28+
"always_panics"
2829
"assert_predicate_with_failure"
2930
"assertion_failure_in_subroutine"
3031
"assertion_failures_with_short_structured_actual_values"
@@ -98,6 +99,8 @@ INTEGRATION_TEST_BINARIES=(
9899
"verify_predicate_with_failure"
99100
"verify_predicate_with_failure_as_method_in_submodule"
100101
"macro_hygiene"
102+
"expect_panic"
103+
"expect_panic_with_expected"
101104
)
102105

103106
cargo build

0 commit comments

Comments
 (0)