Skip to content

Commit e489b8c

Browse files
Googlercopybara-github
authored andcommitted
Enable SCOPED_TRACE in Rust googletest crate.
This CL provides an equivalent to C++ gTest's SCOPED_TRACE in Rust tests by integrating it into the googletest crate. Rationale: - Failures in table-driven tests or tests with random inputs are hard to understand without context. SCOPED_TRACE allows adding this context. Details: - Added scoped_trace! macro that pushes traces to a thread-local stack. - Used RAII guard to pop traces when going out of scope. - Made the guard !Send and !Sync to prevent crossing .await points in async tests. - Integrated with TestAssertionFailure to include traces in failure messages. - Installed a panic hook to print traces on fatal panics. - Added golden test to validate printed output of SCOPED_TRACE. PUBLIC: Internal PiperOrigin-RevId: 913832932
1 parent 0925bc9 commit e489b8c

8 files changed

Lines changed: 268 additions & 0 deletions

File tree

googletest/src/internal/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
pub(crate) mod description_renderer;
1818
pub mod glob;
19+
pub mod scoped_trace;
1920
pub mod test_data;
2021
pub mod test_filter;
2122
pub mod test_outcome;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright 2026 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 std::cell::RefCell;
16+
use std::sync::atomic::{AtomicUsize, Ordering};
17+
18+
static NEXT_TRACE_ID: AtomicUsize = AtomicUsize::new(0);
19+
20+
/// Information about a scoped trace.
21+
#[derive(Clone, Debug)]
22+
pub struct TraceInfo {
23+
pub id: usize,
24+
pub file: &'static str,
25+
pub line: u32,
26+
pub message: String,
27+
}
28+
29+
thread_local! {
30+
static TRACE_STACK: RefCell<Vec<TraceInfo>> = const { RefCell::new(Vec::new()) };
31+
}
32+
33+
/// RAII guard to manage the push and pop of trace information.
34+
///
35+
/// This struct is `!Send` and `!Sync` to prevent it from being held across
36+
/// `.await` points in async tests, which would cause incorrect trace tracking
37+
/// if the task moves between threads.
38+
#[doc(hidden)]
39+
pub struct ScopedTraceGuard {
40+
id: usize,
41+
_phantom: std::marker::PhantomData<*mut ()>,
42+
}
43+
44+
impl ScopedTraceGuard {
45+
#[doc(hidden)]
46+
#[track_caller]
47+
pub fn new(message: String) -> Self {
48+
let caller = std::panic::Location::caller();
49+
let id = NEXT_TRACE_ID.fetch_add(1, Ordering::Relaxed);
50+
TRACE_STACK.with(|stack| {
51+
// Use try_borrow_mut to avoid double panic if called during unwinding.
52+
if let Ok(mut s) = stack.try_borrow_mut() {
53+
s.push(TraceInfo { id, file: caller.file(), line: caller.line(), message });
54+
}
55+
});
56+
Self { id, _phantom: std::marker::PhantomData }
57+
}
58+
}
59+
60+
impl Drop for ScopedTraceGuard {
61+
fn drop(&mut self) {
62+
TRACE_STACK.with(|stack| {
63+
// Use try_borrow_mut to avoid double panic if called during unwinding.
64+
if let Ok(mut s) = stack.try_borrow_mut() {
65+
if let Some(pos) = s.iter().rposition(|t| t.id == self.id) {
66+
s.remove(pos);
67+
}
68+
}
69+
});
70+
}
71+
}
72+
73+
/// Retrieves a clone of the current thread's trace stack.
74+
pub fn get_scoped_traces() -> Vec<TraceInfo> {
75+
TRACE_STACK.with(|stack| stack.try_borrow().map(|s| s.clone()).unwrap_or_default())
76+
}
77+
78+
// Test-only state and helpers, hidden from production API.
79+
#[cfg(test)]
80+
pub(crate) mod test_helpers {
81+
use super::*;
82+
use std::cell::Cell;
83+
84+
thread_local! {
85+
pub static CAPTURED_TRACES_IN_HOOK: RefCell<Vec<TraceInfo>> = const { RefCell::new(Vec::new()) };
86+
pub static USE_CAPTURE_HOOK: Cell<bool> = const { Cell::new(false) };
87+
}
88+
89+
pub fn enable_capture_in_hook(enable: bool) {
90+
USE_CAPTURE_HOOK.with(|v| v.set(enable));
91+
}
92+
93+
pub fn get_captured_traces_in_hook() -> Vec<TraceInfo> {
94+
CAPTURED_TRACES_IN_HOOK.with(|v| v.borrow().clone())
95+
}
96+
97+
pub fn clear_captured_traces_in_hook() {
98+
CAPTURED_TRACES_IN_HOOK.with(|v| v.borrow_mut().clear());
99+
}
100+
}
101+
102+
#[cfg(test)]
103+
mod tests {
104+
use super::test_helpers::*;
105+
use super::*;
106+
107+
#[test]
108+
fn test_scoped_trace_fatal() {
109+
enable_capture_in_hook(true);
110+
clear_captured_traces_in_hook();
111+
112+
// Ensure hook is installed
113+
crate::internal::test_outcome::TestOutcome::init_current_test_outcome();
114+
115+
let _ = std::panic::catch_unwind(|| {
116+
let _guard = ScopedTraceGuard::new("Second trace".to_string());
117+
panic!("Intentional panic");
118+
});
119+
120+
let captured = get_captured_traces_in_hook();
121+
assert_eq!(captured.len(), 1);
122+
assert_eq!(captured[0].message, "Second trace");
123+
124+
enable_capture_in_hook(false);
125+
}
126+
}

