Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions googletest/src/internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

pub(crate) mod description_renderer;
pub mod glob;
pub mod scoped_trace;
pub mod test_data;
pub mod test_filter;
pub mod test_outcome;
Expand Down
126 changes: 126 additions & 0 deletions googletest/src/internal/scoped_trace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::cell::RefCell;
use std::sync::atomic::{AtomicUsize, Ordering};

static NEXT_TRACE_ID: AtomicUsize = AtomicUsize::new(0);

/// Information about a scoped trace.
#[derive(Clone, Debug)]
pub struct TraceInfo {
pub id: usize,
pub file: &'static str,
pub line: u32,
pub message: String,
}

thread_local! {
static TRACE_STACK: RefCell<Vec<TraceInfo>> = const { RefCell::new(Vec::new()) };
}

/// RAII guard to manage the push and pop of trace information.
///
/// This struct is `!Send` and `!Sync` to prevent it from being held across
/// `.await` points in async tests, which would cause incorrect trace tracking
/// if the task moves between threads.
#[doc(hidden)]
pub struct ScopedTraceGuard {
id: usize,
_phantom: std::marker::PhantomData<*mut ()>,
}

impl ScopedTraceGuard {
#[doc(hidden)]
#[track_caller]
pub fn new(message: String) -> Self {
let caller = std::panic::Location::caller();
let id = NEXT_TRACE_ID.fetch_add(1, Ordering::Relaxed);
TRACE_STACK.with(|stack| {
// Use try_borrow_mut to avoid double panic if called during unwinding.
if let Ok(mut s) = stack.try_borrow_mut() {
s.push(TraceInfo { id, file: caller.file(), line: caller.line(), message });
}
});
Self { id, _phantom: std::marker::PhantomData }
}
}

impl Drop for ScopedTraceGuard {
fn drop(&mut self) {
TRACE_STACK.with(|stack| {
// Use try_borrow_mut to avoid double panic if called during unwinding.
if let Ok(mut s) = stack.try_borrow_mut() {
if let Some(pos) = s.iter().rposition(|t| t.id == self.id) {
s.remove(pos);
}
}
});
}
}

/// Retrieves a clone of the current thread's trace stack.
pub fn get_scoped_traces() -> Vec<TraceInfo> {
TRACE_STACK.with(|stack| stack.try_borrow().map(|s| s.clone()).unwrap_or_default())
}

// Test-only state and helpers, hidden from production API.
#[cfg(test)]
pub(crate) mod test_helpers {
use super::*;
use std::cell::Cell;

thread_local! {
pub static CAPTURED_TRACES_IN_HOOK: RefCell<Vec<TraceInfo>> = const { RefCell::new(Vec::new()) };
pub static USE_CAPTURE_HOOK: Cell<bool> = const { Cell::new(false) };
}

pub fn enable_capture_in_hook(enable: bool) {
USE_CAPTURE_HOOK.with(|v| v.set(enable));
}

pub fn get_captured_traces_in_hook() -> Vec<TraceInfo> {
CAPTURED_TRACES_IN_HOOK.with(|v| v.borrow().clone())
}

pub fn clear_captured_traces_in_hook() {
CAPTURED_TRACES_IN_HOOK.with(|v| v.borrow_mut().clear());
}
}

#[cfg(test)]
mod tests {
use super::test_helpers::*;
use super::*;

#[test]
fn test_scoped_trace_fatal() {
enable_capture_in_hook(true);
clear_captured_traces_in_hook();

// Ensure hook is installed
crate::internal::test_outcome::TestOutcome::init_current_test_outcome();

let _ = std::panic::catch_unwind(|| {
let _guard = ScopedTraceGuard::new("Second trace".to_string());
panic!("Intentional panic");
});

let captured = get_captured_traces_in_hook();
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].message, "Second trace");

enable_capture_in_hook(false);
}
}
35 changes: 35 additions & 0 deletions googletest/src/internal/test_outcome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ impl TestOutcome {
/// **For internal use only. API stablility is not guaranteed!**
#[doc(hidden)]
pub fn init_current_test_outcome() {
static INSTALL_HOOK: OnceLock<()> = OnceLock::new();
INSTALL_HOOK.get_or_init(|| {
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let traces = crate::internal::scoped_trace::get_scoped_traces();
if !traces.is_empty() {
#[cfg(test)]
{
use crate::internal::scoped_trace::test_helpers::*;
let use_capture = USE_CAPTURE_HOOK.with(|v| v.get());
if use_capture {
let _ = CAPTURED_TRACES_IN_HOOK
.with(|v| v.try_borrow_mut().map(|mut b| *b = traces.clone()));
prev_hook(info);
return;
}
}

eprintln!("Google Test trace:");
for trace in traces.iter().rev() {
eprintln!(" {}:{}: {}", trace.file, trace.line, trace.message);
}
}
prev_hook(info);
}));
});

