use crate::console::{Console, ConsoleOptions};
use crate::markup;
use crate::renderables::Renderable;
use crate::segment::Segment;
use crate::style::Style;
use crate::text::Text;
use super::panel::Panel;
#[cfg(feature = "backtrace")]
use backtrace::Backtrace as BT;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TracebackFrame {
pub filename: Option<String>,
pub name: String,
pub line: usize,
pub locals: Option<Vec<(String, String)>>,
pub source_context: Option<String>,
pub source_first_line: usize,
}
impl TracebackFrame {
#[must_use]
pub fn new(name: impl Into<String>, line: usize) -> Self {
Self {
filename: None,
name: name.into(),
line,
locals: None,
source_context: None,
source_first_line: 1,
}
}
#[must_use]
pub fn filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
#[must_use]
pub fn locals(mut self, locals: impl Into<Vec<(String, String)>>) -> Self {
self.locals = Some(locals.into());
self
}
#[must_use]
pub fn source_context(mut self, source: impl Into<String>, first_line: usize) -> Self {
self.source_context = Some(source.into());
self.source_first_line = first_line.max(1);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Traceback {
frames: Vec<TracebackFrame>,
exception_type: String,
exception_message: String,
title: Text,
extra_lines: usize,
show_locals: bool,
}
impl Traceback {
#[must_use]
pub fn new(
frames: impl Into<Vec<TracebackFrame>>,
exception_type: impl Into<String>,
exception_message: impl Into<String>,
) -> Self {
Self {
frames: frames.into(),
exception_type: exception_type.into(),
exception_message: exception_message.into(),
title: markup::render_or_plain(
"[bold red]Traceback [bold dim red](most recent call last)[/]",
),
extra_lines: 0,
show_locals: false,
}
}
#[must_use]
pub fn title(mut self, title: impl Into<Text>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub fn extra_lines(mut self, extra_lines: usize) -> Self {
self.extra_lines = extra_lines;
self
}
#[must_use]
pub fn show_locals(mut self, show: bool) -> Self {
self.show_locals = show;
self
}
pub fn push_frame(&mut self, frame: TracebackFrame) {
self.frames.push(frame);
}
#[cfg(feature = "backtrace")]
#[must_use]
pub fn capture(
exception_type: impl Into<String>,
exception_message: impl Into<String>,
) -> Self {
let bt = BT::new();
Self::from_backtrace(&bt, exception_type, exception_message)
}
#[cfg(feature = "backtrace")]
#[must_use]
pub fn from_backtrace(
bt: &BT,
exception_type: impl Into<String>,
exception_message: impl Into<String>,
) -> Self {
let frames = Self::parse_backtrace(bt);
Self::new(frames, exception_type, exception_message)
}
#[cfg(feature = "backtrace")]
fn parse_backtrace(bt: &BT) -> Vec<TracebackFrame> {
let mut frames = Vec::new();
let mut seen_user_code = false;
for frame in bt.frames() {
let symbols: Vec<_> = {
let mut syms = Vec::new();
backtrace::resolve(frame.ip(), |symbol| {
syms.push((
symbol.name().map(|n| n.to_string()),
symbol.filename().map(std::path::Path::to_path_buf),
symbol.lineno(),
));
});
syms
};
for (name, filename, lineno) in symbols {
let Some(name) = name else {
continue;
};
if Self::is_internal_frame(&name) {
if seen_user_code {
continue;
}
continue;
}
seen_user_code = true;
let mut frame =
TracebackFrame::new(Self::demangle_name(&name), lineno.unwrap_or(0) as usize);
if let Some(ref path) = filename {
frame = frame.filename(path.display().to_string());
}
frames.push(frame);
}
}
frames.reverse();
frames
}
#[cfg(feature = "backtrace")]
fn is_internal_frame(name: &str) -> bool {
let internal_prefixes = [
"std::",
"core::",
"alloc::",
"backtrace::",
"log::",
"tracing::",
"tracing_subscriber::",
"rich_rust::logging::",
"<alloc::",
"<core::",
"<std::",
"rust_begin_unwind",
"__rust_",
"_start",
"__libc_",
"clone",
];
for prefix in internal_prefixes {
if name.starts_with(prefix) {
return true;
}
}
if name.contains("Traceback::capture") || name.contains("Traceback::from_backtrace") {
return true;
}
false
}
#[cfg(feature = "backtrace")]
fn demangle_name(name: &str) -> String {
let name = name.to_string();
if let Some(pos) = name.rfind("::h")
&& name[pos + 3..].chars().all(|c| c.is_ascii_hexdigit())
{
return name[..pos].to_string();
}
name
}
fn get_frame_source(&self, frame: &TracebackFrame) -> Option<(String, usize)> {
if let Some(ref source) = frame.source_context {
return Some((source.clone(), frame.source_first_line));
}
if let Some(ref filename) = frame.filename
&& let Ok(source) = std::fs::read_to_string(filename)
{
return Some((source, 1));
}
None
}
}
impl Renderable for Traceback {
fn render<'a>(&'a self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment<'a>> {
let width = options.max_width.max(1);
let file_style = Style::parse("cyan").ok();
let lineno_style = Style::parse("bright_blue").ok();
let func_style = Style::parse("bright_green").ok();
let separator_style = None;
let dim_style = Style::parse("dim").ok();
let error_line_style = Style::parse("bold").ok();
let exception_type_style = Style::parse("bold bright_red").ok();
let exception_msg_style = None;
let mut content_lines: Vec<Vec<Segment<'static>>> = Vec::new();
for frame in &self.frames {
let source_result = self.get_frame_source(frame);
if let Some((source, first_line)) = source_result {
if let Some(filename) = frame.filename.as_deref() {
content_lines.push(vec![
Segment::new(filename.to_string(), file_style.clone()),
Segment::new(":", separator_style.clone()),
Segment::new(frame.line.to_string(), lineno_style.clone()),
Segment::new(" in ", separator_style.clone()),
Segment::new(frame.name.clone(), func_style.clone()),
]);
} else {
content_lines.push(vec![
Segment::new("in ", separator_style.clone()),
Segment::new(frame.name.clone(), func_style.clone()),
Segment::new(":", separator_style.clone()),
Segment::new(frame.line.to_string(), lineno_style.clone()),
]);
}
content_lines.push(vec![Segment::new(String::new(), None)]);
let source_lines: Vec<&str> = source.lines().collect();
let last_line = first_line + source_lines.len().saturating_sub(1);
let start = frame.line.saturating_sub(self.extra_lines).max(first_line);
let end = (frame.line + self.extra_lines).min(last_line);
if start <= end && frame.line >= first_line && frame.line <= last_line {
let line_number_width = end.to_string().len() + 5;
for line_no in start..=end {
let source_idx = line_no.saturating_sub(first_line);
if source_idx < source_lines.len() {
let code = source_lines[source_idx];
let is_error_line = line_no == frame.line;
let indicator = if is_error_line { "❱" } else { " " };
let line_style = if is_error_line {
error_line_style.clone()
} else {
None
};
let indicator_style = if is_error_line {
exception_type_style.clone() } else {
dim_style.clone()
};
content_lines.push(vec![
Segment::new(indicator.to_string(), indicator_style),
Segment::new(" ", None),
Segment::new(
format!("{line_no:<line_number_width$}"),
lineno_style.clone(),
),
Segment::new(code.to_string(), line_style),
]);
}
}
}
if self.show_locals
&& let Some(locals) = frame.locals.as_ref()
&& !locals.is_empty()
{
let locals_key_style = Style::parse("dim").ok();
let locals_value_style = Style::parse("cyan").ok();
content_lines.push(vec![Segment::new(String::new(), None)]);
content_lines.push(vec![Segment::new("locals:", dim_style.clone())]);
for (k, v) in locals {
content_lines.push(vec![
Segment::new(" ", None),
Segment::new(format!("{k}="), locals_key_style.clone()),
Segment::new(v.clone(), locals_value_style.clone()),
]);
}
}
continue;
}
content_lines.push(vec![
Segment::new("in ", separator_style.clone()),
Segment::new(frame.name.clone(), func_style.clone()),
Segment::new(":", separator_style.clone()),
Segment::new(frame.line.to_string(), lineno_style.clone()),
]);
if self.show_locals
&& let Some(locals) = frame.locals.as_ref()
&& !locals.is_empty()
{
let locals_key_style = Style::parse("dim").ok();
let locals_value_style = Style::parse("cyan").ok();
content_lines.push(vec![Segment::new(String::new(), None)]);
content_lines.push(vec![Segment::new("locals:", dim_style.clone())]);
for (k, v) in locals {
content_lines.push(vec![
Segment::new(" ", None),
Segment::new(format!("{k}="), locals_key_style.clone()),
Segment::new(v.clone(), locals_value_style.clone()),
]);
}
}
}
let panel = Panel::new(content_lines)
.title(self.title.clone())
.border_style(Style::parse("red").unwrap_or_default())
.width(width);
let mut segments: Vec<Segment<'static>> = panel.render(width);
segments.push(Segment::new(
format!("{}: ", self.exception_type),
exception_type_style.clone(),
));
segments.push(Segment::new(
self.exception_message.clone(),
exception_msg_style.clone(),
));
segments.push(Segment::line());
segments.into_iter().collect()
}
}
pub fn print_exception(console: &Console, traceback: &Traceback) {
console.print_exception(traceback);
}
#[cfg(test)]
mod tests {
use super::*;
fn render_to_text(traceback: &Traceback, width: usize) -> String {
let console = Console::new();
let options = ConsoleOptions {
max_width: width,
..Default::default()
};
let segments = traceback.render(&console, &options);
segments.iter().map(|s| s.text.as_ref()).collect()
}
#[test]
fn frame_without_source_shows_minimal_info() {
let frame = TracebackFrame::new("my_func", 42);
let traceback = Traceback::new(vec![frame], "Error", "something went wrong");
let output = render_to_text(&traceback, 80);
assert!(output.contains("my_func"));
assert!(output.contains("42"));
assert!(output.contains("Error: something went wrong"));
}
#[test]
fn frame_with_source_context_renders_code() {
let source = "fn main() {\n let x = 1;\n return Err(\"oops\");\n let y = 2;\n}";
let frame = TracebackFrame::new("main", 3).source_context(source, 1);
let traceback = Traceback::new(vec![frame], "PanicError", "oops").extra_lines(1);
let output = render_to_text(&traceback, 80);
assert!(output.contains("❱"));
assert!(output.contains("return Err"));
assert!(output.contains("let x = 1"));
assert!(output.contains("let y = 2"));
assert!(output.contains("PanicError: oops"));
}
#[test]
fn source_context_with_offset_first_line() {
let source = " fn helper() {\n do_thing();\n crash_here();\n }\n";
let frame = TracebackFrame::new("helper", 12).source_context(source, 10);
let traceback = Traceback::new(vec![frame], "Error", "crashed").extra_lines(1);
let output = render_to_text(&traceback, 80);
assert!(output.contains("❱"));
assert!(output.contains("12"));
assert!(output.contains("crash_here"));
assert!(output.contains("11"));
assert!(output.contains("do_thing"));
}
#[test]
fn source_context_takes_priority_over_filename() {
let source = "custom source line";
let frame = TracebackFrame::new("func", 1)
.filename("/nonexistent/file.rs")
.source_context(source, 1);
let traceback = Traceback::new(vec![frame], "Error", "test");
let output = render_to_text(&traceback, 80);
assert!(output.contains("custom source line"));
assert!(output.contains("/nonexistent/file.rs"));
}
#[test]
fn extra_lines_zero_shows_only_error_line() {
let source = "line1\nline2\nline3\nline4\nline5";
let frame = TracebackFrame::new("func", 3).source_context(source, 1);
let traceback = Traceback::new(vec![frame], "Error", "test").extra_lines(0);
let output = render_to_text(&traceback, 80);
assert!(output.contains("line3"));
assert!(output.contains("❱"));
assert!(!output.contains("line1"));
assert!(!output.contains("line5"));
}
#[test]
fn multiple_frames_with_source_context() {
let frame1 =
TracebackFrame::new("outer", 2).source_context("fn outer() {\n inner();\n}", 1);
let frame2 =
TracebackFrame::new("inner", 2).source_context("fn inner() {\n fail();\n}", 1);
let traceback = Traceback::new(vec![frame1, frame2], "PanicError", "boom");
let output = render_to_text(&traceback, 80);
assert!(output.contains("outer"));
assert!(output.contains("inner"));
assert!(output.contains("PanicError: boom"));
}
#[test]
fn frame_builder_methods() {
let frame = TracebackFrame::new("test", 10)
.filename("src/lib.rs")
.source_context("test code", 5);
assert_eq!(frame.name, "test");
assert_eq!(frame.line, 10);
assert_eq!(frame.filename, Some("src/lib.rs".to_string()));
assert_eq!(frame.source_context, Some("test code".to_string()));
assert_eq!(frame.source_first_line, 5);
}
#[test]
fn frame_locals_builder_and_rendering() {
let frame = TracebackFrame::new("func", 1)
.source_context("line1\nline2\nline3", 1)
.locals(vec![
("user".to_string(), "\"alice\"".to_string()),
("retries".to_string(), "3".to_string()),
]);
let traceback = Traceback::new(vec![frame], "Error", "msg")
.extra_lines(0)
.show_locals(true);
let output = render_to_text(&traceback, 80);
assert!(output.contains("locals:"));
assert!(output.contains("user="));
assert!(output.contains("alice"));
assert!(output.contains("retries="));
}
#[test]
fn source_first_line_minimum_is_one() {
let frame = TracebackFrame::new("test", 1).source_context("code", 0);
assert_eq!(frame.source_first_line, 1);
}
#[test]
fn test_frame_new_basic() {
let frame = TracebackFrame::new("my_function", 42);
assert_eq!(frame.name, "my_function");
assert_eq!(frame.line, 42);
assert!(frame.filename.is_none());
assert!(frame.source_context.is_none());
assert_eq!(frame.source_first_line, 1);
}
#[test]
fn test_frame_new_with_string_type() {
let name = String::from("owned_name");
let frame = TracebackFrame::new(name, 100);
assert_eq!(frame.name, "owned_name");
assert_eq!(frame.line, 100);
}
#[test]
fn test_frame_filename_builder() {
let frame = TracebackFrame::new("func", 1).filename("src/main.rs");
assert_eq!(frame.filename, Some("src/main.rs".to_string()));
}
#[test]
fn test_frame_filename_with_owned_string() {
let path = String::from("/path/to/file.rs");
let frame = TracebackFrame::new("func", 1).filename(path);
assert_eq!(frame.filename, Some("/path/to/file.rs".to_string()));
}
#[test]
fn test_frame_source_context_builder() {
let frame = TracebackFrame::new("func", 5).source_context("let x = 1;", 5);
assert_eq!(frame.source_context, Some("let x = 1;".to_string()));
assert_eq!(frame.source_first_line, 5);
}
#[test]
fn test_frame_source_context_multiline() {
let source = "fn foo() {\n bar();\n}";
let frame = TracebackFrame::new("foo", 2).source_context(source, 1);
assert!(frame.source_context.unwrap().contains("bar()"));
}
#[test]
fn test_frame_clone() {
let frame = TracebackFrame::new("func", 10)
.filename("test.rs")
.source_context("code", 5);
let cloned = frame.clone();
assert_eq!(frame, cloned);
}
#[test]
fn test_frame_eq() {
let frame1 = TracebackFrame::new("func", 10);
let frame2 = TracebackFrame::new("func", 10);
assert_eq!(frame1, frame2);
}
#[test]
fn test_frame_ne_different_name() {
let frame1 = TracebackFrame::new("func_a", 10);
let frame2 = TracebackFrame::new("func_b", 10);
assert_ne!(frame1, frame2);
}
#[test]
fn test_frame_ne_different_line() {
let frame1 = TracebackFrame::new("func", 10);
let frame2 = TracebackFrame::new("func", 20);
assert_ne!(frame1, frame2);
}
#[test]
fn test_frame_debug() {
let frame = TracebackFrame::new("test", 1);
let debug = format!("{frame:?}");
assert!(debug.contains("TracebackFrame"));
assert!(debug.contains("test"));
}
#[test]
fn test_frame_chain_builder_pattern() {
let frame = TracebackFrame::new("handler", 50)
.filename("src/handlers/api.rs")
.source_context("async fn handler() -> Result<()>", 50);
assert_eq!(frame.name, "handler");
assert_eq!(frame.line, 50);
assert_eq!(frame.filename, Some("src/handlers/api.rs".to_string()));
assert!(frame.source_context.is_some());
}
#[test]
fn test_frame_empty_source_context() {
let frame = TracebackFrame::new("func", 1).source_context("", 1);
assert_eq!(frame.source_context, Some(String::new()));
}
#[test]
fn test_traceback_new_empty_frames() {
let traceback = Traceback::new(Vec::new(), "Error", "message");
assert!(traceback.frames.is_empty());
assert_eq!(traceback.exception_type, "Error");
assert_eq!(traceback.exception_message, "message");
}
#[test]
fn test_traceback_new_from_vec() {
let frames = vec![
TracebackFrame::new("a", 1),
TracebackFrame::new("b", 2),
TracebackFrame::new("c", 3),
];
let traceback = Traceback::new(frames, "TestError", "test");
assert_eq!(traceback.frames.len(), 3);
}
#[test]
fn test_traceback_title_builder() {
let traceback = Traceback::new(vec![], "Error", "msg").title("Custom Title");
let output = render_to_text(&traceback, 80);
assert!(output.contains("Custom Title"));
}
#[test]
fn test_traceback_extra_lines_builder() {
let source = "a\nb\nc\nd\ne";
let frame = TracebackFrame::new("func", 3).source_context(source, 1);
let traceback = Traceback::new(vec![frame], "Error", "test").extra_lines(2);
let output = render_to_text(&traceback, 80);
assert!(output.contains('a'));
assert!(output.contains('e'));
}
#[test]
fn test_traceback_push_frame() {
let mut traceback = Traceback::new(vec![], "Error", "test");
assert!(traceback.frames.is_empty());
traceback.push_frame(TracebackFrame::new("added", 1));
assert_eq!(traceback.frames.len(), 1);
assert_eq!(traceback.frames[0].name, "added");
}
#[test]
fn test_traceback_clone() {
let traceback = Traceback::new(vec![TracebackFrame::new("func", 1)], "Error", "message");
let cloned = traceback.clone();
assert_eq!(traceback.exception_type, cloned.exception_type);
assert_eq!(traceback.exception_message, cloned.exception_message);
assert_eq!(traceback.frames.len(), cloned.frames.len());
}
#[test]
fn test_traceback_eq() {
let tb1 = Traceback::new(vec![TracebackFrame::new("f", 1)], "E", "m");
let tb2 = Traceback::new(vec![TracebackFrame::new("f", 1)], "E", "m");
assert_eq!(tb1, tb2);
}
#[test]
fn test_render_single_frame_no_source() {
let traceback = Traceback::new(
vec![TracebackFrame::new("single_func", 99)],
"SingleError",
"single message",
);
let output = render_to_text(&traceback, 80);
assert!(output.contains("single_func"));
assert!(output.contains("99"));
assert!(output.contains("SingleError: single message"));
}
#[test]
fn test_render_multi_frame_order() {
let traceback = Traceback::new(
vec![
TracebackFrame::new("first", 1),
TracebackFrame::new("second", 2),
TracebackFrame::new("third", 3),
],
"Error",
"test",
);
let output = render_to_text(&traceback, 80);
assert!(output.contains("first"));
assert!(output.contains("second"));
assert!(output.contains("third"));
}
#[test]
fn test_render_exception_display_bold_red() {
let traceback = Traceback::new(vec![], "ValueError", "invalid value");
let console = Console::new();
let options = ConsoleOptions {
max_width: 80,
..Default::default()
};
let segments = traceback.render(&console, &options);
let exception_seg = segments.iter().find(|s| s.text.contains("ValueError"));
assert!(exception_seg.is_some());
}
#[test]
fn test_render_source_context_line_numbers() {
let source = "line1\nline2\nline3";
let frame = TracebackFrame::new("func", 2).source_context(source, 1);
let traceback = Traceback::new(vec![frame], "Error", "test").extra_lines(1);
let output = render_to_text(&traceback, 80);
assert!(output.contains('1'));
assert!(output.contains('2'));
assert!(output.contains('3'));
}
#[test]
fn test_render_error_line_indicator() {
let source = "before\nerror_line\nafter";
let frame = TracebackFrame::new("func", 2).source_context(source, 1);
let traceback = Traceback::new(vec![frame], "Error", "test").extra_lines(1);
let output = render_to_text(&traceback, 80);
assert!(output.contains("❱"));
assert!(output.contains("error_line"));
}
#[test]
fn test_render_width_constraint_narrow() {
let traceback = Traceback::new(
vec![TracebackFrame::new(
"very_long_function_name_that_might_wrap",
123_456,
)],
"LongError",
"a very long error message that might need to be handled",
);
let output = render_to_text(&traceback, 40);
assert!(output.contains("LongError"));
}
#[test]
fn test_render_width_constraint_minimum() {
let traceback = Traceback::new(vec![], "Error", "msg");
let output = render_to_text(&traceback, 1);
assert!(!output.is_empty());
}
#[test]
fn test_render_with_filename_in_header() {
let frame = TracebackFrame::new("main", 10)
.filename("src/main.rs")
.source_context("fn main() { }", 10);
let traceback = Traceback::new(vec![frame], "Error", "test");
let output = render_to_text(&traceback, 80);
assert!(output.contains("src/main.rs"));
assert!(output.contains("main"));
assert!(output.contains("10"));
}
#[test]
fn test_render_without_filename_fallback() {
let frame = TracebackFrame::new("anonymous", 5).source_context("some_code()", 5);
let traceback = Traceback::new(vec![frame], "Error", "test");
let output = render_to_text(&traceback, 80);
assert!(output.contains("anonymous"));
assert!(output.contains('5'));
}
#[test]
fn test_render_default_title() {
let traceback = Traceback::new(vec![], "Error", "test");
let output = render_to_text(&traceback, 80);
assert!(output.contains("Traceback"));
}
#[test]
fn test_render_custom_title() {
let traceback = Traceback::new(vec![], "Error", "test").title("Exception occurred!");
let output = render_to_text(&traceback, 80);
assert!(output.contains("Exception occurred!"));
}
#[test]
fn test_render_long_source_line_truncation() {
let long_line = "x".repeat(200);
let frame = TracebackFrame::new("func", 1).source_context(&long_line, 1);
let traceback = Traceback::new(vec![frame], "Error", "test");
let output = render_to_text(&traceback, 60);
assert!(!output.is_empty());
}
#[test]
fn test_render_unicode_in_source() {
let source = "let emoji = '🚀';";
let frame = TracebackFrame::new("func", 1).source_context(source, 1);
let traceback = Traceback::new(vec![frame], "Error", "test");
let output = render_to_text(&traceback, 80);
assert!(output.contains("🚀"));
}
#[test]
fn test_render_empty_exception_message() {
let traceback = Traceback::new(vec![], "Error", "");
let output = render_to_text(&traceback, 80);
assert!(output.contains("Error:"));
}
#[test]
fn test_render_special_chars_in_exception() {
let traceback = Traceback::new(vec![], "Error<T>", "msg with \"quotes\" & <brackets>");
let output = render_to_text(&traceback, 80);
assert!(output.contains("Error<T>"));
assert!(output.contains("quotes"));
}
#[cfg(feature = "backtrace")]
mod backtrace_tests {
use super::*;
fn inner_function() -> Traceback {
Traceback::capture("TestError", "test message")
}
fn outer_function() -> Traceback {
inner_function()
}
#[test]
fn capture_creates_traceback_with_frames() {
let traceback = outer_function();
assert!(!traceback.frames.is_empty(), "should capture frames");
assert_eq!(traceback.exception_type, "TestError");
assert_eq!(traceback.exception_message, "test message");
}
#[test]
fn capture_filters_internal_frames() {
let traceback = Traceback::capture("Error", "test");
for frame in &traceback.frames {
assert!(
!frame.name.starts_with("std::"),
"should filter std:: frames: {}",
frame.name
);
assert!(
!frame.name.starts_with("core::"),
"should filter core:: frames: {}",
frame.name
);
}
}
#[test]
fn is_internal_frame_detects_runtime() {
assert!(Traceback::is_internal_frame("std::rt::lang_start"));
assert!(Traceback::is_internal_frame("core::ops::function::FnOnce"));
assert!(Traceback::is_internal_frame("__libc_start_main"));
assert!(!Traceback::is_internal_frame("main"));
assert!(!Traceback::is_internal_frame("my_crate::my_function"));
assert!(!Traceback::is_internal_frame("app::handler::process"));
}
#[test]
fn demangle_removes_hash_suffix() {
assert_eq!(
Traceback::demangle_name("my_crate::func::h1234567890abcdef"),
"my_crate::func"
);
assert_eq!(Traceback::demangle_name("my_crate::func"), "my_crate::func");
}
#[test]
fn capture_renders_without_panic() {
let traceback = Traceback::capture("PanicError", "something went wrong");
let output = render_to_text(&traceback, 100);
assert!(output.contains("PanicError: something went wrong"));
assert!(output.contains("Traceback"));
}
#[test]
fn is_internal_frame_detects_backtrace_frames() {
assert!(Traceback::is_internal_frame("backtrace::capture"));
assert!(Traceback::is_internal_frame("backtrace::Backtrace::new"));
}
#[test]
fn is_internal_frame_detects_alloc_frames() {
assert!(Traceback::is_internal_frame("<alloc::boxed::Box<F,A>"));
assert!(Traceback::is_internal_frame("alloc::vec::Vec::push"));
}
#[test]
fn is_internal_frame_detects_rust_internals() {
assert!(Traceback::is_internal_frame("rust_begin_unwind"));
assert!(Traceback::is_internal_frame("__rust_start_panic"));
assert!(Traceback::is_internal_frame("_start"));
assert!(Traceback::is_internal_frame("clone"));
}
#[test]
fn is_internal_frame_filters_own_capture() {
assert!(Traceback::is_internal_frame(
"rich_rust::renderables::traceback::Traceback::capture"
));
assert!(Traceback::is_internal_frame(
"rich_rust::renderables::traceback::Traceback::from_backtrace"
));
}
#[test]
fn demangle_preserves_non_hash_suffix() {
assert_eq!(
Traceback::demangle_name("module::func::inner"),
"module::func::inner"
);
assert_eq!(
Traceback::demangle_name("crate::module::Type::method"),
"crate::module::Type::method"
);
}
#[test]
fn demangle_handles_various_suffixes() {
assert_eq!(Traceback::demangle_name("func::habcd"), "func");
assert_eq!(Traceback::demangle_name("func::hnotahex"), "func::hnotahex");
}
#[test]
fn from_backtrace_creates_traceback() {
let bt = backtrace::Backtrace::new();
let traceback = Traceback::from_backtrace(&bt, "BTError", "from backtrace");
assert_eq!(traceback.exception_type, "BTError");
assert_eq!(traceback.exception_message, "from backtrace");
}
}
}