use std::collections::BTreeMap;
use std::fs;
use std::io::Stdout;
use std::path::Path;
use crate::Renderable;
use crate::r#box::ROUNDED;
use crate::console::{Console, ConsoleOptions};
use crate::highlighter::{Highlighter, RegexHighlighter, path_highlighter, repr_highlighter};
use crate::measure::Measurement;
use crate::scope::render_scope;
use crate::segment::{Segment, Segments};
use crate::style::Style;
use crate::syntax::Syntax;
use crate::text::Text;
pub const DEFAULT_MAX_FRAMES: usize = 100;
pub const DEFAULT_EXTRA_LINES: usize = 3;
pub const LOCALS_MAX_LENGTH: usize = 10;
pub const LOCALS_MAX_STRING: usize = 80;
#[derive(Debug, Clone)]
pub struct Frame {
pub filename: String,
pub lineno: usize,
pub name: String,
pub line: String,
pub locals: Option<BTreeMap<String, String>>,
}
impl Frame {
pub fn new(filename: impl Into<String>, lineno: usize, name: impl Into<String>) -> Self {
Self {
filename: filename.into(),
lineno,
name: name.into(),
line: String::new(),
locals: None,
}
}
pub fn with_line(mut self, line: impl Into<String>) -> Self {
self.line = line.into();
self
}
pub fn with_locals(mut self, locals: BTreeMap<String, String>) -> Self {
self.locals = Some(locals);
self
}
pub fn add_local(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.locals
.get_or_insert_with(BTreeMap::new)
.insert(name.into(), value.into());
}
pub fn has_locals(&self) -> bool {
self.locals.as_ref().is_some_and(|l| !l.is_empty())
}
}
#[derive(Debug, Clone)]
pub struct SyntaxErrorInfo {
pub offset: usize,
pub filename: String,
pub line: String,
pub lineno: usize,
pub msg: String,
}
impl SyntaxErrorInfo {
pub fn new(
filename: impl Into<String>,
lineno: usize,
offset: usize,
msg: impl Into<String>,
) -> Self {
Self {
offset,
filename: filename.into(),
line: String::new(),
lineno,
msg: msg.into(),
}
}
pub fn with_line(mut self, line: impl Into<String>) -> Self {
self.line = line.into();
self
}
}
#[derive(Debug, Clone)]
pub struct Stack {
pub exc_type: String,
pub exc_value: String,
pub syntax_error: Option<SyntaxErrorInfo>,
pub is_cause: bool,
pub frames: Vec<Frame>,
}
impl Stack {
pub fn new(exc_type: impl Into<String>, exc_value: impl Into<String>) -> Self {
Self {
exc_type: exc_type.into(),
exc_value: exc_value.into(),
syntax_error: None,
is_cause: false,
frames: Vec::new(),
}
}
pub fn with_syntax_error(mut self, error: SyntaxErrorInfo) -> Self {
self.syntax_error = Some(error);
self
}
pub fn with_is_cause(mut self, is_cause: bool) -> Self {
self.is_cause = is_cause;
self
}
pub fn with_frames(mut self, frames: Vec<Frame>) -> Self {
self.frames = frames;
self
}
pub fn with_frame(mut self, frame: Frame) -> Self {
self.frames.push(frame);
self
}
pub fn add_frame(&mut self, frame: Frame) {
self.frames.push(frame);
}
pub fn is_syntax_error(&self) -> bool {
self.syntax_error.is_some()
}
pub fn frame_count(&self) -> usize {
self.frames.len()
}
}
#[derive(Debug, Clone)]
pub struct Trace {
pub stacks: Vec<Stack>,
}
impl Trace {
pub fn new(stacks: Vec<Stack>) -> Self {
Self { stacks }
}
pub fn empty() -> Self {
Self { stacks: Vec::new() }
}
pub fn with_stack(mut self, stack: Stack) -> Self {
self.stacks.push(stack);
self
}
pub fn add_stack(&mut self, stack: Stack) {
self.stacks.push(stack);
}
pub fn stack_count(&self) -> usize {
self.stacks.len()
}
pub fn is_empty(&self) -> bool {
self.stacks.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct TracebackBuilder {
trace: Trace,
width: Option<usize>,
extra_lines: usize,
theme: Option<String>,
word_wrap: bool,
show_locals: bool,
locals_max_length: Option<usize>,
locals_max_string: Option<usize>,
locals_hide_dunder: bool,
locals_hide_sunder: bool,
indent_guides: bool,
suppress: Vec<String>,
max_frames: usize,
}
impl TracebackBuilder {
pub fn new(trace: Trace) -> Self {
Self {
trace,
width: None,
extra_lines: DEFAULT_EXTRA_LINES,
theme: None,
word_wrap: false,
show_locals: false,
locals_max_length: Some(LOCALS_MAX_LENGTH),
locals_max_string: Some(LOCALS_MAX_STRING),
locals_hide_dunder: true,
locals_hide_sunder: false,
indent_guides: true,
suppress: Vec::new(),
max_frames: DEFAULT_MAX_FRAMES,
}
}
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
pub fn extra_lines(mut self, lines: usize) -> Self {
self.extra_lines = lines;
self
}
pub fn theme(mut self, theme: impl Into<String>) -> Self {
self.theme = Some(theme.into());
self
}
pub fn word_wrap(mut self, wrap: bool) -> Self {
self.word_wrap = wrap;
self
}
pub fn show_locals(mut self, show: bool) -> Self {
self.show_locals = show;
self
}
pub fn locals_max_length(mut self, max: Option<usize>) -> Self {
self.locals_max_length = max;
self
}
pub fn locals_max_string(mut self, max: Option<usize>) -> Self {
self.locals_max_string = max;
self
}
pub fn locals_hide_dunder(mut self, hide: bool) -> Self {
self.locals_hide_dunder = hide;
self
}
pub fn locals_hide_sunder(mut self, hide: bool) -> Self {
self.locals_hide_sunder = hide;
self
}
pub fn indent_guides(mut self, guides: bool) -> Self {
self.indent_guides = guides;
self
}
pub fn suppress(mut self, path: impl Into<String>) -> Self {
self.suppress.push(path.into());
self
}
pub fn suppress_all(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.suppress.extend(paths.into_iter().map(Into::into));
self
}
pub fn max_frames(mut self, max: usize) -> Self {
self.max_frames = if max > 0 { max.max(4) } else { 0 };
self
}
pub fn build(self) -> Traceback {
Traceback {
trace: self.trace,
width: self.width,
extra_lines: self.extra_lines,
theme: self.theme,
word_wrap: self.word_wrap,
show_locals: self.show_locals,
locals_max_length: self.locals_max_length,
locals_max_string: self.locals_max_string,
locals_hide_dunder: self.locals_hide_dunder,
locals_hide_sunder: self.locals_hide_sunder,
indent_guides: self.indent_guides,
suppress: self.suppress,
max_frames: self.max_frames,
}
}
}
#[derive(Debug, Clone)]
pub struct Traceback {
pub trace: Trace,
pub width: Option<usize>,
pub extra_lines: usize,
pub theme: Option<String>,
pub word_wrap: bool,
pub show_locals: bool,
pub locals_max_length: Option<usize>,
pub locals_max_string: Option<usize>,
pub locals_hide_dunder: bool,
pub locals_hide_sunder: bool,
pub indent_guides: bool,
pub suppress: Vec<String>,
pub max_frames: usize,
}
impl Traceback {
pub fn new(trace: Trace) -> Self {
Self {
trace,
width: None,
extra_lines: DEFAULT_EXTRA_LINES,
theme: None,
word_wrap: false,
show_locals: false,
locals_max_length: Some(LOCALS_MAX_LENGTH),
locals_max_string: Some(LOCALS_MAX_STRING),
locals_hide_dunder: true,
locals_hide_sunder: false,
indent_guides: true,
suppress: Vec::new(),
max_frames: DEFAULT_MAX_FRAMES,
}
}
pub fn builder(trace: Trace) -> TracebackBuilder {
TracebackBuilder::new(trace)
}
pub fn trace(&self) -> &Trace {
&self.trace
}
pub fn should_show_locals(&self) -> bool {
self.show_locals
}
pub fn filter_locals(&self, locals: &BTreeMap<String, String>) -> BTreeMap<String, String> {
locals
.iter()
.filter(|(name, _)| {
if self.locals_hide_dunder && name.starts_with("__") && name.ends_with("__") {
return false;
}
if self.locals_hide_sunder && name.starts_with('_') && !name.starts_with("__") {
return false;
}
true
})
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn is_suppressed(&self, path: &str) -> bool {
self.suppress.iter().any(|s| path.contains(s))
}
}
fn traceback_title_style() -> Style {
Style::new()
.with_color(crate::color::SimpleColor::Standard(9)) .with_bold(true)
}
fn traceback_border_style() -> Style {
Style::new().with_color(crate::color::SimpleColor::Standard(1)) }
fn exc_type_style() -> Style {
Style::new()
.with_color(crate::color::SimpleColor::Standard(1)) .with_bold(true)
}
fn path_style() -> Style {
Style::new().with_color(crate::color::SimpleColor::Standard(5)) }
fn lineno_style() -> Style {
Style::new().with_color(crate::color::SimpleColor::Standard(6)) }
fn function_style() -> Style {
Style::new().with_color(crate::color::SimpleColor::Standard(2)) }
fn frames_hidden_style() -> Style {
Style::new()
.with_color(crate::color::SimpleColor::Standard(3)) .with_italic(true)
}
fn syntax_error_offset_style() -> Style {
Style::new()
.with_color(crate::color::SimpleColor::Standard(1)) .with_bold(true)
}
impl Renderable for Traceback {
fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
let mut result = Segments::new();
let width = self
.width
.unwrap_or(options.max_width)
.min(options.max_width);
let code_width = width.saturating_sub(8);
let stacks: Vec<&Stack> = self.trace.stacks.iter().rev().collect();
for (idx, stack) in stacks.iter().enumerate() {
let is_last = idx == stacks.len() - 1;
if !stack.frames.is_empty() {
let stack_segments = self.render_stack_frames(stack, console, options, code_width);
let mut title = Text::styled("Traceback ", traceback_title_style());
title.append(
"(most recent call last)",
Some(traceback_title_style().with_dim(true)),
);
let border_style = traceback_border_style();
let box_chars = &ROUNDED;
let title_str = title.plain_text();
let title_len = crate::cells::cell_len(&title_str);
let inner_width = width.saturating_sub(2);
let title_pad = inner_width.saturating_sub(title_len).saturating_sub(2);
let left_pad = title_pad / 2;
let right_pad = title_pad - left_pad;
result.push(Segment::styled(
box_chars.top_left.to_string(),
border_style,
));
result.push(Segment::styled(
box_chars.top.to_string().repeat(left_pad + 1),
border_style,
));
result.extend(title.render(console, options));
result.push(Segment::styled(
box_chars.top.to_string().repeat(right_pad + 1),
border_style,
));
result.push(Segment::styled(
box_chars.top_right.to_string(),
border_style,
));
result.push(Segment::line());
let segment_vec: Vec<Segment> = stack_segments.iter().cloned().collect();
let content_lines = Segment::split_lines(segment_vec);
for line_segments in content_lines {
result.push(Segment::styled(
format!("{} ", box_chars.mid_left),
border_style,
));
result.extend(line_segments.into_iter().map(|s| Segment::from(s)));
let line_width: usize = result
.iter()
.skip(result.len().saturating_sub(10))
.map(|s| crate::cells::cell_len(&s.text))
.sum();
let padding = inner_width.saturating_sub(line_width.saturating_sub(2));
if padding > 0 {
result.push(Segment::new(" ".repeat(padding)));
}
result.push(Segment::styled(
format!(" {}", box_chars.mid_right),
border_style,
));
result.push(Segment::line());
}
result.push(Segment::styled(
box_chars.bottom_left.to_string(),
border_style,
));
result.push(Segment::styled(
box_chars.bottom.to_string().repeat(inner_width),
border_style,
));
result.push(Segment::styled(
box_chars.bottom_right.to_string(),
border_style,
));
result.push(Segment::line());
}
if let Some(ref syntax_error) = stack.syntax_error {
let syntax_error_segments =
self.render_syntax_error_content(syntax_error, console, options);
result.extend(syntax_error_segments);
result.push(Segment::line());
}
let exc_text = self.render_exception_line(stack);
let exc_segments = exc_text.render(console, options);
result.extend(exc_segments);
result.push(Segment::line());
if !is_last {
let chaining_msg = if stack.is_cause {
"\nThe above exception was the direct cause of the following exception:\n"
} else {
"\nDuring handling of the above exception, another exception occurred:\n"
};
let chaining_text = Text::styled(chaining_msg, Style::new().with_italic(true));
let chaining_segments = chaining_text.render(console, options);
result.extend(chaining_segments);
result.push(Segment::line());
}
}
result
}
fn measure(&self, _console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
let width = self
.width
.unwrap_or(options.max_width)
.min(options.max_width);
Measurement::new(width, width)
}
}
impl Traceback {
fn render_stack_frames(
&self,
stack: &Stack,
console: &Console<Stdout>,
options: &ConsoleOptions,
code_width: usize,
) -> Segments {
let mut result = Segments::new();
let highlighter = path_highlighter();
let frames = &stack.frames;
let exclude_range = if self.max_frames > 0 && frames.len() > self.max_frames {
let half = self.max_frames / 2;
Some(half..(frames.len() - half))
} else {
None
};
let mut excluded_shown = false;
for (idx, frame) in frames.iter().enumerate() {
if let Some(ref range) = exclude_range {
if range.contains(&idx) {
if !excluded_shown {
let msg = format!("\n... {} frames hidden ...\n", range.len());
let hidden_text = Text::styled(&msg, frames_hidden_style());
result.extend(hidden_text.render(console, options));
excluded_shown = true;
}
continue;
}
}
let suppressed = self.is_suppressed(&frame.filename);
if idx > 0 && !frame.filename.starts_with('<') {
result.push(Segment::line());
}
let location_text = self.render_frame_location(frame, &highlighter);
result.extend(location_text.render(console, options));
result.push(Segment::line());
if frame.filename.starts_with('<') || suppressed {
if self.show_locals && frame.has_locals() {
self.render_locals(&mut result, frame, console, options);
}
continue;
}
if let Some(code) = self.read_source_code(&frame.filename) {
let lexer = self.guess_lexer(&frame.filename);
let start_line = frame.lineno.saturating_sub(self.extra_lines);
let end_line = frame.lineno + self.extra_lines;
let mut syntax = Syntax::new(&code, lexer)
.with_line_numbers(true)
.with_line_range(Some(start_line), Some(end_line))
.with_highlight_lines(vec![frame.lineno])
.with_indent_guides(self.indent_guides)
.with_word_wrap(self.word_wrap);
if let Some(ref theme) = self.theme {
syntax = syntax.with_theme(theme);
}
if code_width > 0 {
syntax = syntax.with_code_width(code_width);
}
result.extend(syntax.render(console, options));
}
if self.show_locals && frame.has_locals() {
result.push(Segment::line());
self.render_locals(&mut result, frame, console, options);
}
}
result
}
fn render_frame_location(&self, frame: &Frame, highlighter: &RegexHighlighter) -> Text {
let path = Path::new(&frame.filename);
if path.exists() {
let mut text = Text::new();
let mut path_text = Text::styled(&frame.filename, path_style());
highlighter.highlight(&mut path_text);
text.append_text(&path_text);
text.append(":", None);
text.append(&frame.lineno.to_string(), Some(lineno_style()));
text.append(" in ", None);
text.append(&frame.name, Some(function_style()));
text
} else {
let mut text = Text::new();
text.append("in ", None);
text.append(&frame.name, Some(function_style()));
text.append(":", None);
text.append(&frame.lineno.to_string(), Some(lineno_style()));
text
}
}
fn render_locals(
&self,
result: &mut Segments,
frame: &Frame,
console: &Console<Stdout>,
options: &ConsoleOptions,
) {
if let Some(ref locals) = frame.locals {
let filtered = self.filter_locals(locals);
if !filtered.is_empty() {
let scope = render_scope(
&filtered,
Some("locals"),
true, self.indent_guides,
self.locals_max_length,
self.locals_max_string,
);
result.extend(scope.render(console, options));
}
}
}
fn render_syntax_error_content(
&self,
error: &SyntaxErrorInfo,
console: &Console<Stdout>,
options: &ConsoleOptions,
) -> Segments {
let mut result = Segments::new();
let highlighter = path_highlighter();
let repr_hl = repr_highlighter();
if error.filename != "<stdin>" {
let path = Path::new(&error.filename);
if path.exists() {
let mut location = Text::new();
location.append(" ", None);
let mut path_text = Text::styled(&error.filename, path_style());
highlighter.highlight(&mut path_text);
location.append_text(&path_text);
location.append(":", None);
location.append(&error.lineno.to_string(), Some(lineno_style()));
result.extend(location.render(console, options));
result.push(Segment::line());
}
}
let mut error_line = Text::plain(error.line.trim_end());
repr_hl.highlight(&mut error_line);
let offset = error
.offset
.saturating_sub(1)
.min(error_line.plain_text().len());
if offset < error_line.plain_text().len() {
error_line.stylize(
offset,
offset + 1,
Style::new().with_bold(true).with_underline(true),
);
}
result.extend(error_line.render(console, options));
let indicator = format!("\n{}▲", " ".repeat(offset));
let indicator_text = Text::styled(&indicator, syntax_error_offset_style());
result.extend(indicator_text.render(console, options));
result
}
fn render_exception_line(&self, stack: &Stack) -> Text {
let highlighter = repr_highlighter();
let mut text = Text::new();
text.append(&format!("{}: ", stack.exc_type), Some(exc_type_style()));
let mut value_text = Text::plain(&stack.exc_value);
highlighter.highlight(&mut value_text);
text.append_text(&value_text);
text
}
fn read_source_code(&self, filename: &str) -> Option<String> {
let path = Path::new(filename);
if path.exists() {
fs::read_to_string(path).ok()
} else {
None
}
}
fn guess_lexer(&self, filename: &str) -> &'static str {
let ext = Path::new(filename)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
match ext {
"rs" => "rust",
"py" => "python",
"js" | "mjs" => "javascript",
"ts" | "mts" => "typescript",
"tsx" => "tsx",
"jsx" => "jsx",
"c" | "h" => "c",
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
"go" => "go",
"java" => "java",
"rb" => "ruby",
"sh" | "bash" => "bash",
"json" => "json",
"toml" => "toml",
"yaml" | "yml" => "yaml",
"md" | "markdown" => "markdown",
"html" | "htm" => "html",
"css" => "css",
"sql" => "sql",
"xml" => "xml",
_ => "text",
}
}
}
pub fn install() {
install_with_options(TracebackBuilder::new(Trace::empty()));
}
pub fn install_with_options(builder: TracebackBuilder) {
use crate::ColorSystem;
use std::io::{self, Write};
let config = builder.build();
std::panic::set_hook(Box::new(move |panic_info| {
let console = Console::new();
let options = console.options();
let location = panic_info.location();
let (filename, lineno, _col) = location
.map(|l| (l.file().to_string(), l.line() as usize, l.column() as usize))
.unwrap_or_else(|| ("<unknown>".to_string(), 0, 0));
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Box<dyn Any>".to_string()
};
let frame = Frame::new(&filename, lineno, "panic");
let stack = Stack::new("panic", &message).with_frame(frame);
let trace = Trace::new(vec![stack]);
let traceback = Traceback {
trace,
width: config.width,
extra_lines: config.extra_lines,
theme: config.theme.clone(),
word_wrap: config.word_wrap,
show_locals: false, locals_max_length: config.locals_max_length,
locals_max_string: config.locals_max_string,
locals_hide_dunder: config.locals_hide_dunder,
locals_hide_sunder: config.locals_hide_sunder,
indent_guides: config.indent_guides,
suppress: config.suppress.clone(),
max_frames: config.max_frames,
};
let segments = traceback.render(&console, &options);
let color_system = ColorSystem::TrueColor;
let stderr = io::stderr();
let mut handle = stderr.lock();
for segment in segments.iter() {
if segment.is_control() {
continue;
}
if let Some(style) = segment.style {
let _ = write!(handle, "{}", style.render(&segment.text, color_system));
} else {
let _ = write!(handle, "{}", segment.text);
}
}
let _ = writeln!(handle);
let _ = handle.flush();
}));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_new() {
let frame = Frame::new("main.rs", 42, "main");
assert_eq!(frame.filename, "main.rs");
assert_eq!(frame.lineno, 42);
assert_eq!(frame.name, "main");
assert!(frame.line.is_empty());
assert!(frame.locals.is_none());
}
#[test]
fn test_frame_with_line() {
let frame = Frame::new("test.rs", 10, "test").with_line(" let x = 42;");
assert_eq!(frame.line, " let x = 42;");
}
#[test]
fn test_frame_with_locals() {
let mut locals = BTreeMap::new();
locals.insert("x".to_string(), "42".to_string());
let frame = Frame::new("test.rs", 10, "test").with_locals(locals);
assert!(frame.has_locals());
assert_eq!(frame.locals.unwrap().get("x"), Some(&"42".to_string()));
}
#[test]
fn test_frame_add_local() {
let mut frame = Frame::new("test.rs", 10, "test");
frame.add_local("x", "42");
frame.add_local("y", "100");
assert!(frame.has_locals());
let locals = frame.locals.unwrap();
assert_eq!(locals.len(), 2);
}
#[test]
fn test_syntax_error_info_new() {
let info = SyntaxErrorInfo::new("test.rs", 5, 10, "unexpected token");
assert_eq!(info.filename, "test.rs");
assert_eq!(info.lineno, 5);
assert_eq!(info.offset, 10);
assert_eq!(info.msg, "unexpected token");
}
#[test]
fn test_syntax_error_info_with_line() {
let info = SyntaxErrorInfo::new("test.rs", 5, 10, "error").with_line("let x = ;");
assert_eq!(info.line, "let x = ;");
}
#[test]
fn test_stack_new() {
let stack = Stack::new("ValueError", "invalid input");
assert_eq!(stack.exc_type, "ValueError");
assert_eq!(stack.exc_value, "invalid input");
assert!(!stack.is_cause);
assert!(stack.frames.is_empty());
assert!(!stack.is_syntax_error());
}
#[test]
fn test_stack_with_frame() {
let frame = Frame::new("test.rs", 10, "test");
let stack = Stack::new("Error", "msg").with_frame(frame);
assert_eq!(stack.frame_count(), 1);
}
#[test]
fn test_stack_with_frames() {
let frames = vec![Frame::new("a.rs", 1, "a"), Frame::new("b.rs", 2, "b")];
let stack = Stack::new("Error", "msg").with_frames(frames);
assert_eq!(stack.frame_count(), 2);
}
#[test]
fn test_stack_add_frame() {
let mut stack = Stack::new("Error", "msg");
stack.add_frame(Frame::new("a.rs", 1, "a"));
stack.add_frame(Frame::new("b.rs", 2, "b"));
assert_eq!(stack.frame_count(), 2);
}
#[test]
fn test_stack_with_syntax_error() {
let syntax_err = SyntaxErrorInfo::new("test.rs", 5, 10, "error");
let stack = Stack::new("SyntaxError", "msg").with_syntax_error(syntax_err);
assert!(stack.is_syntax_error());
}
#[test]
fn test_stack_is_cause() {
let stack = Stack::new("Error", "caused by").with_is_cause(true);
assert!(stack.is_cause);
}
#[test]
fn test_trace_new() {
let stacks = vec![Stack::new("Error", "msg")];
let trace = Trace::new(stacks);
assert_eq!(trace.stack_count(), 1);
assert!(!trace.is_empty());
}
#[test]
fn test_trace_empty() {
let trace = Trace::empty();
assert!(trace.is_empty());
assert_eq!(trace.stack_count(), 0);
}
#[test]
fn test_trace_with_stack() {
let trace = Trace::empty()
.with_stack(Stack::new("E1", "m1"))
.with_stack(Stack::new("E2", "m2"));
assert_eq!(trace.stack_count(), 2);
}
#[test]
fn test_trace_add_stack() {
let mut trace = Trace::empty();
trace.add_stack(Stack::new("E1", "m1"));
trace.add_stack(Stack::new("E2", "m2"));
assert_eq!(trace.stack_count(), 2);
}
#[test]
fn test_traceback_new() {
let trace = Trace::new(vec![Stack::new("Error", "msg")]);
let tb = Traceback::new(trace);
assert!(tb.width.is_none());
assert_eq!(tb.extra_lines, DEFAULT_EXTRA_LINES);
assert!(tb.theme.is_none());
assert!(!tb.word_wrap);
assert!(!tb.show_locals);
assert!(tb.locals_hide_dunder);
assert!(!tb.locals_hide_sunder);
assert!(tb.indent_guides);
assert!(tb.suppress.is_empty());
assert_eq!(tb.max_frames, DEFAULT_MAX_FRAMES);
}
#[test]
fn test_traceback_builder() {
let trace = Trace::new(vec![Stack::new("Error", "msg")]);
let tb = Traceback::builder(trace)
.width(100)
.extra_lines(5)
.theme("monokai")
.word_wrap(true)
.show_locals(true)
.locals_max_length(Some(20))
.locals_max_string(Some(100))
.locals_hide_dunder(false)
.locals_hide_sunder(true)
.indent_guides(false)
.suppress("/usr/lib")
.suppress_all(vec!["site-packages"])
.max_frames(50)
.build();
assert_eq!(tb.width, Some(100));
assert_eq!(tb.extra_lines, 5);
assert_eq!(tb.theme, Some("monokai".to_string()));
assert!(tb.word_wrap);
assert!(tb.show_locals);
assert_eq!(tb.locals_max_length, Some(20));
assert_eq!(tb.locals_max_string, Some(100));
assert!(!tb.locals_hide_dunder);
assert!(tb.locals_hide_sunder);
assert!(!tb.indent_guides);
assert_eq!(tb.suppress.len(), 2);
assert_eq!(tb.max_frames, 50);
}
#[test]
fn test_traceback_max_frames_minimum() {
let trace = Trace::empty();
let tb = Traceback::builder(trace).max_frames(2).build();
assert_eq!(tb.max_frames, 4);
}
#[test]
fn test_traceback_max_frames_zero() {
let trace = Trace::empty();
let tb = Traceback::builder(trace).max_frames(0).build();
assert_eq!(tb.max_frames, 0);
}
#[test]
fn test_traceback_filter_locals() {
let trace = Trace::empty();
let tb = Traceback::builder(trace)
.locals_hide_dunder(true)
.locals_hide_sunder(true)
.build();
let mut locals = BTreeMap::new();
locals.insert("x".to_string(), "1".to_string());
locals.insert("_private".to_string(), "2".to_string());
locals.insert("__dunder__".to_string(), "3".to_string());
locals.insert("normal_var".to_string(), "4".to_string());
let filtered = tb.filter_locals(&locals);
assert!(filtered.contains_key("x"));
assert!(filtered.contains_key("normal_var"));
assert!(!filtered.contains_key("_private")); assert!(!filtered.contains_key("__dunder__")); }
#[test]
fn test_traceback_filter_locals_show_all() {
let trace = Trace::empty();
let tb = Traceback::builder(trace)
.locals_hide_dunder(false)
.locals_hide_sunder(false)
.build();
let mut locals = BTreeMap::new();
locals.insert("x".to_string(), "1".to_string());
locals.insert("_private".to_string(), "2".to_string());
locals.insert("__dunder__".to_string(), "3".to_string());
let filtered = tb.filter_locals(&locals);
assert_eq!(filtered.len(), 3);
}
#[test]
fn test_traceback_is_suppressed() {
let trace = Trace::empty();
let tb = Traceback::builder(trace)
.suppress("/usr/lib/python")
.suppress("site-packages")
.build();
assert!(tb.is_suppressed("/usr/lib/python/foo.py"));
assert!(tb.is_suppressed("/home/user/.local/lib/site-packages/bar.py"));
assert!(!tb.is_suppressed("/home/user/project/main.py"));
}
#[test]
fn test_traceback_should_show_locals() {
let trace = Trace::empty();
let tb1 = Traceback::new(trace.clone());
let tb2 = Traceback::builder(trace).show_locals(true).build();
assert!(!tb1.should_show_locals());
assert!(tb2.should_show_locals());
}
#[test]
fn test_frame_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Frame>();
assert_sync::<Frame>();
}
#[test]
fn test_stack_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Stack>();
assert_sync::<Stack>();
}
#[test]
fn test_trace_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Trace>();
assert_sync::<Trace>();
}
#[test]
fn test_traceback_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Traceback>();
assert_sync::<Traceback>();
}
#[test]
fn test_traceback_render_basic() {
use crate::{Console, Renderable};
let frame = Frame::new("test.rs", 42, "test_function").with_line(" let x = 42;");
let stack = Stack::new("RuntimeError", "Something went wrong").with_frame(frame);
let trace = Trace::new(vec![stack]);
let tb = Traceback::new(trace);
let console = Console::new();
let options = console.options();
let segments = tb.render(&console, &options);
assert!(!segments.is_empty());
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains("RuntimeError"));
assert!(output.contains("Something went wrong"));
assert!(output.contains("Traceback"));
}
#[test]
fn test_traceback_render_with_chaining() {
use crate::{Console, Renderable};
let frame1 = Frame::new("inner.rs", 10, "inner_fn");
let stack1 = Stack::new("ValueError", "inner error").with_frame(frame1);
let frame2 = Frame::new("outer.rs", 20, "outer_fn");
let stack2 = Stack::new("RuntimeError", "outer error")
.with_frame(frame2)
.with_is_cause(true);
let trace = Trace::new(vec![stack1, stack2]);
let tb = Traceback::new(trace);
let console = Console::new();
let options = console.options();
let segments = tb.render(&console, &options);
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains("ValueError"));
assert!(output.contains("RuntimeError"));
assert!(output.contains("above exception") || output.contains("another exception"));
}
#[test]
fn test_traceback_render_empty() {
use crate::{Console, Renderable};
let trace = Trace::empty();
let tb = Traceback::new(trace);
let console = Console::new();
let options = console.options();
let segments = tb.render(&console, &options);
assert!(segments.is_empty());
}
#[test]
fn test_traceback_render_syntax_error() {
use crate::{Console, Renderable};
let syntax_err =
SyntaxErrorInfo::new("test.py", 5, 10, "unexpected token").with_line("def foo(:");
let stack = Stack::new("SyntaxError", "invalid syntax").with_syntax_error(syntax_err);
let trace = Trace::new(vec![stack]);
let tb = Traceback::new(trace);
let console = Console::new();
let options = console.options();
let segments = tb.render(&console, &options);
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains("SyntaxError"));
assert!(output.contains("invalid syntax"));
assert!(output.contains("▲"));
}
}