Self::with_current_test_outcome(|mut current_test_outcome| {
*current_test_outcome = Some(TestOutcome::Success);
})
Expand Down Expand Up @@ -169,6 +196,7 @@ pub struct TestAssertionFailure {
/// A human-readable formatted string describing the error.
pub description: String,
pub custom_message: Option<String>,
pub traces: Vec<crate::internal::scoped_trace::TraceInfo>,
location: Location,
}

Expand Down Expand Up @@ -218,6 +246,7 @@ impl TestAssertionFailure {
Self {
description,
custom_message: None,
traces: crate::internal::scoped_trace::get_scoped_traces(),
location: Location::Real(std::panic::Location::caller()),
}
}
Expand Down Expand Up @@ -262,6 +291,12 @@ impl Display for TestAssertionFailure {
if let Some(custom_message) = &self.custom_message {
writeln!(f, "{custom_message}")?;
}
if !self.traces.is_empty() {
writeln!(f, "Google Test trace:")?;
for trace in self.traces.iter().rev() {
writeln!(f, " {}:{}: {}", trace.file, trace.line, trace.message)?;
}
}
writeln!(f, " at {}", self.location)
}
}
Expand Down
13 changes: 13 additions & 0 deletions googletest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub mod prelude {
pub use super::gtest;
pub use super::matcher::{Matcher, MatcherBase};
pub use super::matchers::*;
pub use super::scoped_trace;
pub use super::verify_current_test_outcome;
pub use super::GoogleTestSupport;
pub use super::OrFail;
Expand All @@ -65,6 +66,18 @@ pub mod prelude {
};
}

/// Causes a trace to be included in every test failure message generated
/// by code in the current scope.
#[macro_export]
macro_rules! scoped_trace {
($($arg:tt)*) => {
#[allow(clippy::shadow_same, clippy::shadow_unrelated)]
let _gtest_trace_guard = $crate::internal::scoped_trace::ScopedTraceGuard::new(
format!($($arg)*),
);
};
}

pub use googletest_macro::gtest;
pub use googletest_macro::test;

Expand Down
3 changes: 3 additions & 0 deletions googletest/tests/scoped_trace_panic_test.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Google Test trace:
scoped_trace_panic_test.rs:24: Inner trace
scoped_trace_panic_test.rs:22: Outer trace
27 changes: 27 additions & 0 deletions googletest/tests/scoped_trace_panic_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use googletest::internal::test_outcome::TestOutcome;
use googletest::prelude::*;

fn main() {
// Initialize the panic hook to capture and print scoped traces on panic.
TestOutcome::init_current_test_outcome();

scoped_trace!("Outer trace");
{
scoped_trace!("Inner trace");
panic!("Intentional panic");
}
}
17 changes: 17 additions & 0 deletions googletest/tests/scoped_trace_panic_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
# fail on error
set -e

# Read binary path from argument
BINARY_PATH="$1"

if [ -z "$BINARY_PATH" ]; then
echo "Usage: $0 <binary_path>"
exit 1
fi

# Run the binary, capture output, and process with sed
# We ignore the failure of the binary as per the original genrule (|| true)
( "${BINARY_PATH}" 2>&1 || true ) | \
sed -n -e '/^Google Test trace:/p' -e '/^ .*trace/p' | \
sed 's#^.*scoped_trace_panic_test.rs# scoped_trace_panic_test.rs#g'
46 changes: 46 additions & 0 deletions googletest/tests/scoped_trace_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[cfg(test)]
mod tests {
use googletest::internal::scoped_trace::get_scoped_traces;
use googletest::prelude::*;

#[gtest]
fn test_scoped_trace_non_fatal() -> googletest::Result<()> {
let result = {
scoped_trace!("First trace");
verify_eq!(1, 2)
};

verify_that!(result, err(displays_as(contains_substring("First trace"))))?;

verify_that!(result, err(displays_as(contains_substring("Google Test trace:"))))
}

#[gtest]
fn test_scoped_trace_nesting() {
scoped_trace!("Outer trace");
{
scoped_trace!("Inner trace");
let traces = get_scoped_traces();
assert_eq!(traces.len(), 2);
assert_eq!(traces[0].message, "Outer trace");
assert_eq!(traces[1].message, "Inner trace");
}
let traces = get_scoped_traces();
assert_eq!(traces.len(), 1);
assert_eq!(traces[0].message, "Outer trace");
}
}
Loading