1use std::collections::BTreeMap;
22use std::fs;
23use std::io::Stdout;
24use std::path::Path;
25
26use crate::Renderable;
27use crate::r#box::ROUNDED;
28use crate::console::{Console, ConsoleOptions};
29use crate::highlighter::{Highlighter, RegexHighlighter, path_highlighter, repr_highlighter};
30use crate::measure::Measurement;
31use crate::scope::render_scope;
32use crate::segment::{Segment, Segments};
33use crate::style::Style;
34use crate::syntax::Syntax;
35use crate::text::Text;
36
37pub const DEFAULT_MAX_FRAMES: usize = 100;
43
44pub const DEFAULT_EXTRA_LINES: usize = 3;
46
47pub const LOCALS_MAX_LENGTH: usize = 10;
49
50pub const LOCALS_MAX_STRING: usize = 80;
52
53#[derive(Debug, Clone)]
62pub struct Frame {
63 pub filename: String,
65 pub lineno: usize,
67 pub name: String,
69 pub line: String,
71 pub locals: Option<BTreeMap<String, String>>,
74}
75
76impl Frame {
77 pub fn new(filename: impl Into<String>, lineno: usize, name: impl Into<String>) -> Self {
93 Self {
94 filename: filename.into(),
95 lineno,
96 name: name.into(),
97 line: String::new(),
98 locals: None,
99 }
100 }
101
102 pub fn with_line(mut self, line: impl Into<String>) -> Self {
104 self.line = line.into();
105 self
106 }
107
108 pub fn with_locals(mut self, locals: BTreeMap<String, String>) -> Self {
110 self.locals = Some(locals);
111 self
112 }
113
114 pub fn add_local(&mut self, name: impl Into<String>, value: impl Into<String>) {
116 self.locals
117 .get_or_insert_with(BTreeMap::new)
118 .insert(name.into(), value.into());
119 }
120
121 pub fn has_locals(&self) -> bool {
123 self.locals.as_ref().is_some_and(|l| !l.is_empty())
124 }
125}
126
127#[derive(Debug, Clone)]
136pub struct SyntaxErrorInfo {
137 pub offset: usize,
139 pub filename: String,
141 pub line: String,
143 pub lineno: usize,
145 pub msg: String,
147}
148
149impl SyntaxErrorInfo {
150 pub fn new(
159 filename: impl Into<String>,
160 lineno: usize,
161 offset: usize,
162 msg: impl Into<String>,
163 ) -> Self {
164 Self {
165 offset,
166 filename: filename.into(),
167 line: String::new(),
168 lineno,
169 msg: msg.into(),
170 }
171 }
172
173 pub fn with_line(mut self, line: impl Into<String>) -> Self {
175 self.line = line.into();
176 self
177 }
178}
179
180#[derive(Debug, Clone)]
189pub struct Stack {
190 pub exc_type: String,
192 pub exc_value: String,
194 pub syntax_error: Option<SyntaxErrorInfo>,
196 pub is_cause: bool,
198 pub frames: Vec<Frame>,
200}
201
202impl Stack {
203 pub fn new(exc_type: impl Into<String>, exc_value: impl Into<String>) -> Self {
218 Self {
219 exc_type: exc_type.into(),
220 exc_value: exc_value.into(),
221 syntax_error: None,
222 is_cause: false,
223 frames: Vec::new(),
224 }
225 }
226
227 pub fn with_syntax_error(mut self, error: SyntaxErrorInfo) -> Self {
229 self.syntax_error = Some(error);
230 self
231 }
232
233 pub fn with_is_cause(mut self, is_cause: bool) -> Self {
235 self.is_cause = is_cause;
236 self
237 }
238
239 pub fn with_frames(mut self, frames: Vec<Frame>) -> Self {
241 self.frames = frames;
242 self
243 }
244
245 pub fn with_frame(mut self, frame: Frame) -> Self {
247 self.frames.push(frame);
248 self
249 }
250
251 pub fn add_frame(&mut self, frame: Frame) {
253 self.frames.push(frame);
254 }
255
256 pub fn is_syntax_error(&self) -> bool {
258 self.syntax_error.is_some()
259 }
260
261 pub fn frame_count(&self) -> usize {
263 self.frames.len()
264 }
265}
266
267#[derive(Debug, Clone)]
276pub struct Trace {
277 pub stacks: Vec<Stack>,
279}
280
281impl Trace {
282 pub fn new(stacks: Vec<Stack>) -> Self {
297 Self { stacks }
298 }
299
300 pub fn empty() -> Self {
302 Self { stacks: Vec::new() }
303 }
304
305 pub fn with_stack(mut self, stack: Stack) -> Self {
307 self.stacks.push(stack);
308 self
309 }
310
311 pub fn add_stack(&mut self, stack: Stack) {
313 self.stacks.push(stack);
314 }
315
316 pub fn stack_count(&self) -> usize {
318 self.stacks.len()
319 }
320
321 pub fn is_empty(&self) -> bool {
323 self.stacks.is_empty()
324 }
325}
326
327#[derive(Debug, Clone)]
333pub struct TracebackBuilder {
334 trace: Trace,
335 width: Option<usize>,
336 extra_lines: usize,
337 theme: Option<String>,
338 word_wrap: bool,
339 show_locals: bool,
340 locals_max_length: Option<usize>,
341 locals_max_string: Option<usize>,
342 locals_hide_dunder: bool,
343 locals_hide_sunder: bool,
344 indent_guides: bool,
345 suppress: Vec<String>,
346 max_frames: usize,
347}
348
349impl TracebackBuilder {
350 pub fn new(trace: Trace) -> Self {
352 Self {
353 trace,
354 width: None,
355 extra_lines: DEFAULT_EXTRA_LINES,
356 theme: None,
357 word_wrap: false,
358 show_locals: false,
359 locals_max_length: Some(LOCALS_MAX_LENGTH),
360 locals_max_string: Some(LOCALS_MAX_STRING),
361 locals_hide_dunder: true,
362 locals_hide_sunder: false,
363 indent_guides: true,
364 suppress: Vec::new(),
365 max_frames: DEFAULT_MAX_FRAMES,
366 }
367 }
368
369 pub fn width(mut self, width: usize) -> Self {
371 self.width = Some(width);
372 self
373 }
374
375 pub fn extra_lines(mut self, lines: usize) -> Self {
377 self.extra_lines = lines;
378 self
379 }
380
381 pub fn theme(mut self, theme: impl Into<String>) -> Self {
383 self.theme = Some(theme.into());
384 self
385 }
386
387 pub fn word_wrap(mut self, wrap: bool) -> Self {
389 self.word_wrap = wrap;
390 self
391 }
392
393 pub fn show_locals(mut self, show: bool) -> Self {
395 self.show_locals = show;
396 self
397 }
398
399 pub fn locals_max_length(mut self, max: Option<usize>) -> Self {
401 self.locals_max_length = max;
402 self
403 }
404
405 pub fn locals_max_string(mut self, max: Option<usize>) -> Self {
407 self.locals_max_string = max;
408 self
409 }
410
411 pub fn locals_hide_dunder(mut self, hide: bool) -> Self {
413 self.locals_hide_dunder = hide;
414 self
415 }
416
417 pub fn locals_hide_sunder(mut self, hide: bool) -> Self {
419 self.locals_hide_sunder = hide;
420 self
421 }
422
423 pub fn indent_guides(mut self, guides: bool) -> Self {
425 self.indent_guides = guides;
426 self
427 }
428
429 pub fn suppress(mut self, path: impl Into<String>) -> Self {
431 self.suppress.push(path.into());
432 self
433 }
434
435 pub fn suppress_all(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
437 self.suppress.extend(paths.into_iter().map(Into::into));
438 self
439 }
440
441 pub fn max_frames(mut self, max: usize) -> Self {
443 self.max_frames = if max > 0 { max.max(4) } else { 0 };
444 self
445 }
446
447 pub fn build(self) -> Traceback {
449 Traceback {
450 trace: self.trace,
451 width: self.width,
452 extra_lines: self.extra_lines,
453 theme: self.theme,
454 word_wrap: self.word_wrap,
455 show_locals: self.show_locals,
456 locals_max_length: self.locals_max_length,
457 locals_max_string: self.locals_max_string,
458 locals_hide_dunder: self.locals_hide_dunder,
459 locals_hide_sunder: self.locals_hide_sunder,
460 indent_guides: self.indent_guides,
461 suppress: self.suppress,
462 max_frames: self.max_frames,
463 }
464 }
465}
466
467#[derive(Debug, Clone)]
500pub struct Traceback {
501 pub trace: Trace,
503 pub width: Option<usize>,
505 pub extra_lines: usize,
507 pub theme: Option<String>,
509 pub word_wrap: bool,
511 pub show_locals: bool,
513 pub locals_max_length: Option<usize>,
515 pub locals_max_string: Option<usize>,
517 pub locals_hide_dunder: bool,
519 pub locals_hide_sunder: bool,
521 pub indent_guides: bool,
523 pub suppress: Vec<String>,
525 pub max_frames: usize,
527}
528
529impl Traceback {
530 pub fn new(trace: Trace) -> Self {
545 Self {
546 trace,
547 width: None,
548 extra_lines: DEFAULT_EXTRA_LINES,
549 theme: None,
550 word_wrap: false,
551 show_locals: false,
552 locals_max_length: Some(LOCALS_MAX_LENGTH),
553 locals_max_string: Some(LOCALS_MAX_STRING),
554 locals_hide_dunder: true,
555 locals_hide_sunder: false,
556 indent_guides: true,
557 suppress: Vec::new(),
558 max_frames: DEFAULT_MAX_FRAMES,
559 }
560 }
561
562 pub fn builder(trace: Trace) -> TracebackBuilder {
580 TracebackBuilder::new(trace)
581 }
582
583 pub fn trace(&self) -> &Trace {
585 &self.trace
586 }
587
588 pub fn should_show_locals(&self) -> bool {
590 self.show_locals
591 }
592
593 pub fn filter_locals(&self, locals: &BTreeMap<String, String>) -> BTreeMap<String, String> {
597 locals
598 .iter()
599 .filter(|(name, _)| {
600 if self.locals_hide_dunder && name.starts_with("__") && name.ends_with("__") {
602 return false;
603 }
604 if self.locals_hide_sunder && name.starts_with('_') && !name.starts_with("__") {
606 return false;
607 }
608 true
609 })
610 .map(|(k, v)| (k.clone(), v.clone()))
611 .collect()
612 }
613
614 pub fn is_suppressed(&self, path: &str) -> bool {
616 self.suppress.iter().any(|s| path.contains(s))
617 }
618}
619
620fn traceback_title_style() -> Style {
626 Style::new()
627 .with_color(crate::color::SimpleColor::Standard(9)) .with_bold(true)
629}
630
631fn traceback_border_style() -> Style {
633 Style::new().with_color(crate::color::SimpleColor::Standard(1)) }
635
636fn exc_type_style() -> Style {
638 Style::new()
639 .with_color(crate::color::SimpleColor::Standard(1)) .with_bold(true)
641}
642
643fn path_style() -> Style {
645 Style::new().with_color(crate::color::SimpleColor::Standard(5)) }
647
648fn lineno_style() -> Style {
650 Style::new().with_color(crate::color::SimpleColor::Standard(6)) }
652
653fn function_style() -> Style {
655 Style::new().with_color(crate::color::SimpleColor::Standard(2)) }
657
658fn frames_hidden_style() -> Style {
660 Style::new()
661 .with_color(crate::color::SimpleColor::Standard(3)) .with_italic(true)
663}
664
665fn syntax_error_offset_style() -> Style {
667 Style::new()
668 .with_color(crate::color::SimpleColor::Standard(1)) .with_bold(true)
670}
671
672impl Renderable for Traceback {
677 fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
678 let mut result = Segments::new();
679
680 let width = self
682 .width
683 .unwrap_or(options.max_width)
684 .min(options.max_width);
685 let code_width = width.saturating_sub(8); let stacks: Vec<&Stack> = self.trace.stacks.iter().rev().collect();
689
690 for (idx, stack) in stacks.iter().enumerate() {
691 let is_last = idx == stacks.len() - 1;
692
693 if !stack.frames.is_empty() {
695 let stack_segments = self.render_stack_frames(stack, console, options, code_width);
697
698 let mut title = Text::styled("Traceback ", traceback_title_style());
704 title.append(
705 "(most recent call last)",
706 Some(traceback_title_style().with_dim(true)),
707 );
708
709 let border_style = traceback_border_style();
711 let box_chars = &ROUNDED;
712
713 let title_str = title.plain_text();
715 let title_len = crate::cells::cell_len(&title_str);
716 let inner_width = width.saturating_sub(2);
717 let title_pad = inner_width.saturating_sub(title_len).saturating_sub(2);
718 let left_pad = title_pad / 2;
719 let right_pad = title_pad - left_pad;
720
721 result.push(Segment::styled(
723 box_chars.top_left.to_string(),
724 border_style,
725 ));
726 result.push(Segment::styled(
727 box_chars.top.to_string().repeat(left_pad + 1),
728 border_style,
729 ));
730 result.extend(title.render(console, options));
731 result.push(Segment::styled(
732 box_chars.top.to_string().repeat(right_pad + 1),
733 border_style,
734 ));
735 result.push(Segment::styled(
736 box_chars.top_right.to_string(),
737 border_style,
738 ));
739 result.push(Segment::line());
740
741 let segment_vec: Vec<Segment> = stack_segments.iter().cloned().collect();
743 let content_lines = Segment::split_lines(segment_vec);
744 for line_segments in content_lines {
745 result.push(Segment::styled(
746 format!("{} ", box_chars.mid_left),
747 border_style,
748 ));
749 result.extend(line_segments.into_iter().map(|s| Segment::from(s)));
750 let line_width: usize = result
752 .iter()
753 .skip(result.len().saturating_sub(10))
754 .map(|s| crate::cells::cell_len(&s.text))
755 .sum();
756 let padding = inner_width.saturating_sub(line_width.saturating_sub(2));
757 if padding > 0 {
758 result.push(Segment::new(" ".repeat(padding)));
759 }
760 result.push(Segment::styled(
761 format!(" {}", box_chars.mid_right),
762 border_style,
763 ));
764 result.push(Segment::line());
765 }
766
767 result.push(Segment::styled(
769 box_chars.bottom_left.to_string(),
770 border_style,
771 ));
772 result.push(Segment::styled(
773 box_chars.bottom.to_string().repeat(inner_width),
774 border_style,
775 ));
776 result.push(Segment::styled(
777 box_chars.bottom_right.to_string(),
778 border_style,
779 ));
780 result.push(Segment::line());
781 }
782
783 if let Some(ref syntax_error) = stack.syntax_error {
785 let syntax_error_segments =
786 self.render_syntax_error_content(syntax_error, console, options);
787 result.extend(syntax_error_segments);
788 result.push(Segment::line());
789 }
790
791 let exc_text = self.render_exception_line(stack);
793 let exc_segments = exc_text.render(console, options);
794 result.extend(exc_segments);
795 result.push(Segment::line());
796
797 if !is_last {
799 let chaining_msg = if stack.is_cause {
800 "\nThe above exception was the direct cause of the following exception:\n"
801 } else {
802 "\nDuring handling of the above exception, another exception occurred:\n"
803 };
804
805 let chaining_text = Text::styled(chaining_msg, Style::new().with_italic(true));
806 let chaining_segments = chaining_text.render(console, options);
807 result.extend(chaining_segments);
808 result.push(Segment::line());
809 }
810 }
811
812 result
813 }
814
815 fn measure(&self, _console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
816 let width = self
818 .width
819 .unwrap_or(options.max_width)
820 .min(options.max_width);
821 Measurement::new(width, width)
822 }
823}
824
825impl Traceback {
826 fn render_stack_frames(
828 &self,
829 stack: &Stack,
830 console: &Console<Stdout>,
831 options: &ConsoleOptions,
832 code_width: usize,
833 ) -> Segments {
834 let mut result = Segments::new();
835 let highlighter = path_highlighter();
836 let frames = &stack.frames;
837
838 let exclude_range = if self.max_frames > 0 && frames.len() > self.max_frames {
840 let half = self.max_frames / 2;
841 Some(half..(frames.len() - half))
842 } else {
843 None
844 };
845
846 let mut excluded_shown = false;
847
848 for (idx, frame) in frames.iter().enumerate() {
849 if let Some(ref range) = exclude_range {
851 if range.contains(&idx) {
852 if !excluded_shown {
854 let msg = format!("\n... {} frames hidden ...\n", range.len());
855 let hidden_text = Text::styled(&msg, frames_hidden_style());
856 result.extend(hidden_text.render(console, options));
857 excluded_shown = true;
858 }
859 continue;
860 }
861 }
862
863 let suppressed = self.is_suppressed(&frame.filename);
865
866 if idx > 0 && !frame.filename.starts_with('<') {
868 result.push(Segment::line());
869 }
870
871 let location_text = self.render_frame_location(frame, &highlighter);
873 result.extend(location_text.render(console, options));
874 result.push(Segment::line());
875
876 if frame.filename.starts_with('<') || suppressed {
878 if self.show_locals && frame.has_locals() {
880 self.render_locals(&mut result, frame, console, options);
881 }
882 continue;
883 }
884
885 if let Some(code) = self.read_source_code(&frame.filename) {
887 let lexer = self.guess_lexer(&frame.filename);
888
889 let start_line = frame.lineno.saturating_sub(self.extra_lines);
891 let end_line = frame.lineno + self.extra_lines;
892
893 let mut syntax = Syntax::new(&code, lexer)
895 .with_line_numbers(true)
896 .with_line_range(Some(start_line), Some(end_line))
897 .with_highlight_lines(vec![frame.lineno])
898 .with_indent_guides(self.indent_guides)
899 .with_word_wrap(self.word_wrap);
900
901 if let Some(ref theme) = self.theme {
902 syntax = syntax.with_theme(theme);
903 }
904
905 if code_width > 0 {
906 syntax = syntax.with_code_width(code_width);
907 }
908
909 result.extend(syntax.render(console, options));
910 }
911
912 if self.show_locals && frame.has_locals() {
914 result.push(Segment::line());
915 self.render_locals(&mut result, frame, console, options);
916 }
917 }
918
919 result
920 }
921
922 fn render_frame_location(&self, frame: &Frame, highlighter: &RegexHighlighter) -> Text {
924 let path = Path::new(&frame.filename);
925
926 if path.exists() {
927 let mut text = Text::new();
929
930 let mut path_text = Text::styled(&frame.filename, path_style());
932 highlighter.highlight(&mut path_text);
933 text.append_text(&path_text);
934
935 text.append(":", None);
937 text.append(&frame.lineno.to_string(), Some(lineno_style()));
938
939 text.append(" in ", None);
941 text.append(&frame.name, Some(function_style()));
942
943 text
944 } else {
945 let mut text = Text::new();
947 text.append("in ", None);
948 text.append(&frame.name, Some(function_style()));
949 text.append(":", None);
950 text.append(&frame.lineno.to_string(), Some(lineno_style()));
951 text
952 }
953 }
954
955 fn render_locals(
957 &self,
958 result: &mut Segments,
959 frame: &Frame,
960 console: &Console<Stdout>,
961 options: &ConsoleOptions,
962 ) {
963 if let Some(ref locals) = frame.locals {
964 let filtered = self.filter_locals(locals);
966
967 if !filtered.is_empty() {
968 let scope = render_scope(
969 &filtered,
970 Some("locals"),
971 true, self.indent_guides,
973 self.locals_max_length,
974 self.locals_max_string,
975 );
976 result.extend(scope.render(console, options));
977 }
978 }
979 }
980
981 fn render_syntax_error_content(
983 &self,
984 error: &SyntaxErrorInfo,
985 console: &Console<Stdout>,
986 options: &ConsoleOptions,
987 ) -> Segments {
988 let mut result = Segments::new();
989
990 let highlighter = path_highlighter();
991 let repr_hl = repr_highlighter();
992
993 if error.filename != "<stdin>" {
995 let path = Path::new(&error.filename);
996 if path.exists() {
997 let mut location = Text::new();
998 location.append(" ", None);
999
1000 let mut path_text = Text::styled(&error.filename, path_style());
1001 highlighter.highlight(&mut path_text);
1002 location.append_text(&path_text);
1003
1004 location.append(":", None);
1005 location.append(&error.lineno.to_string(), Some(lineno_style()));
1006
1007 result.extend(location.render(console, options));
1008 result.push(Segment::line());
1009 }
1010 }
1011
1012 let mut error_line = Text::plain(error.line.trim_end());
1014 repr_hl.highlight(&mut error_line);
1015
1016 let offset = error
1018 .offset
1019 .saturating_sub(1)
1020 .min(error_line.plain_text().len());
1021 if offset < error_line.plain_text().len() {
1022 error_line.stylize(
1023 offset,
1024 offset + 1,
1025 Style::new().with_bold(true).with_underline(true),
1026 );
1027 }
1028
1029 result.extend(error_line.render(console, options));
1030
1031 let indicator = format!("\n{}▲", " ".repeat(offset));
1033 let indicator_text = Text::styled(&indicator, syntax_error_offset_style());
1034 result.extend(indicator_text.render(console, options));
1035
1036 result
1037 }
1038
1039 fn render_exception_line(&self, stack: &Stack) -> Text {
1041 let highlighter = repr_highlighter();
1042
1043 let mut text = Text::new();
1044
1045 text.append(&format!("{}: ", stack.exc_type), Some(exc_type_style()));
1047
1048 let mut value_text = Text::plain(&stack.exc_value);
1050 highlighter.highlight(&mut value_text);
1051 text.append_text(&value_text);
1052
1053 text
1054 }
1055
1056 fn read_source_code(&self, filename: &str) -> Option<String> {
1058 let path = Path::new(filename);
1059 if path.exists() {
1060 fs::read_to_string(path).ok()
1061 } else {
1062 None
1063 }
1064 }
1065
1066 fn guess_lexer(&self, filename: &str) -> &'static str {
1068 let ext = Path::new(filename)
1069 .extension()
1070 .and_then(|e| e.to_str())
1071 .unwrap_or("");
1072
1073 match ext {
1074 "rs" => "rust",
1075 "py" => "python",
1076 "js" | "mjs" => "javascript",
1077 "ts" | "mts" => "typescript",
1078 "tsx" => "tsx",
1079 "jsx" => "jsx",
1080 "c" | "h" => "c",
1081 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
1082 "go" => "go",
1083 "java" => "java",
1084 "rb" => "ruby",
1085 "sh" | "bash" => "bash",
1086 "json" => "json",
1087 "toml" => "toml",
1088 "yaml" | "yml" => "yaml",
1089 "md" | "markdown" => "markdown",
1090 "html" | "htm" => "html",
1091 "css" => "css",
1092 "sql" => "sql",
1093 "xml" => "xml",
1094 _ => "text",
1095 }
1096 }
1097}
1098
1099pub fn install() {
1119 install_with_options(TracebackBuilder::new(Trace::empty()));
1120}
1121
1122pub fn install_with_options(builder: TracebackBuilder) {
1136 use crate::ColorSystem;
1137 use std::io::{self, Write};
1138
1139 let config = builder.build();
1140
1141 std::panic::set_hook(Box::new(move |panic_info| {
1142 let console = Console::new();
1144 let options = console.options();
1145
1146 let location = panic_info.location();
1148 let (filename, lineno, _col) = location
1149 .map(|l| (l.file().to_string(), l.line() as usize, l.column() as usize))
1150 .unwrap_or_else(|| ("<unknown>".to_string(), 0, 0));
1151
1152 let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
1154 s.to_string()
1155 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
1156 s.clone()
1157 } else {
1158 "Box<dyn Any>".to_string()
1159 };
1160
1161 let frame = Frame::new(&filename, lineno, "panic");
1163 let stack = Stack::new("panic", &message).with_frame(frame);
1164 let trace = Trace::new(vec![stack]);
1165
1166 let traceback = Traceback {
1168 trace,
1169 width: config.width,
1170 extra_lines: config.extra_lines,
1171 theme: config.theme.clone(),
1172 word_wrap: config.word_wrap,
1173 show_locals: false, locals_max_length: config.locals_max_length,
1175 locals_max_string: config.locals_max_string,
1176 locals_hide_dunder: config.locals_hide_dunder,
1177 locals_hide_sunder: config.locals_hide_sunder,
1178 indent_guides: config.indent_guides,
1179 suppress: config.suppress.clone(),
1180 max_frames: config.max_frames,
1181 };
1182
1183 let segments = traceback.render(&console, &options);
1185
1186 let color_system = ColorSystem::TrueColor;
1188
1189 let stderr = io::stderr();
1191 let mut handle = stderr.lock();
1192 for segment in segments.iter() {
1193 if segment.is_control() {
1194 continue;
1195 }
1196 if let Some(style) = segment.style {
1197 let _ = write!(handle, "{}", style.render(&segment.text, color_system));
1198 } else {
1199 let _ = write!(handle, "{}", segment.text);
1200 }
1201 }
1202 let _ = writeln!(handle);
1203 let _ = handle.flush();
1204 }));
1205}
1206
1207#[cfg(test)]
1212mod tests {
1213 use super::*;
1214
1215 #[test]
1218 fn test_frame_new() {
1219 let frame = Frame::new("main.rs", 42, "main");
1220 assert_eq!(frame.filename, "main.rs");
1221 assert_eq!(frame.lineno, 42);
1222 assert_eq!(frame.name, "main");
1223 assert!(frame.line.is_empty());
1224 assert!(frame.locals.is_none());
1225 }
1226
1227 #[test]
1228 fn test_frame_with_line() {
1229 let frame = Frame::new("test.rs", 10, "test").with_line(" let x = 42;");
1230 assert_eq!(frame.line, " let x = 42;");
1231 }
1232
1233 #[test]
1234 fn test_frame_with_locals() {
1235 let mut locals = BTreeMap::new();
1236 locals.insert("x".to_string(), "42".to_string());
1237
1238 let frame = Frame::new("test.rs", 10, "test").with_locals(locals);
1239 assert!(frame.has_locals());
1240 assert_eq!(frame.locals.unwrap().get("x"), Some(&"42".to_string()));
1241 }
1242
1243 #[test]
1244 fn test_frame_add_local() {
1245 let mut frame = Frame::new("test.rs", 10, "test");
1246 frame.add_local("x", "42");
1247 frame.add_local("y", "100");
1248
1249 assert!(frame.has_locals());
1250 let locals = frame.locals.unwrap();
1251 assert_eq!(locals.len(), 2);
1252 }
1253
1254 #[test]
1257 fn test_syntax_error_info_new() {
1258 let info = SyntaxErrorInfo::new("test.rs", 5, 10, "unexpected token");
1259 assert_eq!(info.filename, "test.rs");
1260 assert_eq!(info.lineno, 5);
1261 assert_eq!(info.offset, 10);
1262 assert_eq!(info.msg, "unexpected token");
1263 }
1264
1265 #[test]
1266 fn test_syntax_error_info_with_line() {
1267 let info = SyntaxErrorInfo::new("test.rs", 5, 10, "error").with_line("let x = ;");
1268 assert_eq!(info.line, "let x = ;");
1269 }
1270
1271 #[test]
1274 fn test_stack_new() {
1275 let stack = Stack::new("ValueError", "invalid input");
1276 assert_eq!(stack.exc_type, "ValueError");
1277 assert_eq!(stack.exc_value, "invalid input");
1278 assert!(!stack.is_cause);
1279 assert!(stack.frames.is_empty());
1280 assert!(!stack.is_syntax_error());
1281 }
1282
1283 #[test]
1284 fn test_stack_with_frame() {
1285 let frame = Frame::new("test.rs", 10, "test");
1286 let stack = Stack::new("Error", "msg").with_frame(frame);
1287 assert_eq!(stack.frame_count(), 1);
1288 }
1289
1290 #[test]
1291 fn test_stack_with_frames() {
1292 let frames = vec![Frame::new("a.rs", 1, "a"), Frame::new("b.rs", 2, "b")];
1293 let stack = Stack::new("Error", "msg").with_frames(frames);
1294 assert_eq!(stack.frame_count(), 2);
1295 }
1296
1297 #[test]
1298 fn test_stack_add_frame() {
1299 let mut stack = Stack::new("Error", "msg");
1300 stack.add_frame(Frame::new("a.rs", 1, "a"));
1301 stack.add_frame(Frame::new("b.rs", 2, "b"));
1302 assert_eq!(stack.frame_count(), 2);
1303 }
1304
1305 #[test]
1306 fn test_stack_with_syntax_error() {
1307 let syntax_err = SyntaxErrorInfo::new("test.rs", 5, 10, "error");
1308 let stack = Stack::new("SyntaxError", "msg").with_syntax_error(syntax_err);
1309 assert!(stack.is_syntax_error());
1310 }
1311
1312 #[test]
1313 fn test_stack_is_cause() {
1314 let stack = Stack::new("Error", "caused by").with_is_cause(true);
1315 assert!(stack.is_cause);
1316 }
1317
1318 #[test]
1321 fn test_trace_new() {
1322 let stacks = vec![Stack::new("Error", "msg")];
1323 let trace = Trace::new(stacks);
1324 assert_eq!(trace.stack_count(), 1);
1325 assert!(!trace.is_empty());
1326 }
1327
1328 #[test]
1329 fn test_trace_empty() {
1330 let trace = Trace::empty();
1331 assert!(trace.is_empty());
1332 assert_eq!(trace.stack_count(), 0);
1333 }
1334
1335 #[test]
1336 fn test_trace_with_stack() {
1337 let trace = Trace::empty()
1338 .with_stack(Stack::new("E1", "m1"))
1339 .with_stack(Stack::new("E2", "m2"));
1340 assert_eq!(trace.stack_count(), 2);
1341 }
1342
1343 #[test]
1344 fn test_trace_add_stack() {
1345 let mut trace = Trace::empty();
1346 trace.add_stack(Stack::new("E1", "m1"));
1347 trace.add_stack(Stack::new("E2", "m2"));
1348 assert_eq!(trace.stack_count(), 2);
1349 }
1350
1351 #[test]
1354 fn test_traceback_new() {
1355 let trace = Trace::new(vec![Stack::new("Error", "msg")]);
1356 let tb = Traceback::new(trace);
1357
1358 assert!(tb.width.is_none());
1359 assert_eq!(tb.extra_lines, DEFAULT_EXTRA_LINES);
1360 assert!(tb.theme.is_none());
1361 assert!(!tb.word_wrap);
1362 assert!(!tb.show_locals);
1363 assert!(tb.locals_hide_dunder);
1364 assert!(!tb.locals_hide_sunder);
1365 assert!(tb.indent_guides);
1366 assert!(tb.suppress.is_empty());
1367 assert_eq!(tb.max_frames, DEFAULT_MAX_FRAMES);
1368 }
1369
1370 #[test]
1371 fn test_traceback_builder() {
1372 let trace = Trace::new(vec![Stack::new("Error", "msg")]);
1373 let tb = Traceback::builder(trace)
1374 .width(100)
1375 .extra_lines(5)
1376 .theme("monokai")
1377 .word_wrap(true)
1378 .show_locals(true)
1379 .locals_max_length(Some(20))
1380 .locals_max_string(Some(100))
1381 .locals_hide_dunder(false)
1382 .locals_hide_sunder(true)
1383 .indent_guides(false)
1384 .suppress("/usr/lib")
1385 .suppress_all(vec!["site-packages"])
1386 .max_frames(50)
1387 .build();
1388
1389 assert_eq!(tb.width, Some(100));
1390 assert_eq!(tb.extra_lines, 5);
1391 assert_eq!(tb.theme, Some("monokai".to_string()));
1392 assert!(tb.word_wrap);
1393 assert!(tb.show_locals);
1394 assert_eq!(tb.locals_max_length, Some(20));
1395 assert_eq!(tb.locals_max_string, Some(100));
1396 assert!(!tb.locals_hide_dunder);
1397 assert!(tb.locals_hide_sunder);
1398 assert!(!tb.indent_guides);
1399 assert_eq!(tb.suppress.len(), 2);
1400 assert_eq!(tb.max_frames, 50);
1401 }
1402
1403 #[test]
1404 fn test_traceback_max_frames_minimum() {
1405 let trace = Trace::empty();
1406 let tb = Traceback::builder(trace).max_frames(2).build();
1408 assert_eq!(tb.max_frames, 4);
1409 }
1410
1411 #[test]
1412 fn test_traceback_max_frames_zero() {
1413 let trace = Trace::empty();
1414 let tb = Traceback::builder(trace).max_frames(0).build();
1416 assert_eq!(tb.max_frames, 0);
1417 }
1418
1419 #[test]
1420 fn test_traceback_filter_locals() {
1421 let trace = Trace::empty();
1422 let tb = Traceback::builder(trace)
1423 .locals_hide_dunder(true)
1424 .locals_hide_sunder(true)
1425 .build();
1426
1427 let mut locals = BTreeMap::new();
1428 locals.insert("x".to_string(), "1".to_string());
1429 locals.insert("_private".to_string(), "2".to_string());
1430 locals.insert("__dunder__".to_string(), "3".to_string());
1431 locals.insert("normal_var".to_string(), "4".to_string());
1432
1433 let filtered = tb.filter_locals(&locals);
1434 assert!(filtered.contains_key("x"));
1435 assert!(filtered.contains_key("normal_var"));
1436 assert!(!filtered.contains_key("_private")); assert!(!filtered.contains_key("__dunder__")); }
1439
1440 #[test]
1441 fn test_traceback_filter_locals_show_all() {
1442 let trace = Trace::empty();
1443 let tb = Traceback::builder(trace)
1444 .locals_hide_dunder(false)
1445 .locals_hide_sunder(false)
1446 .build();
1447
1448 let mut locals = BTreeMap::new();
1449 locals.insert("x".to_string(), "1".to_string());
1450 locals.insert("_private".to_string(), "2".to_string());
1451 locals.insert("__dunder__".to_string(), "3".to_string());
1452
1453 let filtered = tb.filter_locals(&locals);
1454 assert_eq!(filtered.len(), 3);
1455 }
1456
1457 #[test]
1458 fn test_traceback_is_suppressed() {
1459 let trace = Trace::empty();
1460 let tb = Traceback::builder(trace)
1461 .suppress("/usr/lib/python")
1462 .suppress("site-packages")
1463 .build();
1464
1465 assert!(tb.is_suppressed("/usr/lib/python/foo.py"));
1466 assert!(tb.is_suppressed("/home/user/.local/lib/site-packages/bar.py"));
1467 assert!(!tb.is_suppressed("/home/user/project/main.py"));
1468 }
1469
1470 #[test]
1471 fn test_traceback_should_show_locals() {
1472 let trace = Trace::empty();
1473 let tb1 = Traceback::new(trace.clone());
1474 let tb2 = Traceback::builder(trace).show_locals(true).build();
1475
1476 assert!(!tb1.should_show_locals());
1477 assert!(tb2.should_show_locals());
1478 }
1479
1480 #[test]
1483 fn test_frame_is_send_sync() {
1484 fn assert_send<T: Send>() {}
1485 fn assert_sync<T: Sync>() {}
1486 assert_send::<Frame>();
1487 assert_sync::<Frame>();
1488 }
1489
1490 #[test]
1491 fn test_stack_is_send_sync() {
1492 fn assert_send<T: Send>() {}
1493 fn assert_sync<T: Sync>() {}
1494 assert_send::<Stack>();
1495 assert_sync::<Stack>();
1496 }
1497
1498 #[test]
1499 fn test_trace_is_send_sync() {
1500 fn assert_send<T: Send>() {}
1501 fn assert_sync<T: Sync>() {}
1502 assert_send::<Trace>();
1503 assert_sync::<Trace>();
1504 }
1505
1506 #[test]
1507 fn test_traceback_is_send_sync() {
1508 fn assert_send<T: Send>() {}
1509 fn assert_sync<T: Sync>() {}
1510 assert_send::<Traceback>();
1511 assert_sync::<Traceback>();
1512 }
1513
1514 #[test]
1517 fn test_traceback_render_basic() {
1518 use crate::{Console, Renderable};
1519
1520 let frame = Frame::new("test.rs", 42, "test_function").with_line(" let x = 42;");
1521 let stack = Stack::new("RuntimeError", "Something went wrong").with_frame(frame);
1522 let trace = Trace::new(vec![stack]);
1523 let tb = Traceback::new(trace);
1524
1525 let console = Console::new();
1526 let options = console.options();
1527 let segments = tb.render(&console, &options);
1528
1529 assert!(!segments.is_empty());
1531
1532 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1534 assert!(output.contains("RuntimeError"));
1535 assert!(output.contains("Something went wrong"));
1536 assert!(output.contains("Traceback"));
1537 }
1538
1539 #[test]
1540 fn test_traceback_render_with_chaining() {
1541 use crate::{Console, Renderable};
1542
1543 let frame1 = Frame::new("inner.rs", 10, "inner_fn");
1546 let stack1 = Stack::new("ValueError", "inner error").with_frame(frame1);
1547
1548 let frame2 = Frame::new("outer.rs", 20, "outer_fn");
1549 let stack2 = Stack::new("RuntimeError", "outer error")
1550 .with_frame(frame2)
1551 .with_is_cause(true); let trace = Trace::new(vec![stack1, stack2]);
1554 let tb = Traceback::new(trace);
1555
1556 let console = Console::new();
1557 let options = console.options();
1558 let segments = tb.render(&console, &options);
1559
1560 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1561
1562 assert!(output.contains("ValueError"));
1564 assert!(output.contains("RuntimeError"));
1565 assert!(output.contains("above exception") || output.contains("another exception"));
1567 }
1568
1569 #[test]
1570 fn test_traceback_render_empty() {
1571 use crate::{Console, Renderable};
1572
1573 let trace = Trace::empty();
1574 let tb = Traceback::new(trace);
1575
1576 let console = Console::new();
1577 let options = console.options();
1578 let segments = tb.render(&console, &options);
1579
1580 assert!(segments.is_empty());
1582 }
1583
1584 #[test]
1585 fn test_traceback_render_syntax_error() {
1586 use crate::{Console, Renderable};
1587
1588 let syntax_err =
1589 SyntaxErrorInfo::new("test.py", 5, 10, "unexpected token").with_line("def foo(:");
1590
1591 let stack = Stack::new("SyntaxError", "invalid syntax").with_syntax_error(syntax_err);
1592 let trace = Trace::new(vec![stack]);
1593 let tb = Traceback::new(trace);
1594
1595 let console = Console::new();
1596 let options = console.options();
1597 let segments = tb.render(&console, &options);
1598
1599 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1600
1601 assert!(output.contains("SyntaxError"));
1603 assert!(output.contains("invalid syntax"));
1604 assert!(output.contains("▲"));
1606 }
1607}