googletest/src/internal/test_outcome.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ impl TestOutcome {
4444
/// **For internal use only. API stablility is not guaranteed!**
4545
#[doc(hidden)]
4646
pub fn init_current_test_outcome() {
47+
static INSTALL_HOOK: OnceLock<()> = OnceLock::new();
48+
INSTALL_HOOK.get_or_init(|| {
49+
let prev_hook = std::panic::take_hook();
50+
std::panic::set_hook(Box::new(move |info| {
51+
let traces = crate::internal::scoped_trace::get_scoped_traces();
52+
if !traces.is_empty() {
53+
#[cfg(test)]
54+
{
55+
use crate::internal::scoped_trace::test_helpers::*;
56+
let use_capture = USE_CAPTURE_HOOK.with(|v| v.get());
57+
if use_capture {
58+
let _ = CAPTURED_TRACES_IN_HOOK
59+
.with(|v| v.try_borrow_mut().map(|mut b| *b = traces.clone()));
60+
prev_hook(info);
61+
return;
62+
}
63+
}
64+
65+
eprintln!("Google Test trace:");
66+
for trace in traces.iter().rev() {
67+
eprintln!(" {}:{}: {}", trace.file, trace.line, trace.message);
68+
}
69+
}
70+
prev_hook(info);
71+
}));
72+
});
73+
4774
Self::with_current_test_outcome(|mut current_test_outcome| {
4875
*current_test_outcome = Some(TestOutcome::Success);
4976
})
@@ -169,6 +196,7 @@ pub struct TestAssertionFailure {
169196
/// A human-readable formatted string describing the error.
170197
pub description: String,
171198
pub custom_message: Option<String>,
199+
pub traces: Vec<crate::internal::scoped_trace::TraceInfo>,
172200
location: Location,
173201
}
174202

@@ -218,6 +246,7 @@ impl TestAssertionFailure {
218246
Self {
219247
description,
220248
custom_message: None,
249+
traces: crate::internal::scoped_trace::get_scoped_traces(),
221250
location: Location::Real(std::panic::Location::caller()),
222251
}
223252
}
@@ -262,6 +291,12 @@ impl Display for TestAssertionFailure {
262291
if let Some(custom_message) = &self.custom_message {
263292
writeln!(f, "{custom_message}")?;
264293
}
294+
if !self.traces.is_empty() {
295+
writeln!(f, "Google Test trace:")?;
296+
for trace in self.traces.iter().rev() {
297+
writeln!(f, " {}:{}: {}", trace.file, trace.line, trace.message)?;
298+
}
299+
}
265300
writeln!(f, " at {}", self.location)
266301
}
267302
}

googletest/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub mod prelude {
5151
pub use super::gtest;
5252
pub use super::matcher::{Matcher, MatcherBase};
5353
pub use super::matchers::*;
54+
pub use super::scoped_trace;
5455
pub use super::verify_current_test_outcome;
5556
pub use super::GoogleTestSupport;
5657
pub use super::OrFail;
@@ -65,6 +66,18 @@ pub mod prelude {
6566
};
6667
}
6768

69+
/// Causes a trace to be included in every test failure message generated
70+
/// by code in the current scope.
71+
#[macro_export]
72+
macro_rules! scoped_trace {
73+
($($arg:tt)*) => {
74+
#[allow(clippy::shadow_same, clippy::shadow_unrelated)]
75+
let _gtest_trace_guard = $crate::internal::scoped_trace::ScopedTraceGuard::new(
76+
format!($($arg)*),
77+
);
78+
};
79+
}
80+
6881
pub use googletest_macro::gtest;
6982
pub use googletest_macro::test;
7083

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Google Test trace:
2+
scoped_trace_panic_test.rs:24: Inner trace
3+
scoped_trace_panic_test.rs:22: Outer trace
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2026 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::internal::test_outcome::TestOutcome;
16+
use googletest::prelude::*;
17+
18+
fn main() {
19+
// Initialize the panic hook to capture and print scoped traces on panic.
20+
TestOutcome::init_current_test_outcome();
21+
22+
scoped_trace!("Outer trace");
23+
{
24+
scoped_trace!("Inner trace");
25+
panic!("Intentional panic");
26+
}
27+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
# fail on error
3+
set -e
4+
5+
# Read binary path from argument
6+
BINARY_PATH="$1"
7+
8+
if [ -z "$BINARY_PATH" ]; then
9+
echo "Usage: $0 <binary_path>"
10+
exit 1
11+
fi
12+
13+
# Run the binary, capture output, and process with sed
14+
# We ignore the failure of the binary as per the original genrule (|| true)
15+
( "${BINARY_PATH}" 2>&1 || true ) | \
16+
sed -n -e '/^Google Test trace:/p' -e '/^ .*trace/p' | \
17+
sed 's#^.*scoped_trace_panic_test.rs# scoped_trace_panic_test.rs#g'
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2026 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+
#[cfg(test)]
16+
mod tests {
17+
use googletest::internal::scoped_trace::get_scoped_traces;
18+
use googletest::prelude::*;
19+
20+
#[gtest]
21+
fn test_scoped_trace_non_fatal() -> googletest::Result<()> {
22+
let result = {
23+
scoped_trace!("First trace");
24+
verify_eq!(1, 2)
25+
};
26+
27+
verify_that!(result, err(displays_as(contains_substring("First trace"))))?;
28+
29+
verify_that!(result, err(displays_as(contains_substring("Google Test trace:"))))
30+
}
31+
32+
#[gtest]
33+
fn test_scoped_trace_nesting() {
34+
scoped_trace!("Outer trace");
35+
{
36+
scoped_trace!("Inner trace");
37+
let traces = get_scoped_traces();
38+
assert_eq!(traces.len(), 2);
39+
assert_eq!(traces[0].message, "Outer trace");
40+
assert_eq!(traces[1].message, "Inner trace");
41+
}
42+
let traces = get_scoped_traces();
43+
assert_eq!(traces.len(), 1);
44+
assert_eq!(traces[0].message, "Outer trace");
45+
}
46+
}

0 commit comments

Comments
 (0)