Skip to main content

tsoracle_server/
bt.rs

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