Skip to main content

tsoracle_core/
bt.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//
8//  Copyright (c) 2026 Prisma Risk
9//  Licensed under the Apache License, Version 2.0
10//  https://github.com/prisma-risk/tsoracle
11//
12
13//! Optional backtrace capture for error types.
14//!
15//! [`Bt`] is a zero-sized type unless the `bt` Cargo feature is enabled. With
16//! the feature off, [`Bt::capture`] is a no-op and the field adds nothing to
17//! the enclosing error's layout. With the feature on, [`Bt::capture`] delegates
18//! to [`std::backtrace::Backtrace::capture`], which itself honors the
19//! `RUST_BACKTRACE` / `RUST_LIB_BACKTRACE` environment variables — so symbol
20//! resolution stays opt-in at runtime even when the feature is compiled in.
21//!
22//! Embedding `Bt` in a `thiserror` enum variant:
23//!
24//! ```ignore
25//! use tsoracle_core::Bt;
26//!
27//! #[derive(Debug, thiserror::Error)]
28//! pub enum MyError {
29//!     #[error("something went wrong{bt}")]
30//!     SomethingWentWrong { bt: Bt },
31//! }
32//!
33//! let err = MyError::SomethingWentWrong { bt: Bt::capture() };
34//! ```
35
36use core::fmt;
37
38/// Optional captured backtrace.
39///
40/// See module docs for behavior under the `bt` feature.
41#[cfg(feature = "bt")]
42#[derive(Debug)]
43pub struct Bt(std::backtrace::Backtrace);
44
45#[cfg(not(feature = "bt"))]
46#[derive(Debug, Clone, Copy)]
47pub struct Bt;
48
49impl Bt {
50    /// Capture a backtrace at the call site.
51    ///
52    /// With the `bt` feature off this is a no-op that returns the unit-sized
53    /// [`Bt`]. With the feature on, capture is delegated to
54    /// [`std::backtrace::Backtrace::capture`] and is gated by the
55    /// `RUST_BACKTRACE` / `RUST_LIB_BACKTRACE` environment variables.
56    #[inline]
57    pub fn capture() -> Self {
58        #[cfg(feature = "bt")]
59        {
60            Self(std::backtrace::Backtrace::capture())
61        }
62        #[cfg(not(feature = "bt"))]
63        {
64            Self
65        }
66    }
67}
68
69impl fmt::Display for Bt {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        #[cfg(feature = "bt")]
72        {
73            use std::backtrace::BacktraceStatus;
74            if matches!(self.0.status(), BacktraceStatus::Captured) {
75                // Newline-prefix so the backtrace renders on its own line
76                // beneath the error message in default `Display` output.
77                write!(formatter, "\n{}", self.0)?;
78            }
79            Ok(())
80        }
81        #[cfg(not(feature = "bt"))]
82        {
83            let _ = formatter;
84            Ok(())
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn capture_does_not_panic() {
95        let _ = Bt::capture();
96    }
97
98    #[test]
99    fn display_is_format_safe() {
100        // Must format without panic in either feature mode. Whether content
101        // is present depends on `RUST_BACKTRACE`/`RUST_LIB_BACKTRACE` and the
102        // feature flag; only absence-of-panic is asserted here.
103        let _ = format!("{}", Bt::capture());
104    }
105
106    #[cfg(not(feature = "bt"))]
107    #[test]
108    fn bt_is_zero_sized_without_feature() {
109        // Without the feature, embedding `Bt` in a struct must add no bytes.
110        // This is the load-bearing claim that makes opt-in instrumentation
111        // free for downstream crates that never enable `bt`.
112        assert_eq!(core::mem::size_of::<Bt>(), 0);
113    }
114
115    #[cfg(not(feature = "bt"))]
116    #[test]
117    fn display_is_empty_without_feature() {
118        // No trailing backtrace section under `Display` when the feature is
119        // off. Important so `#[error("...{bt}")]` formats identically to
120        // `#[error("...")]` in the off configuration.
121        assert_eq!(format!("{}", Bt::capture()), "");
122    }
123
124    #[cfg(feature = "bt")]
125    #[test]
126    fn bt_is_nonzero_sized_with_feature() {
127        // With the feature on, `Bt` holds a `std::backtrace::Backtrace` and
128        // therefore must occupy some space.
129        assert!(core::mem::size_of::<Bt>() > 0);
130    }
131
132    #[cfg(feature = "bt")]
133    #[test]
134    fn display_renders_captured_backtrace() {
135        // Exercise the `BacktraceStatus::Captured` branch of `Display::fmt`
136        // without relying on `RUST_BACKTRACE` at test time (coverage runs
137        // don't set it). `force_capture` ignores the env var and always
138        // produces a `Captured` status, which is what gates the `write!`.
139        let bt = Bt(std::backtrace::Backtrace::force_capture());
140        let rendered = format!("{bt}");
141        assert!(
142            rendered.starts_with('\n'),
143            "captured backtrace must render with a leading newline so it sits \
144             beneath the error message; got {rendered:?}",
145        );
146        assert!(rendered.len() > 1, "captured backtrace must be non-empty");
147    }
148}