Skip to content

Commit 992b88e

Browse files
cramertjcopybara-github
authored andcommitted
Add unstable support for adding failures in test-generated threads
PiperOrigin-RevId: 810562543
1 parent a26d818 commit 992b88e

File tree

7 files changed

+151
-35
lines changed

7 files changed

+151
-35
lines changed

.github/workflows/ci.yml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- name: cargo clippy
3838
uses: actions-rs/clippy-check@b5b5f21f4797c02da247df37026fcd0a5024aa4d # v1.0.7
3939
with:
40-
args: --all-targets --all-features
40+
args: --all-targets --features all_stable_features
4141
token: ${{ secrets.GITHUB_TOKEN }}
4242

4343
test-latest-deps:
@@ -55,14 +55,29 @@ jobs:
5555
- name: cargo update
5656
run: cargo update
5757
- name: cargo test --locked
58-
run: cargo test --locked --all-features
58+
run: cargo test --locked --features all_stable_features
5959

6060
test:
6161
runs-on: ubuntu-latest
6262
name: test / ubuntu / ${{ matrix.toolchain }}
6363
strategy:
6464
matrix:
65-
toolchain: [stable, nightly, beta]
65+
toolchain: [stable, beta]
66+
steps:
67+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
68+
- name: Install ${{ matrix.toolchain }}
69+
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
70+
with:
71+
toolchain: ${{ matrix.toolchain }}
72+
- name: cargo test --locked
73+
run: cargo test --locked --features all_stable_features
74+
75+
test-nightly:
76+
runs-on: ubuntu-latest
77+
name: test / ubuntu / ${{ matrix.toolchain }}
78+
strategy:
79+
matrix:
80+
toolchain: nightly
6681
steps:
6782
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
6883
- name: Install ${{ matrix.toolchain }}

googletest/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,10 @@ rustversion = "1.0.22"
4141
[dev-dependencies]
4242
indoc = "2"
4343
quickcheck = "1.0.3"
44+
45+
[features]
46+
# Enables use of the nightly-only `thread_spawn_hook` for capturing test
47+
# failures in spawned threads.
48+
unstable_thread_spawn_hook = []
49+
# A group like `--all-features`, but excluding nightly-only features.
50+
all_stable_features = ["anyhow", "proptest"]

googletest/src/internal/test_outcome.rs

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
use std::cell::{RefCell, RefMut};
1616
use std::fmt::{Debug, Display, Error, Formatter};
17+
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
18+
use std::sync::Arc;
1719
use std::thread_local;
1820

