use std::fmt;
use std::ops::Deref;
use std::path;
use oopsie_core::SpanTraceStatus;
use crate::Backtrace;
use crate::style::{Colorize as _, Style};
#[non_exhaustive]
pub struct BacktraceFrame {
pub ip: usize,
pub name: Option<Box<str>>,
pub filename: Option<Box<path::Path>>,
pub lineno: Option<u32>,
pub colno: Option<u32>,
}
impl BacktraceFrame {
#[must_use]
#[inline]
pub const fn new(
ip: usize,
name: Option<Box<str>>,
filename: Option<Box<path::Path>>,
lineno: Option<u32>,
colno: Option<u32>,
) -> Self {
Self {
ip,
name,
filename,
lineno,
colno,
}
}
}
#[non_exhaustive]
pub struct SpanMetadata<'a> {
pub name: &'a str,
pub target: &'a str,
pub file: Option<&'a str>,
pub line: Option<u32>,
}
impl<'a> SpanMetadata<'a> {
#[must_use]
#[inline]
pub const fn new(
name: &'a str,
target: &'a str,
file: Option<&'a str>,
line: Option<u32>,
) -> Self {
Self {
name,
target,
file,
line,
}
}
}
pub trait BacktraceProvider {
fn frames(&self) -> Vec<BacktraceFrame>;
}
pub trait SpanTraceProvider {
fn with_spans(&self, f: &mut dyn FnMut(&SpanMetadata<'_>, &str) -> bool);
fn status(&self) -> SpanTraceStatus;
}
impl BacktraceProvider for Backtrace {
fn frames(&self) -> Vec<BacktraceFrame> {
Self::frames(self)
.iter()
.flat_map(|frame| {
frame.symbols().iter().map(|sym| BacktraceFrame {
ip: frame.ip() as usize,
name: sym.name().map(|s| s.to_string().into_boxed_str()),
filename: sym.filename().map(Box::from),
lineno: sym.lineno(),
colno: sym.colno(),
})
})
.collect()
}
}
#[cfg(feature = "tracing")]
impl SpanTraceProvider for crate::SpanTrace {
#[inline]
fn with_spans(&self, f: &mut dyn FnMut(&SpanMetadata<'_>, &str) -> bool) {
self.as_span_trace().with_spans(|md, fields| {
let meta = SpanMetadata {
name: md.name(),
target: md.target(),
file: md.file(),
line: md.line(),
};
f(&meta, fields)
});
}
#[inline]
fn status(&self) -> SpanTraceStatus {
self.status()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct TraceTheme {
pub frame_number: Style,
pub function_name: Style,
pub function_hash: Style,
pub file_path: Style,
pub line_number: Style,
pub separator: Style,
pub fields: Style,
pub header: Style,
pub frames_hidden: Style,
}
impl TraceTheme {
pub const PLAIN: Self = Self {
frame_number: Style::new(),
function_name: Style::new(),
function_hash: Style::new(),
file_path: Style::new(),
line_number: Style::new(),
separator: Style::new(),
fields: Style::new(),
header: Style::new(),
frames_hidden: Style::new(),
};
}
const BACKTRACE_CAPTURE_PREFIXES: &[&str] = &[
"std::backtrace_rs::backtrace::",
"<std::backtrace::Backtrace>::create",
"<std::backtrace::Backtrace as oopsie_core::Capturable>::",
"<alloc::boxed::Box<oopsie_core::backtrace::Backtrace> as oopsie_core::Capturable>::",
];
struct ParsedSymbol<'a> {
name: &'a str,
krate: &'a str,
path: &'a str,
}
fn scan_crate(s: &str) -> Option<(&str, &str)> {
let s = s.strip_prefix('<').unwrap_or(s);
let ident_len = s
.bytes()
.take_while(|&b| b.is_ascii_alphanumeric() || b == b'_')
.count();
if ident_len == 0 {
return None;
}
let (krate, rest) = s.split_at(ident_len);
if let Some(designator) = rest.strip_prefix('[') {
let (_, path) = designator.split_once("]::")?;
return Some((krate, path));
}
rest.strip_prefix("::").map(|path| (krate, path))
}
fn impl_trait_side(name: &str) -> Option<(&str, &str)> {
if !name.starts_with('<') {
return None;
}
let (_, trait_side) = name.rsplit_once(" as ")?;
scan_crate(trait_side)
}
fn parse_symbol(name: &str) -> Option<ParsedSymbol<'_>> {
let (krate, path) = scan_crate(name).or_else(|| impl_trait_side(name))?;
Some(ParsedSymbol { name, krate, path })
}
impl<'a> ParsedSymbol<'a> {
fn trait_crate(&self) -> Option<&'a str> {
impl_trait_side(self.name).map(|(krate, _)| krate)
}
}
fn matches_symbol(name: &str, bare: &[&str], scoped: &[(&str, &str)]) -> bool {
if bare.iter().any(|prefix| name.starts_with(prefix)) {
return true;
}
let Some(symbol) = parse_symbol(name) else {
return false;
};
scoped
.iter()
.any(|&(k, p)| symbol.krate == k && symbol.path.starts_with(p))
|| bare.iter().any(|prefix| {
symbol.path.strip_prefix(prefix).is_some_and(|rest| {
!rest
.bytes()
.next()
.is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_')
})
})
}
const POST_PANIC_BARE: &[&str] = &[
"rust_begin_unwind",
"__rust_start_panic",
"__rust_end_short_backtrace",
];
const POST_PANIC_SCOPED: &[(&str, &str)] = &[
("core", "panicking::"),
("std", "panicking::panic"),
("std", "panicking::begin_panic"),
("std", "panicking::rust_panic"),
("std", "sys::backtrace::__rust_end_short_backtrace"),
];
const RUNTIME_INIT_BARE: &[&str] = &[
"__rust_begin_short_backtrace",
"__rustc",
"__libc_start",
"__scrt_common_main",
];
const RUNTIME_INIT_SCOPED: &[(&str, &str)] = &[
("std", "sys::backtrace::__rust_begin_short_backtrace"),
("std", "rt::lang_start"),
("test", "__rust_begin_short_backtrace"),
];
const OS_ENTRY_PREFIXES: &[&str] = &[
"_main",
"___rust_try",
"__rust_try",
"_start",
"start_thread",
"__clone",
"clone3",
"__pthread",
"RtlUserThreadStart",
"BaseThreadInitThunk",
"invoke_main",
"mainCRTStartup",
];
fn is_backtrace_capture_code(name: &str, filename: Option<&path::Path>) -> bool {
BACKTRACE_CAPTURE_PREFIXES
.iter()
.any(|prefix| name.starts_with(prefix))
|| filename.is_some_and(|f| f.starts_with(oopsie_core::__private::CORE_SRC_PATH))
}
fn is_generated_site_frame(frame: &BacktraceFrame) -> bool {
let Some(name) = frame.name.as_deref() else {
return false;
};
let Some(symbol) = parse_symbol(name) else {
return false;
};
let Some(line) = frame.lineno else {
return false;
};
let Some(filename) = frame.filename.as_deref() else {
return false;
};
crate::generated::GENERATED_SITES.iter().any(|site| {
matches_site(
symbol.krate,
line,
filename,
site.krate,
site.file,
site.line,
)
})
}
fn matches_site(
frame_krate: &str,
frame_line: u32,
frame_file: &path::Path,
site_krate: &str,
site_file: &str,
site_line: u32,
) -> bool {
frame_krate == site_krate
&& frame_line == site_line
&& frame_file_matches(frame_file, site_file)
}
fn frame_file_matches(frame_file: &path::Path, site_file: &str) -> bool {
let frame_file = frame_file.to_string_lossy();
frame_file.as_ref() == site_file
|| (frame_file.ends_with(site_file)
&& frame_file
.as_bytes()
.get(frame_file.len() - site_file.len() - 1)
.is_some_and(|&b| b == b'/' || b == b'\\'))
}
fn frame_matches_location(frame: &BacktraceFrame, file: &str, line: u32) -> bool {
frame.lineno == Some(line)
&& frame
.filename
.as_deref()
.is_some_and(|f| frame_file_matches(f, file))
}
fn is_post_panic_code(name: &str, _filename: Option<&path::Path>) -> bool {
matches_symbol(name, POST_PANIC_BARE, POST_PANIC_SCOPED)
}
fn is_runtime_init_code(name: &str, _filename: Option<&path::Path>) -> bool {
matches_symbol(name, RUNTIME_INIT_BARE, RUNTIME_INIT_SCOPED)
}
fn is_runtime_tail_code(name: &str, filename: Option<&path::Path>) -> bool {
let std_owned = |krate: &str| matches!(krate, "std" | "core" | "alloc" | "test");
if let Some(symbol) = parse_symbol(name)
&& (std_owned(symbol.krate) || symbol.trait_crate().is_some_and(std_owned))
{
return true;
}
if is_runtime_init_code(name, filename) {
return true;
}
if name == "main" {
return true;
}
OS_ENTRY_PREFIXES
.iter()
.any(|prefix| name.starts_with(prefix))
}
pub fn error_backtrace_frame_filter(frames: &mut [Option<&BacktraceFrame>]) {
let internal = |frame: &BacktraceFrame| match frame.name.as_ref() {
Some(name) => is_runtime_tail_code(name, frame.filename.as_deref()),
None => true,
};
let mut keep = frames.len();
while keep > 0 {
match frames[keep - 1] {
Some(frame) if !internal(frame) => break,
_ => keep -= 1,
}
}
if keep > 0 {
for slot in &mut frames[keep..] {
*slot = None;
}
}
let top_cutoff_idx = frames.iter().rposition(|slot| {
slot.is_some_and(|frame| {
frame
.name
.as_ref()
.is_some_and(|name| is_backtrace_capture_code(name, frame.filename.as_deref()))
|| is_generated_site_frame(frame)
})
});
if let Some(top) = top_cutoff_idx {
for slot in &mut frames[..=top] {
*slot = None;
}
}
}
pub fn post_panic_frame_filter(frames: &mut [Option<&BacktraceFrame>]) {
let top_cutoff_idx = frames.iter().rposition(|slot| {
slot.is_some_and(|frame| {
frame
.name
.as_ref()
.is_some_and(|name| is_post_panic_code(name, frame.filename.as_deref()))
})
});
if let Some(top) = top_cutoff_idx {
for slot in &mut frames[..=top] {
*slot = None;
}
}
}
pub fn panic_frame_filter(frames: &mut [Option<&BacktraceFrame>]) {
post_panic_frame_filter(frames);
error_backtrace_frame_filter(frames);
}
fn split_function_hash(name: &str) -> (&str, Option<&str>) {
if name.len() >= 20 {
let suffix_start = name.len() - 19; if name.is_char_boundary(suffix_start) {
let candidate = &name[suffix_start..];
if candidate.starts_with("::h")
&& candidate[3..].len() == 16
&& candidate[3..].chars().all(|c| c.is_ascii_hexdigit())
{
return (&name[..suffix_start], Some(candidate));
}
}
}
(name, None)
}
type FrameFilterBox = BoxOrBorrow<'static, dyn FrameFilter + Send + Sync + 'static>;
pub trait FrameFilter {
fn apply(&self, frames: &mut [Option<&BacktraceFrame>]);
}
impl<F> FrameFilter for F
where
F: Fn(&mut [Option<&BacktraceFrame>]),
{
#[inline]
fn apply(&self, frames: &mut [Option<&BacktraceFrame>]) {
self(frames);
}
}
impl<F> FrameFilter for Option<F>
where
F: FrameFilter,
{
#[inline]
fn apply(&self, frames: &mut [Option<&BacktraceFrame>]) {
if let Some(filter) = self {
filter.apply(frames);
}
}
}
impl<F1, F2> FrameFilter for (F1, F2)
where
F1: FrameFilter,
F2: FrameFilter,
{
#[inline]
fn apply(&self, frames: &mut [Option<&BacktraceFrame>]) {
self.0.apply(frames);
self.1.apply(frames);
}
}
impl FrameFilter for FrameFilterBox {
#[inline]
fn apply(&self, frames: &mut [Option<&BacktraceFrame>]) {
self.as_ref().apply(frames);
}
}
pub struct TracePrinter {
theme: Option<TraceTheme>,
frame_filter: FrameFilterBox,
strip_cwd: bool,
}
impl TracePrinter {
#[must_use]
#[inline]
pub const fn new() -> Self {
Self::with_const_filter(&error_backtrace_frame_filter)
}
#[must_use]
#[inline]
pub const fn unfiltered() -> Self {
Self::with_const_filter(&noop_frame_filter).absolute_paths()
}
#[must_use]
#[inline]
pub fn with_filter(filter: impl FrameFilter + Send + Sync + 'static) -> Self {
Self {
frame_filter: BoxOrBorrow::Box(Box::new(filter)),
theme: None,
strip_cwd: true,
}
}
#[must_use]
#[inline]
pub const fn with_const_filter(
filter: &'static (impl FrameFilter + Send + Sync + 'static),
) -> Self {
Self {
frame_filter: BoxOrBorrow::Borrow(filter),
theme: None,
strip_cwd: true,
}
}
#[must_use]
pub fn add_frame_filter(
mut self,
filter: impl Fn(&mut [Option<&BacktraceFrame>]) + Send + Sync + 'static,
) -> Self {
self.frame_filter =
overlay_frame_filters(self.frame_filter, BoxOrBorrow::Box(Box::new(filter)));
self
}
#[must_use]
#[inline]
pub const fn absolute_paths(mut self) -> Self {
self.strip_cwd = false;
self
}
#[must_use]
#[inline]
pub const fn with_theme(mut self, theme: TraceTheme) -> Self {
self.theme = Some(theme);
self
}
#[must_use]
#[inline]
pub const fn plain(mut self) -> Self {
self.theme = Some(TraceTheme::PLAIN);
self
}
fn resolved_theme(&self) -> TraceTheme {
self.theme
.unwrap_or_else(|| crate::theme::get_theme().trace())
}
pub fn write_backtrace(
&self,
f: &mut fmt::Formatter<'_>,
bt: &impl BacktraceProvider,
) -> fmt::Result {
let all_frames = bt.frames();
let theme = self.resolved_theme();
let mut filtered: Vec<_> = all_frames.iter().map(Some).collect();
(self.frame_filter).apply(&mut filtered);
if !filtered.iter().any(Option::is_some) {
return Ok(());
}
writeln!(
f,
"{}",
format_args!("{:━^80}", " BACKTRACE ").style(theme.header)
)?;
let cwd = if self.strip_cwd {
std::env::current_dir().ok()
} else {
None
};
let mut hidden_run = 0_usize;
let mut number = 0_usize;
for slot in &filtered {
match slot {
Some(frame) => {
if hidden_run > 0 {
Self::write_hidden_notice(f, hidden_run, &theme)?;
hidden_run = 0;
}
number += 1;
Self::write_backtrace_frame(f, number, frame, cwd.as_deref(), &theme)?;
}
None => hidden_run += 1,
}
}
if hidden_run > 0 {
Self::write_hidden_notice(f, hidden_run, &theme)?;
}
Ok(())
}
fn write_hidden_notice(
f: &mut fmt::Formatter<'_>,
count: usize,
theme: &TraceTheme,
) -> fmt::Result {
writeln!(
f,
"{}",
format_args!(" ... {count} frames hidden ...").style(theme.frames_hidden)
)
}
fn write_backtrace_frame(
f: &mut fmt::Formatter<'_>,
number: usize,
frame: &BacktraceFrame,
cwd: Option<&path::Path>,
theme: &TraceTheme,
) -> fmt::Result {
write!(
f,
"{}{}",
format_args!("{number:>3}").style(theme.frame_number),
": ".style(theme.separator)
)?;
if let Some(name) = &frame.name {
let (base, hash) = split_function_hash(name);
write!(f, "{}", base.style(theme.function_name))?;
if let Some(h) = hash {
write!(f, "{}", h.style(theme.function_hash))?;
}
} else {
write!(f, "{}", "<unknown>".style(theme.function_name))?;
}
writeln!(f)?;
if let Some(filename) = &frame.filename {
let display_path = cwd
.and_then(|cwd| filename.strip_prefix(cwd).ok())
.unwrap_or(filename);
write!(
f,
" {}{}",
"at ".style(theme.separator),
display_path.display().style(theme.file_path)
)?;
if let Some(lineno) = frame.lineno {
write!(f, "{}", format_args!(":{lineno}").style(theme.line_number))?;
if let Some(colno) = frame.colno {
write!(f, "{}", format_args!(":{colno}").style(theme.line_number))?;
}
}
writeln!(f)?;
}
Ok(())
}
pub fn write_spantrace(
&self,
f: &mut fmt::Formatter<'_>,
st: &impl SpanTraceProvider,
) -> fmt::Result {
let theme = self.resolved_theme();
writeln!(
f,
"{}",
format_args!("{:━^80}", " SPANTRACE ").style(theme.header)
)?;
match st.status() {
SpanTraceStatus::Captured => {
let mut index = 1usize;
let mut err = Ok(());
st.with_spans(&mut |meta, fields| {
if let Err(e) = Self::write_span_frame(f, index, meta, fields, &theme) {
err = Err(e);
return false;
}
index += 1;
true
});
err
}
SpanTraceStatus::Empty => {
writeln!(
f,
"{}",
" ... no spans captured ...".style(theme.frames_hidden)
)
}
SpanTraceStatus::Unsupported => {
writeln!(
f,
"{}",
" ... span traces unsupported ...".style(theme.frames_hidden)
)
}
}
}
fn write_span_frame(
f: &mut fmt::Formatter<'_>,
index: usize,
meta: &SpanMetadata,
fields: &str,
theme: &TraceTheme,
) -> fmt::Result {
write!(
f,
"{}",
format_args!("{index:>3}").style(theme.frame_number)
)?;
write!(f, "{}", ": ".style(theme.separator))?;
write!(
f,
"{}",
format_args!("{}::{}", meta.target, meta.name).style(theme.function_name)
)?;
writeln!(f)?;
if !fields.is_empty() {
write!(f, " ")?; write!(f, "{}", "with ".style(theme.separator))?;
writeln!(f, "{}", fields.style(theme.fields))?;
}
if let Some(file) = &meta.file {
write!(f, " ")?; write!(f, "{}", "at ".style(theme.separator))?;
write!(f, "{}", file.style(theme.file_path))?;
if let Some(line) = meta.line {
write!(f, "{}", format_args!(":{line}").style(theme.line_number))?;
}
writeln!(f)?;
}
Ok(())
}
}
impl Default for TracePrinter {
#[inline]
fn default() -> Self {
Self::new()
}
}
enum BoxOrBorrow<'a, T: ?Sized> {
Borrow(&'a T),
Box(Box<T>),
}
impl<T: ?Sized> AsRef<T> for BoxOrBorrow<'_, T> {
#[inline]
fn as_ref(&self) -> &T {
match self {
BoxOrBorrow::Borrow(b) => b,
BoxOrBorrow::Box(b) => b,
}
}
}
impl<T: ?Sized> Deref for BoxOrBorrow<'_, T> {
type Target = T;
#[inline]
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
const fn noop_frame_filter(_frames: &mut [Option<&BacktraceFrame>]) {}
pub fn marker_strip_filter(cut: usize) -> impl Fn(&mut [Option<&BacktraceFrame>]) + Send + Sync {
move |frames: &mut [Option<&BacktraceFrame>]| {
let keep = frames.len().saturating_sub(cut);
if keep > 0 {
for slot in &mut frames[keep..] {
*slot = None;
}
}
}
}
pub fn location_anchor_filter(
file: &'static str,
line: u32,
) -> impl Fn(&mut [Option<&BacktraceFrame>]) + Send + Sync {
move |frames: &mut [Option<&BacktraceFrame>]| {
let anchor = frames
.iter()
.position(|slot| slot.is_some_and(|frame| frame_matches_location(frame, file, line)));
if let Some(idx) = anchor {
for slot in &mut frames[..idx] {
*slot = None;
}
}
}
}
fn overlay_frame_filters(under: FrameFilterBox, above: FrameFilterBox) -> FrameFilterBox {
BoxOrBorrow::Box(Box::new((under, above)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trace_printer_is_send_sync() {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<TracePrinter>();
}
fn kept_names<'a>(frames: &[Option<&'a BacktraceFrame>]) -> Vec<&'a str> {
frames
.iter()
.flatten()
.map(|frame| frame.name.as_deref().unwrap_or("<unknown>"))
.collect()
}
fn make_frame(name: Option<impl Into<String>>, lineno: Option<u32>) -> BacktraceFrame {
BacktraceFrame {
ip: 0,
name: name.map(|s| s.into().into_boxed_str()),
filename: None,
lineno,
colno: None,
}
}
#[test]
fn test_error_backtrace_frame_filter() {
let capture = make_frame(Some("std::backtrace_rs::backtrace::libunwind::trace"), None);
let app1 = make_frame(Some("my_crate::function_a"), None);
let app2 = make_frame(Some("my_crate::function_b"), None);
let runtime = make_frame(Some("std::rt::lang_start_internal::invoke"), None);
let mut frames: Vec<Option<&BacktraceFrame>> =
vec![Some(&capture), Some(&app1), Some(&app2), Some(&runtime)];
error_backtrace_frame_filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 2);
assert_eq!(kept_names(&frames)[0], "my_crate::function_a");
assert_eq!(kept_names(&frames)[1], "my_crate::function_b");
}
#[test]
fn test_error_backtrace_frame_filter_no_capture_no_runtime() {
let app1 = make_frame(Some("my_crate::function_a"), None);
let app2 = make_frame(Some("my_crate::function_b"), None);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![Some(&app1), Some(&app2)];
error_backtrace_frame_filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 2);
assert_eq!(kept_names(&frames)[0], "my_crate::function_a");
assert_eq!(kept_names(&frames)[1], "my_crate::function_b");
}
#[test]
fn test_error_backtrace_frame_filter_multiple_capture_frames() {
let capture1 = make_frame(Some("<std::backtrace::Backtrace>::create::something"), None);
let capture2 = make_frame(Some("std::backtrace_rs::backtrace::libunwind::trace"), None);
let app = make_frame(Some("my_crate::function_a"), None);
let mut frames: Vec<Option<&BacktraceFrame>> =
vec![Some(&capture1), Some(&capture2), Some(&app)];
error_backtrace_frame_filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 1);
assert_eq!(kept_names(&frames)[0], "my_crate::function_a");
}
#[test]
fn test_post_panic_frame_filter() {
let hook = make_frame(
Some("oopsie::panic_hook::install_panic_hook::{{closure}}"),
None,
);
let rust_panic = make_frame(Some("std::panicking::rust_panic_with_hook"), None);
let begin = make_frame(
Some("std::panicking::begin_panic_handler::{{closure}}"),
None,
);
let panic_fmt = make_frame(Some("core::panicking::panic_fmt"), None);
let user_site = make_frame(Some("my_crate::do_work"), None);
let user_main = make_frame(Some("my_crate::main"), None);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![
Some(&hook),
Some(&rust_panic),
Some(&begin),
Some(&panic_fmt),
Some(&user_site),
Some(&user_main),
];
post_panic_frame_filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 2);
assert_eq!(kept_names(&frames)[0], "my_crate::do_work");
assert_eq!(kept_names(&frames)[1], "my_crate::main");
}
#[test]
fn test_post_panic_frame_filter_no_panic_frames() {
let app1 = make_frame(Some("my_crate::function_a"), None);
let app2 = make_frame(Some("my_crate::function_b"), None);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![Some(&app1), Some(&app2)];
post_panic_frame_filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 2);
assert_eq!(kept_names(&frames)[0], "my_crate::function_a");
}
#[test]
fn test_panic_frame_filter_trims_both_ends() {
let capture = make_frame(Some("std::backtrace_rs::backtrace::libunwind::trace"), None);
let unwind = make_frame(Some("__rustc[ab12cd34]::rust_begin_unwind"), None);
let panic_fmt = make_frame(Some("core[ab12cd34]::panicking::panic_fmt"), None);
let user_site = make_frame(Some("my_crate::parse_header"), None);
let user_main = make_frame(Some("my_crate::main"), None);
let begin = make_frame(Some("__rust_begin_short_backtrace<fn(), ()>"), None);
let catch = make_frame(
Some("std[ab12cd34]::panicking::catch_unwind::do_call"),
None,
);
let main_c = make_frame(Some("_main"), None);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![
Some(&capture),
Some(&unwind),
Some(&panic_fmt),
Some(&user_site),
Some(&user_main),
Some(&begin),
Some(&catch),
Some(&main_c),
];
panic_frame_filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 2);
assert_eq!(kept_names(&frames)[0], "my_crate::parse_header");
assert_eq!(kept_names(&frames)[1], "my_crate::main");
}
#[test]
fn test_split_function_hash() {
let (base, hash) = split_function_hash("my_crate::foo::hab1234567890abcd");
assert_eq!(base, "my_crate::foo");
assert_eq!(hash, Some("::hab1234567890abcd"));
let (base, hash) = split_function_hash("my_crate::foo");
assert_eq!(base, "my_crate::foo");
assert_eq!(hash, None);
let (base, hash) = split_function_hash("my_crate::foo::habcd");
assert_eq!(base, "my_crate::foo::habcd");
assert_eq!(hash, None);
}
#[test]
fn bottom_trim_spares_user_frames_below_mid_stack_catch_unwind() {
let app_top = make_frame(Some("my_crate::inner_work"), None);
let catch = make_frame(Some("std::panic::catch_unwind::do_call"), None);
let supervisor = make_frame(Some("my_crate::supervisor"), None);
let runtime = make_frame(Some("std::rt::lang_start_internal"), None);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![
Some(&app_top),
Some(&catch),
Some(&supervisor),
Some(&runtime),
];
error_backtrace_frame_filter(&mut frames);
assert_eq!(
kept_names(&frames),
[
"my_crate::inner_work",
"std::panic::catch_unwind::do_call",
"my_crate::supervisor"
],
);
}
#[test]
fn bottom_trim_peels_nameless_frames_in_the_tail() {
let app = make_frame(Some("my_crate::function_a"), None);
let runtime = make_frame(Some("std::rt::lang_start"), None);
let nameless = make_frame(None::<String>, None);
let mut frames: Vec<Option<&BacktraceFrame>> =
vec![Some(&app), Some(&runtime), Some(&nameless)];
error_backtrace_frame_filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 1);
assert_eq!(kept_names(&frames)[0], "my_crate::function_a");
}
#[test]
fn bottom_trim_keeps_everything_when_all_frames_are_nameless() {
let a = make_frame(None::<String>, None);
let b = make_frame(None::<String>, None);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![Some(&a), Some(&b)];
error_backtrace_frame_filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 2);
}
#[test]
fn split_function_hash_non_ascii_does_not_panic() {
let name = format!("é{}", "a".repeat(18));
assert_eq!(name.len(), 20);
assert!(!name.is_char_boundary(1));
let (base, hash) = split_function_hash(&name);
assert_eq!(base, name);
assert_eq!(hash, None);
}
fn make_frame_at(ip: usize, name: &str) -> BacktraceFrame {
BacktraceFrame {
ip,
name: Some(name.to_owned().into_boxed_str()),
filename: None,
lineno: None,
colno: None,
}
}
#[test]
fn marker_strip_drops_exactly_the_trailing_cut() {
let a = make_frame_at(1, "my_crate::a");
let b = make_frame_at(7, "my_crate::b");
let c = make_frame_at(2, "my_crate::c");
let tail1 = make_frame_at(7, "std::rt::whatever");
let tail2 = make_frame_at(8, "std::rt::deeper");
let filter = marker_strip_filter(2);
let mut frames: Vec<Option<&BacktraceFrame>> =
vec![Some(&a), Some(&b), Some(&c), Some(&tail1), Some(&tail2)];
filter(&mut frames);
assert_eq!(
frames.iter().flatten().map(|f| f.ip).collect::<Vec<_>>(),
[1, 7, 2],
"the cut is positional; frames above it survive regardless of ip"
);
}
#[test]
fn rendered_paths_are_stripped_of_the_current_dir() {
struct OneFrame(std::path::PathBuf);
impl BacktraceProvider for OneFrame {
fn frames(&self) -> Vec<BacktraceFrame> {
vec![BacktraceFrame {
ip: 1,
name: Some("my_crate::work".into()),
filename: Some(self.0.clone().into_boxed_path()),
lineno: Some(7),
colno: None,
}]
}
}
fn render(printer: &TracePrinter, bt: &impl BacktraceProvider) -> String {
struct Render<'a, P>(&'a TracePrinter, &'a P);
impl<P: BacktraceProvider> fmt::Display for Render<'_, P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.write_backtrace(f, self.1)
}
}
Render(printer, bt).to_string()
}
let cwd = std::env::current_dir().unwrap();
let provider = OneFrame(cwd.join("src/work.rs"));
let stripped = render(&TracePrinter::new().plain(), &provider);
assert!(stripped.contains("at src/work.rs:7"), "{stripped}");
let absolute = render(&TracePrinter::unfiltered().plain(), &provider);
assert!(
absolute.contains(&format!("at {}:7", cwd.join("src/work.rs").display())),
"{absolute}"
);
}
#[test]
fn capture_code_matches_core_src_filenames() {
let core_file = std::path::PathBuf::from(format!(
"{}backtrace.rs",
oopsie_core::__private::CORE_SRC_PATH
));
assert!(is_backtrace_capture_code("any::name", Some(&core_file)));
let user_file = std::path::Path::new("/home/u/my_backtrace_tool/src/lib.rs");
assert!(!is_backtrace_capture_code("any::name", Some(user_file)));
}
#[test]
fn matches_site_path_forms() {
assert!(matches_site(
"myapp",
17,
std::path::Path::new("/home/u/project/src/lib.rs"),
"myapp",
"src/lib.rs",
17
));
assert!(matches_site(
"myapp",
17,
std::path::Path::new("src/lib.rs"),
"myapp",
"src/lib.rs",
17
));
assert!(!matches_site(
"myapp",
17,
std::path::Path::new("/home/u/notsrc/lib.rs"),
"myapp",
"src/lib.rs",
17
));
assert!(matches_site(
"myapp",
17,
std::path::Path::new(r"C:\project\src/lib.rs"),
"myapp",
"src/lib.rs",
17
));
}
#[test]
fn matches_site_rejects_crate_and_line_mismatch() {
assert!(!matches_site(
"other_crate",
17,
std::path::Path::new("/home/u/other/src/lib.rs"),
"myapp",
"src/lib.rs",
17
));
assert!(!matches_site(
"myapp",
18,
std::path::Path::new("/home/u/project/src/lib.rs"),
"myapp",
"src/lib.rs",
17
));
}
fn frame_with_loc(name: &str, file: &str, line: u32) -> BacktraceFrame {
BacktraceFrame {
ip: 0,
name: Some(name.to_owned().into_boxed_str()),
filename: Some(path::PathBuf::from(file).into_boxed_path()),
lineno: Some(line),
colno: None,
}
}
#[test]
fn location_anchor_hides_frames_above_the_call_site() {
let capture = frame_with_loc("oopsie_core::backtrace::capture", "src/backtrace.rs", 196);
let generated = frame_with_loc("my_crate::query_oopsies::fail", "src/lib.rs", 99);
let user = frame_with_loc("my_crate::fetch_user", "src/lib.rs", 21);
let caller = frame_with_loc("my_crate::main", "src/lib.rs", 27);
let filter = location_anchor_filter("src/lib.rs", 21);
let mut frames: Vec<Option<&BacktraceFrame>> =
vec![Some(&capture), Some(&generated), Some(&user), Some(&caller)];
filter(&mut frames);
assert_eq!(
kept_names(&frames),
["my_crate::fetch_user", "my_crate::main"]
);
}
#[test]
fn location_anchor_matches_absolute_dwarf_path_against_relative_location() {
let capture = frame_with_loc("capture", "/abs/proj/src/internal.rs", 5);
let user = frame_with_loc("my_crate::work", "/abs/proj/src/lib.rs", 21);
let filter = location_anchor_filter("src/lib.rs", 21);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![Some(&capture), Some(&user)];
filter(&mut frames);
assert_eq!(kept_names(&frames), ["my_crate::work"]);
}
#[test]
fn location_anchor_picks_topmost_match_under_recursion() {
let machinery = frame_with_loc("oopsie_core::capture", "src/bt.rs", 1);
let recur_top = frame_with_loc("my_crate::recur", "src/lib.rs", 42);
let recur_mid = frame_with_loc("my_crate::recur", "src/lib.rs", 42);
let recur_low = frame_with_loc("my_crate::recur", "src/lib.rs", 42);
let filter = location_anchor_filter("src/lib.rs", 42);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![
Some(&machinery),
Some(&recur_top),
Some(&recur_mid),
Some(&recur_low),
];
filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 3);
assert_eq!(frames.iter().filter(|s| s.is_none()).count(), 1);
}
#[test]
fn location_anchor_is_a_noop_when_nothing_matches() {
let a = frame_with_loc("my_crate::a", "src/lib.rs", 10);
let b = frame_with_loc("my_crate::b", "src/lib.rs", 20);
let filter = location_anchor_filter("src/lib.rs", 999);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![Some(&a), Some(&b)];
filter(&mut frames);
assert_eq!(kept_names(&frames), ["my_crate::a", "my_crate::b"]);
}
#[test]
fn frame_matches_location_requires_both_file_and_line() {
let frame = frame_with_loc("my_crate::work", "/abs/src/lib.rs", 21);
assert!(frame_matches_location(&frame, "src/lib.rs", 21));
assert!(!frame_matches_location(&frame, "src/lib.rs", 22));
let nameless = make_frame(None::<String>, None);
assert!(!frame_matches_location(&nameless, "src/lib.rs", 21));
}
#[test]
fn marker_strip_never_empties_the_frame_list() {
let only = make_frame_at(3, "my_crate::a");
for cut in [1, 2, usize::MAX] {
let filter = marker_strip_filter(cut);
let mut frames: Vec<Option<&BacktraceFrame>> = vec![Some(&only)];
filter(&mut frames);
assert_eq!(kept_names(&frames).len(), 1);
}
}
#[test]
fn test_is_backtrace_capture_code() {
assert!(is_backtrace_capture_code(
"std::backtrace_rs::backtrace::libunwind::trace",
None
));
assert!(!is_backtrace_capture_code("my_crate::do_stuff", None));
}
#[test]
fn test_is_runtime_init_code() {
assert!(is_runtime_init_code(
"std::rt::lang_start_internal::something",
None
));
assert!(is_runtime_init_code(
"__rust_begin_short_backtrace<fn(), ()>",
None
));
assert!(!is_runtime_init_code("my_crate::main_logic", None));
assert!(!is_runtime_init_code("main", None));
assert!(!is_runtime_init_code("my_app::main", None));
}
#[test]
fn test_is_runtime_init_code_bracket_form() {
assert!(is_runtime_init_code(
"test[a1b2c3d4]::__rust_begin_short_backtrace",
None
));
assert!(is_runtime_init_code(
"std[a1b2c3d4]::sys::backtrace::__rust_begin_short_backtrace",
None
));
assert!(!is_runtime_init_code(
"std[a1b2c3d4]::collections::HashMap::insert",
None
));
assert!(!is_runtime_init_code(
"mycrate[a1b2c3d4]::rt::lang_start",
None
));
assert!(is_runtime_init_code(
"mycrate[a1b2c3d4]::__rust_begin_short_backtrace",
None
));
}
#[test]
fn test_is_post_panic_code() {
assert!(is_post_panic_code("core::panicking::panic_fmt", None));
assert!(is_post_panic_code(
"std::panicking::begin_panic_handler::{{closure}}",
None
));
assert!(is_post_panic_code("rust_begin_unwind", None));
assert!(is_post_panic_code(
"std::sys::backtrace::__rust_end_short_backtrace::<…>",
None
));
assert!(!is_post_panic_code(
"std::panicking::catch_unwind::do_call",
None
));
assert!(!is_post_panic_code("my_app::do_work", None));
assert!(!is_post_panic_code("my_app::main", None));
}
#[test]
fn test_is_post_panic_code_bracket_form() {
assert!(is_post_panic_code(
"std[a1b2c3d4]::panicking::begin_panic_handler",
None
));
assert!(is_post_panic_code(
"core[a1b2c3d4]::panicking::panic_fmt",
None
));
assert!(is_post_panic_code(
"std[a1b2c3d4]::sys::backtrace::__rust_end_short_backtrace",
None
));
assert!(is_post_panic_code(
"__rustc[a1b2c3d4]::rust_begin_unwind",
None
));
assert!(!is_post_panic_code(
"std[a1b2c3d4]::panicking::catch_unwind::do_call",
None
));
assert!(!is_post_panic_code(
"std[a1b2c3d4]::collections::HashMap::insert",
None
));
}
#[test]
fn pin_post_panic_spellings() {
for name in [
"__rustc[1a2b]::rust_begin_unwind",
"std[1a2b]::panicking::begin_panic_handler",
"core[9f]::panicking::panic_fmt",
"std[1a2b]::sys::backtrace::__rust_end_short_backtrace",
"core::panicking::panic_fmt",
"std::panicking::rust_panic_with_hook",
] {
assert!(is_post_panic_code(name, None), "should match: {name}");
}
for name in [
"std::panicking::catch_unwind::do_call",
"my_crate::panicking::panic_like",
"mycrate::rust_begin_unwinding",
"app::__rust_start_panicky",
] {
assert!(!is_post_panic_code(name, None), "must not match: {name}");
}
}
#[test]
fn pin_runtime_init_spellings() {
for name in [
"std[1a2b]::rt::lang_start_internal",
"test[3c]::__rust_begin_short_backtrace",
] {
assert!(is_runtime_init_code(name, None), "should match: {name}");
}
assert!(!is_runtime_init_code(
"std::panic::catch_unwind::{{closure}}",
None
));
assert!(is_runtime_tail_code(
"std::panic::catch_unwind::{{closure}}",
None
));
for name in [
"my_crate::rt::lang_start",
"std_extras::rt::lang_start",
"user::__rustc_internal_thing",
"app::__libc_startup_helper",
] {
assert!(!is_runtime_init_code(name, None), "must not match: {name}");
}
}
#[test]
fn parse_symbol_across_spellings() {
fn krate_and_path(name: &str) -> Option<(&str, &str)> {
parse_symbol(name).map(|s| (s.krate, s.path))
}
fn owner(name: &str) -> Option<&str> {
parse_symbol(name).map(|s| s.krate)
}
fn trait_crate(name: &str) -> Option<&str> {
parse_symbol(name)?.trait_crate()
}
assert_eq!(
krate_and_path("std::rt::lang_start"),
Some(("std", "rt::lang_start"))
);
assert_eq!(
krate_and_path("std[1a2b]::rt::lang_start"),
Some(("std", "rt::lang_start"))
);
assert_eq!(
krate_and_path("<std[1a2b]::sys::thread::Thread>::new"),
Some(("std", "sys::thread::Thread>::new"))
);
let assert_unwind_shim = "<core[9f]::panic::unwind_safe::AssertUnwindSafe<f> as core[9f]::ops::function::FnOnce<()>>::call_once";
assert_eq!(owner(assert_unwind_shim), Some("core"));
assert_eq!(trait_crate(assert_unwind_shim), Some("core"));
let user_closure_shim = "<my::Foo as core[9f]::ops::function::FnOnce<()>>::call_once";
assert_eq!(owner(user_closure_shim), Some("my"));
assert_eq!(trait_crate(user_closure_shim), Some("core"));
assert_eq!(
owner("<fn() -> i32 as core[9f]::ops::function::FnOnce<()>>::call_once"),
Some("core")
);
assert_eq!(owner("<<a::A as b::B>::C as d::D>::m"), Some("d"));
assert_eq!(trait_crate("std::rt::lang_start"), None);
assert!(parse_symbol("main_loop").is_none());
assert!(parse_symbol("rust_begin_unwind").is_none());
assert!(parse_symbol("std").is_none());
assert_eq!(krate_and_path("corey::parse"), Some(("corey", "parse")));
}
#[test]
fn scoped_tables_match_all_spellings() {
type Classifier = fn(&str, Option<&std::path::Path>) -> bool;
for (table, classify) in [
(RUNTIME_INIT_SCOPED, is_runtime_init_code as Classifier),
(POST_PANIC_SCOPED, is_post_panic_code as Classifier),
] {
for &(krate, path) in table {
for name in [
format!("{krate}::{path}x"),
format!("{krate}[abc123]::{path}x"),
format!("<{krate}[abc123]::{path}x>::m"),
] {
assert!(classify(&name, None), "should match: {name}");
}
}
}
}
#[test]
fn runtime_tail_recognizes_test_thread_tail_spellings() {
for name in [
"__pthread_cond_wait",
"<std[1a2b]::sys::thread::unix::Thread>::new::thread_start",
"<alloc[9f]::boxed::Box<dyn core[9f]::ops::function::FnOnce<(), Output = ()> + core[9f]::marker::Send> as core[9f]::ops::function::FnOnce<()>>::call_once",
"<std[1a2b]::thread::lifecycle::spawn_unchecked<f, ()>::{closure#1} as core[9f]::ops::function::FnOnce<()>>::call_once::{shim:vtable#0}",
"std[1a2b]::thread::lifecycle::spawn_unchecked::<f, ()>::{closure#1}",
"<core[9f]::panic::unwind_safe::AssertUnwindSafe<f> as core[9f]::ops::function::FnOnce<()>>::call_once",
"test[3c]::run_test_in_process",
"test[3c]::run_test::{closure#0}",
"std::thread::lifecycle::spawn_unchecked",
"std::sys::pal::unix::thread::Thread::new::thread_start",
"_start",
"start_thread",
"__clone",
"clone3",
"RtlUserThreadStart",
"BaseThreadInitThunk",
"invoke_main",
"mainCRTStartup",
"__rust_try",
"main",
] {
assert!(is_runtime_tail_code(name, None), "should match: {name}");
}
}
#[test]
fn runtime_tail_hides_user_closure_dispatch_shims() {
assert!(is_runtime_tail_code(
"<my_app[1a2b]::main::{closure#0} as core[9f]::ops::function::FnOnce<()>>::call_once",
None
));
}
#[test]
fn runtime_tail_spares_user_spellings() {
for name in [
"my_crate::run_tests",
"my_crate::test::run_testish",
"<my_crate::Foo as my_crate::Bar>::call_me",
"<my_crate::Foo as my_crate::Bar>::call_once",
"testing::utils::run",
"corey::parse",
"my_crate::sys::thread_pool::spawn",
"my_crate::thread::worker",
"main_loop",
"mainframe::connect",
] {
assert!(!is_runtime_tail_code(name, None), "must not match: {name}");
}
}
#[test]
fn tail_only_rules_do_not_classify_per_frame_internal() {
for name in ["std::thread::sleep", "std::sys::pal::unix::futex", "main"] {
assert!(
!is_runtime_init_code(name, None),
"leaked into per-frame: {name}"
);
assert!(
is_runtime_tail_code(name, None),
"missing from tail: {name}"
);
}
}
}