use std::cell::Cell;
#[cfg(test)]
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU8;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::{Arc, LazyLock};
use std::{env, fmt};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum RustBacktrace {
Disabled = 1,
Enabled = 2,
Full = 3,
}
impl RustBacktrace {
pub fn detect_opt() -> Option<Self> {
const NONE: u8 = u8::MAX;
const NOT_SET: u8 = 0;
static ENABLED: AtomicU8 = AtomicU8::new(NOT_SET);
if let Some(cached) = match ENABLED.load(Relaxed) {
1 => Some(Some(Self::Disabled)),
2 => Some(Some(Self::Enabled)),
3 => Some(Some(Self::Full)),
NONE => Some(None),
NOT_SET => None,
_ => unreachable!(),
} {
return cached;
}
let raw = env::var("RUST_LIB_BACKTRACE").or_else(|_| env::var("RUST_BACKTRACE"));
let enabled = match raw.as_deref() {
Ok("0") => Some(Self::Disabled),
Ok("full") => Some(Self::Full),
Ok(_) => Some(Self::Enabled),
Err(_) => None,
};
ENABLED.store(enabled.map_or(NONE, |f| f as u8), Relaxed);
enabled
}
pub fn detect_panic_opt() -> Option<Self> {
const NONE: u8 = u8::MAX;
const NOT_SET: u8 = 0;
static ENABLED: AtomicU8 = AtomicU8::new(NOT_SET);
if let Some(cached) = match ENABLED.load(Relaxed) {
1 => Some(Some(Self::Disabled)),
2 => Some(Some(Self::Enabled)),
3 => Some(Some(Self::Full)),
NONE => Some(None),
NOT_SET => None,
_ => unreachable!(),
} {
return cached;
}
let enabled = match env::var("RUST_BACKTRACE").as_deref() {
Ok("0") => Some(Self::Disabled),
Ok("full") => Some(Self::Full),
Ok(_) => Some(Self::Enabled),
Err(_) => None,
};
ENABLED.store(enabled.map_or(NONE, |f| f as u8), Relaxed);
enabled
}
#[inline]
#[must_use]
pub const fn is_enabled(self) -> bool {
matches!(self, Self::Full | Self::Enabled)
}
#[inline]
#[must_use]
pub const fn is_full(self) -> bool {
matches!(self, Self::Full)
}
}
thread_local! {
static OVERRIDE: Cell<Option<RustBacktrace>> = const { Cell::new(None) };
}
#[inline]
pub fn set_rust_backtrace_override(value: RustBacktrace) {
OVERRIDE.with(|o| o.set(Some(value)));
}
#[inline]
pub fn clear_rust_backtrace_override() {
OVERRIDE.with(|o| o.set(None));
}
#[must_use]
#[inline]
pub fn rust_backtrace() -> RustBacktrace {
if let Some(over) = OVERRIDE.with(Cell::get) {
return over;
}
RustBacktrace::detect_opt().unwrap_or(RustBacktrace::Disabled)
}
#[must_use]
#[inline]
pub fn rust_panic_backtrace() -> RustBacktrace {
if let Some(over) = OVERRIDE.with(Cell::get) {
return over;
}
RustBacktrace::detect_panic_opt().unwrap_or(RustBacktrace::Disabled)
}
#[inline]
pub fn with_rust_backtrace_override<R>(value: RustBacktrace, f: impl FnOnce() -> R) -> R {
struct Restore(Option<RustBacktrace>);
impl Drop for Restore {
fn drop(&mut self) {
OVERRIDE.with(|o| o.set(self.0));
}
}
let _restore = Restore(OVERRIDE.with(Cell::get));
OVERRIDE.with(|o| o.set(Some(value)));
f()
}
#[derive(Clone)]
enum Inner {
Captured(Arc<Lazy>),
Disabled(backtrace::Backtrace), }
#[derive(Clone)]
pub struct Backtrace {
inner: Inner,
marker: Option<crate::marker::TraceMarker>,
}
impl crate::Capturable for Backtrace {
#[inline]
fn capture() -> Self {
if rust_backtrace().is_enabled() {
Self {
inner: Inner::Captured(Arc::new(Lazy::new(helper::lazy_resolve(Capture {
backtrace: backtrace::Backtrace::new_unresolved(),
})))),
marker: crate::marker::current(),
}
} else {
Self {
inner: Inner::Disabled(backtrace::Backtrace::from(vec![])),
marker: None,
}
}
}
#[inline]
fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
match source.oopsie_backtrace() {
Some(trace) if trace.is_captured() => trace.clone(),
_ => Self::capture(),
}
}
}
pub const CORE_SRC_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/");
impl fmt::Debug for Backtrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self.as_backtrace(), f)
}
}
impl Backtrace {
#[doc(hidden)]
#[must_use]
#[inline]
pub fn as_backtrace(&self) -> &backtrace::Backtrace {
match &self.inner {
Inner::Captured(bt) => &bt.force().backtrace,
Inner::Disabled(bt) => bt,
}
}
#[must_use]
#[inline]
pub const fn is_captured(&self) -> bool {
match &self.inner {
Inner::Captured(_) => true,
Inner::Disabled(_) => false,
}
}
#[doc(hidden)]
#[must_use]
#[inline]
pub fn frames(&self) -> &[backtrace::BacktraceFrame] {
self.as_backtrace().frames()
}
#[must_use]
pub fn marker_hidden_frames(&self) -> Option<usize> {
let marker = self.marker.as_ref()?;
let frames = self.frames();
let cut = marker.cut_len(frames);
if cut == 0 || cut >= frames.len() {
return None;
}
let rendered = |frames: &[backtrace::BacktraceFrame]| {
frames
.iter()
.map(|frame| frame.symbols().len())
.sum::<usize>()
};
let hidden = rendered(&frames[frames.len() - cut..]);
(hidden > 0 && hidden < rendered(frames)).then_some(hidden)
}
#[inline]
pub fn resolve(&self) {
let _ = self.as_backtrace();
}
#[cfg(test)]
fn is_resolved(&self) -> bool {
match &self.inner {
Inner::Captured(bt) => bt.is_resolved(),
Inner::Disabled(_) => true,
}
}
}
struct Capture {
backtrace: backtrace::Backtrace,
}
struct Lazy {
cell: LazyLock<Capture, helper::LazyResolve>,
#[cfg(test)]
forced: AtomicBool,
}
impl Lazy {
fn new(resolve: helper::LazyResolve) -> Self {
Self {
cell: LazyLock::new(resolve),
#[cfg(test)]
forced: AtomicBool::new(false),
}
}
#[inline]
fn force(&self) -> &Capture {
let capture = &*self.cell;
#[cfg(test)]
self.forced.store(true, Relaxed);
capture
}
#[cfg(test)]
fn is_resolved(&self) -> bool {
self.forced.load(Relaxed)
}
}
mod helper {
use std::panic::UnwindSafe;
use super::*;
pub(super) type LazyResolve = Box<dyn FnOnce() -> Capture + Send + Sync + UnwindSafe>;
pub(super) fn lazy_resolve(mut capture: Capture) -> LazyResolve {
Box::new(move || {
capture.backtrace.resolve();
capture
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Capturable, Diagnostic};
#[derive(Debug)]
struct ErrorWithBacktrace {
backtrace: Backtrace,
}
impl fmt::Display for ErrorWithBacktrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "error with backtrace")
}
}
impl std::error::Error for ErrorWithBacktrace {}
impl Diagnostic for ErrorWithBacktrace {
fn oopsie_backtrace(&self) -> Option<&Backtrace> {
Some(&self.backtrace)
}
}
#[derive(Debug)]
struct ErrorWithoutBacktrace;
impl fmt::Display for ErrorWithoutBacktrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "error without backtrace")
}
}
impl std::error::Error for ErrorWithoutBacktrace {}
impl Diagnostic for ErrorWithoutBacktrace {}
#[test]
fn test_rust_backtrace_override() {
set_rust_backtrace_override(RustBacktrace::Enabled);
assert_eq!(rust_backtrace(), RustBacktrace::Enabled);
}
#[test]
fn test_backtrace_capture_produces_backtrace() {
let bt = Backtrace::capture();
let _ = bt.as_backtrace().frames();
}
#[test]
fn test_backtrace_capture_or_extract_with_existing_backtrace() {
let original_bt = Backtrace::capture();
let error = ErrorWithBacktrace {
backtrace: original_bt.clone(),
};
let extracted = Backtrace::capture_or_extract(&error);
assert_eq!(
extracted.as_backtrace().frames().len(),
original_bt.as_backtrace().frames().len()
);
}
#[test]
fn test_backtrace_capture_or_extract_without_existing_backtrace() {
let error = ErrorWithoutBacktrace;
let captured = Backtrace::capture_or_extract(&error);
let _ = captured.as_backtrace().frames();
}
#[test]
fn test_backtrace_inner_returns_reference() {
let bt = Backtrace::capture();
let inner = bt.as_backtrace();
let _ = inner.frames();
}
#[test]
fn test_capture_is_empty_when_disabled() {
set_rust_backtrace_override(RustBacktrace::Disabled);
let bt = Backtrace::capture();
clear_rust_backtrace_override();
assert!(
bt.frames().is_empty(),
"disabled capture must record no frames, got {}",
bt.frames().len()
);
}
#[test]
fn test_capture_or_extract_is_empty_when_disabled() {
set_rust_backtrace_override(RustBacktrace::Disabled);
let bt = Backtrace::capture_or_extract(&ErrorWithoutBacktrace);
clear_rust_backtrace_override();
assert!(bt.frames().is_empty());
}
#[test]
fn capture_or_extract_recaptures_over_empty_source_backtrace() {
let src = with_rust_backtrace_override(RustBacktrace::Disabled, || ErrorWithBacktrace {
backtrace: Backtrace::capture(),
});
assert!(
src.backtrace.frames().is_empty(),
"precondition: source bt empty"
);
let extracted = with_rust_backtrace_override(RustBacktrace::Enabled, || {
<Backtrace as Capturable>::capture_or_extract(&src)
});
assert!(!extracted.frames().is_empty());
}
#[test]
fn clone_does_not_force_resolution() {
let bt = with_rust_backtrace_override(RustBacktrace::Enabled, Backtrace::capture);
let clone = bt.clone();
assert!(!bt.is_resolved(), "clone must not symbolicate");
assert!(!clone.is_resolved());
let _ = clone.frames();
assert!(bt.is_resolved(), "clones share a single resolution");
}
#[test]
fn debug_never_renders_empty_for_nonempty_capture() {
let bt = with_rust_backtrace_override(RustBacktrace::Enabled, Backtrace::capture);
assert!(!bt.frames().is_empty());
let rendered = with_rust_backtrace_override(RustBacktrace::Enabled, || format!("{bt:?}"));
assert!(rendered.contains("0:"), "rendered: {rendered}");
}
#[test]
fn test_capture_records_frames_when_enabled() {
set_rust_backtrace_override(RustBacktrace::Enabled);
let bt = Backtrace::capture();
clear_rust_backtrace_override();
assert!(!bt.frames().is_empty());
}
#[test]
fn with_override_scopes_and_restores() {
let baseline = rust_backtrace();
let panic_baseline = rust_panic_backtrace();
let inside = with_rust_backtrace_override(RustBacktrace::Full, rust_backtrace);
assert_eq!(inside, RustBacktrace::Full);
assert_eq!(rust_backtrace(), baseline);
let inside = with_rust_backtrace_override(RustBacktrace::Disabled, rust_panic_backtrace);
assert_eq!(inside, RustBacktrace::Disabled);
assert_eq!(rust_panic_backtrace(), panic_baseline);
}
#[test]
fn with_override_restores_on_unwind() {
let baseline = rust_backtrace();
let _ = std::panic::catch_unwind(|| {
with_rust_backtrace_override(RustBacktrace::Full, || panic!("boom"))
});
assert_eq!(rust_backtrace(), baseline);
}
const _: () = {
const fn is_capturable<T: crate::Capturable>() {}
is_capturable::<Backtrace>();
};
#[test]
fn capture_embeds_marker_and_computes_hidden_frames() {
crate::with_rust_backtrace_override(RustBacktrace::Enabled, || {
let _restore_guard = {
let prev = crate::marker::current();
scopeguard(prev)
};
let _ = crate::__private::set_marker();
let bt = <Backtrace as crate::Capturable>::capture();
let hidden = bt
.marker_hidden_frames()
.expect("same-thread marker must produce a cut");
assert!(hidden > 0);
let total_rendered: usize = bt.frames().iter().map(|f| f.symbols().len()).sum();
assert!(hidden < total_rendered);
});
}
#[test]
fn capture_without_marker_has_no_hidden_frames() {
crate::with_rust_backtrace_override(RustBacktrace::Enabled, || {
let _restore_guard = scopeguard(crate::marker::current());
crate::__private::restore_marker(None);
let bt = <Backtrace as crate::Capturable>::capture();
assert!(bt.marker_hidden_frames().is_none());
});
}
#[test]
fn marker_does_not_cross_threads() {
crate::with_rust_backtrace_override(RustBacktrace::Enabled, || {
let _ = crate::__private::set_marker();
let bt = std::thread::spawn(|| {
crate::with_rust_backtrace_override(RustBacktrace::Enabled, || {
<Backtrace as crate::Capturable>::capture()
})
})
.join()
.unwrap();
assert!(bt.marker_hidden_frames().is_none());
});
}
fn scopeguard(prev: Option<crate::marker::TraceMarker>) -> impl Drop {
struct Restore(Option<crate::marker::TraceMarker>);
impl Drop for Restore {
fn drop(&mut self) {
crate::__private::restore_marker(self.0.take());
}
}
Restore(prev)
}
}