1921
/// The outcome hitherto of running a test.
@@ -23,16 +25,45 @@ use std::thread_local;
2325
///
2426
/// **For internal use only. API stablility is not guaranteed!**
2527
#[doc(hidden)]
26-
pub enum TestOutcome {
27-
/// The test ran or is currently running and no assertions have failed.
28-
Success,
29-
/// The test ran or is currently running and at least one assertion has
30-
/// failed.
31-
Failure,
28+
pub struct TestOutcome {
29+
is_success: AtomicBool,
30+
}
31+
32+
impl Default for TestOutcome {
33+
fn default() -> Self {
34+
Self::new()
35+
}
36+
}
37+
38+
impl TestOutcome {
39+
pub fn new() -> Self {
40+
Self { is_success: AtomicBool::new(true) }
41+
}
42+
pub fn fail(&self) {
43+
self.is_success.store(false, AtomicOrdering::Relaxed)
44+
}
45+
#[must_use]
46+
pub fn is_success(&self) -> bool {
47+
self.is_success.load(AtomicOrdering::Relaxed)
48+
}
49+
#[must_use]
50+
pub fn is_failed(&self) -> bool {
51+
self.is_success.load(AtomicOrdering::Relaxed)
52+
}
3253
}
3354

3455
thread_local! {
35-
static CURRENT_TEST_OUTCOME: RefCell<Option<TestOutcome>> = const { RefCell::new(None) };
56+
// Whether or not the current test has failed.
57+
//
58+
// If inside a `#[gtest]` function, this value will initially be set to a new `TestOutcome`.
59+
// Upon assertion failure (e.g. `expect_that!` failing), the `TestOutcome` will be updated to
60+
// indicate failure.
61+
//
62+
// The `Arc` is used to share the `TestOutcome` across threads that have been spawned by the
63+
// `#[gtest]` function, which can then set it to fail upon an assertion failure in a thread.
64+
static CURRENT_TEST_OUTCOME: RefCell<Option<Arc<TestOutcome>>> = const { RefCell::new(None) };
65+
#[cfg(feature = "unstable_thread_spawn_hook")]
66+
static HAS_SET_SPAWN_HOOK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3667
}
3768

3869
impl TestOutcome {
@@ -44,8 +75,26 @@ impl TestOutcome {
4475
#[doc(hidden)]
4576
pub fn init_current_test_outcome() {
4677
Self::with_current_test_outcome(|mut current_test_outcome| {
47-
*current_test_outcome = Some(TestOutcome::Success);
48-
})
78+
*current_test_outcome = Some(Arc::new(TestOutcome::new()));
79+
});
80+
81+
#[cfg(feature = "unstable_thread_spawn_hook")]
82+
if !HAS_SET_SPAWN_HOOK.get() {
83+
// Ensure that the spawn hook is only set once so that we don't accumulate spawn
84+
// hooks for threads that run multiple tests.
85+
HAS_SET_SPAWN_HOOK.set(true);
86+
std::thread::add_spawn_hook(|_thread| {
87+
let outcome: Option<Arc<TestOutcome>> =
88+
Self::with_current_test_outcome(|current_test_outcome| {
89+
current_test_outcome.clone()
90+
});
91+
move || {
92+
Self::with_current_test_outcome(|mut current_test_outcome| {
93+
*current_test_outcome = outcome;
94+
});
95+
}
96+
})
97+
}
4998
}
5099

51100
/// Evaluates the current test's [`TestOutcome`], producing a suitable
@@ -62,23 +111,21 @@ impl TestOutcome {
62111
/// **For internal use only. API stablility is not guaranteed!**
63112
#[doc(hidden)]
64113
pub fn close_current_test_outcome<E: Display>(
65-
inner_result: Result<(), E>,
114+
test_return_value: Result<(), E>,
66115
) -> Result<(), TestFailure> {
67-
TestOutcome::with_current_test_outcome(|mut outcome| {
68-
let outer_result = match &*outcome {
69-
Some(TestOutcome::Success) => match inner_result {
70-
Ok(()) => Ok(()),
71-
Err(_) => Err(TestFailure),
72-
},
73-
Some(TestOutcome::Failure) => Err(TestFailure),
74-
None => {
75-
panic!("No test context found. This indicates a bug in GoogleTest.")
76-
}
116+
TestOutcome::with_current_test_outcome(|mut outcome_arc| {
117+
let Some(outcome) = outcome_arc.as_ref() else {
118+
panic!("No test context found. This indicates a bug in GoogleTest.")
119+
};
120+
let outer_result = if outcome.is_success() && test_return_value.is_ok() {
121+
Ok(())
122+
} else {
123+
Err(TestFailure)
77124
};
78-
if let Err(fatal_assertion_failure) = inner_result {
125+
if let Err(fatal_assertion_failure) = test_return_value {
79126
println!("{fatal_assertion_failure}");
80127
}
81-
*outcome = None;
128+
*outcome_arc = None;
82129
outer_result
83130
})
84131
}
@@ -88,31 +135,35 @@ impl TestOutcome {
88135
#[track_caller]
89136
pub(crate) fn get_current_test_outcome() -> Result<(), TestAssertionFailure> {
90137
TestOutcome::with_current_test_outcome(|mut outcome| {
91-
let outcome = outcome
138+
let is_success = outcome
92139
.as_mut()
93-
.expect("No test context found. This indicates a bug in GoogleTest.");
94-
match outcome {
95-
TestOutcome::Success => Ok(()),
96-
TestOutcome::Failure => Err(TestAssertionFailure::create("Test failed".into())),
140+
.expect("No test context found. This indicates a bug in GoogleTest.")
141+
.is_success();
142+
if is_success {
143+
Ok(())
144+
} else {
145+
Err(TestAssertionFailure::create("Test failed".into()))
97146
}
98147
})
99148
}
100149

101150
/// Records that the currently running test has failed.
102151
fn fail_current_test() {
103152
TestOutcome::with_current_test_outcome(|mut outcome| {
104-
let outcome = outcome
153+
outcome
105154
.as_mut()
106-
.expect("No test context found. This indicates a bug in GoogleTest.");
107-
*outcome = TestOutcome::Failure;
155+
.expect("No test context found. This indicates a bug in GoogleTest.")
156+
.fail();
108157
})
109158
}
110159

111160
/// Runs `action` with the [`TestOutcome`] for the currently running test.
112161
///
113162
/// This is primarily intended for use by assertion macros like
114163
/// `expect_that!`.
115-
fn with_current_test_outcome<T>(action: impl FnOnce(RefMut<Option<TestOutcome>>) -> T) -> T {
164+
fn with_current_test_outcome<T>(
165+
action: impl FnOnce(RefMut<Option<Arc<TestOutcome>>>) -> T,
166+
) -> T {
116167
CURRENT_TEST_OUTCOME.with(|current_test_outcome| action(current_test_outcome.borrow_mut()))
117168
}
118169

googletest/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
#![cfg_attr(feature = "unstable_thread_spawn_hook", feature(thread_spawn_hook))]
1516
#![doc = include_str!("../crate_docs.md")]
1617

1718
extern crate googletest_macro;

integration_tests/Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ rustversion = "1.0.22"
3636
tempfile = "3.22.0"
3737
tokio = { version = "1.47", features = ["time", "macros", "rt"] }
3838

39+
[features]
40+
unstable_thread_spawn_hook=["googletest/unstable_thread_spawn_hook"]
41+
3942
[[bin]]
4043
name = "integration_tests"
4144
path = "src/integration_tests.rs"
@@ -131,6 +134,12 @@ name = "add_failure_macro_causes_failure_but_continues_execution"
131134
path = "src/add_failure_macro_causes_failure_but_continues_execution.rs"
132135
test = false
133136

137+
[[bin]]
138+
name = "add_failure_in_new_thread_fails_test"
139+
path = "src/add_failure_in_new_thread_fails_test.rs"
140+
test=false
141+
required-features=["unstable_thread_spawn_hook"]
142+
134143
[[bin]]
135144
name = "add_failure_macro_allows_empty_message"
136145
path = "src/add_failure_macro_allows_empty_message.rs"
@@ -449,4 +458,4 @@ test = false
449458
[[bin]]
450459
name = "expect_pred_macro_on_assertion_failure_with_format_args"
451460
path = "src/expect_pred_macro_on_assertion_failure_with_format_args.rs"
452-
test = false
461+
test = false
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
use googletest::{add_failure_at, gtest};
16+
17+
#[gtest]
18+
fn should_fail() {
19+
std::thread::spawn(|| {
20+
add_failure_at!("file.rs", 1, 1);
21+
})
22+
.join()
23+
.unwrap();
24+
}

integration_tests/src/integration_tests.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,15 @@ mod tests {
645645
verify_that!(output, contains_regex("Success message with argument: An argument"))
646646
}
647647

648+
#[cfg(feature = "unstable_thread_spawn_hook")]
649+
#[gtest]
650+
fn add_failure_in_new_thread_fails_test() -> Result<()> {
651+
let output =
652+
run_external_process_in_tests_directory("add_failure_in_new_thread_fails_test")?;
653+
expect_that!(output, contains_regex("test should_fail ... FAILED"));
654+
Ok(())
655+
}
656+
648657
#[gtest]
649658
fn add_failure_macro_causes_failure_but_continues_execution() -> Result<()> {
650659
let output = run_external_process_in_tests_directory(

0 commit comments

Comments
 (0)