1use std::collections::HashMap;
20use std::fs;
21use std::path::Path;
22
23use unicode_width::UnicodeWidthStr;
24
25use crate::console::{ConsoleOptions, RenderResult, Renderable};
26use crate::segment::Segment;
27use crate::style::Style;
28use crate::theme;
29
30#[derive(Debug, Clone)]
36pub struct Frame {
37 pub filename: String,
38 pub lineno: usize,
39 pub name: String,
40 pub line: Option<String>,
41 pub locals: Option<HashMap<String, String>>,
42 pub last_instruction: Option<String>,
43}
44
45impl Frame {
46 pub fn new(filename: impl Into<String>, lineno: usize, name: impl Into<String>) -> Self {
47 Self {
48 filename: filename.into(),
49 lineno,
50 name: name.into(),
51 line: None,
52 locals: None,
53 last_instruction: None,
54 }
55 }
56
57 pub fn line(mut self, line: impl Into<String>) -> Self {
59 self.line = Some(line.into());
60 self
61 }
62
63 pub fn locals(mut self, locals: HashMap<String, String>) -> Self {
65 self.locals = Some(locals);
66 self
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct Stack {
73 pub exc_type: Option<String>,
74 pub exc_value: Option<String>,
75 pub syntax_error: Option<String>,
76 pub is_cause: bool,
77 pub frames: Vec<Frame>,
78 pub notes: Vec<String>,
79 pub is_group: bool,
80 pub exceptions: Vec<Stack>,
81}
82
83impl Stack {
84 pub fn new() -> Self {
85 Self {
86 exc_type: None,
87 exc_value: None,
88 syntax_error: None,
89 is_cause: false,
90 frames: Vec::new(),
91 notes: Vec::new(),
92 is_group: false,
93 exceptions: Vec::new(),
94 }
95 }
96
97 pub fn exc_type(mut self, t: impl Into<String>) -> Self {
99 self.exc_type = Some(t.into());
100 self
101 }
102
103 pub fn exc_value(mut self, v: impl Into<String>) -> Self {
105 self.exc_value = Some(v.into());
106 self
107 }
108
109 pub fn add_frame(mut self, frame: Frame) -> Self {
111 self.frames.push(frame);
112 self
113 }
114}
115
116#[derive(Debug, Clone)]
118pub struct Trace {
119 pub stacks: Vec<Stack>,
120}
121
122impl Trace {
123 pub fn new() -> Self {
124 Self { stacks: Vec::new() }
125 }
126
127 pub fn from_stack(stack: Stack) -> Self {
128 Self { stacks: vec![stack] }
129 }
130}
131
132#[derive(Debug, Clone)]
140pub struct Traceback {
141 trace: Trace,
142 width: Option<usize>,
143 code_width: Option<usize>,
144 extra_lines: usize,
145 theme_name: Option<String>,
146 word_wrap: bool,
147 show_locals: bool,
148 indent_guides: bool,
149 locals_max_length: usize,
150 locals_max_string: usize,
151 locals_max_depth: usize,
152 locals_hide_dunder: bool,
153 locals_hide_sunder: bool,
154 suppress: Vec<String>,
155 max_frames: Option<usize>,
156}
157
158impl Traceback {
159 pub fn new(trace: Trace) -> Self {
161 Self {
162 trace,
163 width: None,
164 code_width: None,
165 extra_lines: 3,
166 theme_name: None,
167 word_wrap: false,
168 show_locals: false,
169 indent_guides: false,
170 locals_max_length: 10,
171 locals_max_string: 80,
172 locals_max_depth: 5,
173 locals_hide_dunder: true,
174 locals_hide_sunder: false,
175 suppress: Vec::new(),
176 max_frames: None,
177 }
178 }
179
180 pub fn from_exception(
183 exc_type: impl Into<String>,
184 exc_value: impl Into<String>,
185 frames: Vec<Frame>,
186 ) -> Self {
187 let mut stack = Stack::new();
188 stack.exc_type = Some(exc_type.into());
189 stack.exc_value = Some(exc_value.into());
190 stack.frames = frames;
191 let trace = Trace::from_stack(stack);
192 Self::new(trace)
193 }
194
195 pub fn width(mut self, width: usize) -> Self {
198 self.width = Some(width);
199 self
200 }
201
202 pub fn code_width(mut self, width: usize) -> Self {
203 self.code_width = Some(width);
204 self
205 }
206
207 pub fn extra_lines(mut self, n: usize) -> Self {
208 self.extra_lines = n;
209 self
210 }
211
212 pub fn theme(mut self, theme: impl Into<String>) -> Self {
213 self.theme_name = Some(theme.into());
214 self
215 }
216
217 pub fn word_wrap(mut self, wrap: bool) -> Self {
218 self.word_wrap = wrap;
219 self
220 }
221
222 pub fn show_locals(mut self, show: bool) -> Self {
223 self.show_locals = show;
224 self
225 }
226
227 pub fn indent_guides(mut self, guides: bool) -> Self {
228 self.indent_guides = guides;
229 self
230 }
231
232 pub fn locals_max_length(mut self, n: usize) -> Self {
233 self.locals_max_length = n;
234 self
235 }
236
237 pub fn locals_max_string(mut self, n: usize) -> Self {
238 self.locals_max_string = n;
239 self
240 }
241
242 pub fn locals_max_depth(mut self, n: usize) -> Self {
243 self.locals_max_depth = n;
244 self
245 }
246
247 pub fn locals_hide_dunder(mut self, hide: bool) -> Self {
248 self.locals_hide_dunder = hide;
249 self
250 }
251
252 pub fn locals_hide_sunder(mut self, hide: bool) -> Self {
253 self.locals_hide_sunder = hide;
254 self
255 }
256
257 pub fn suppress(mut self, suppress: Vec<String>) -> Self {
258 self.suppress = suppress;
259 self
260 }
261
262 pub fn max_frames(mut self, n: usize) -> Self {
263 self.max_frames = Some(n);
264 self
265 }
266}
267
268fn theme_style(name: &str) -> Style {
275 crate::theme::default_theme()
276 .get(name)
277 .cloned()
278 .unwrap_or_default()
279}
280
281fn outer_content_line(content: Vec<Segment>, total_width: usize) -> Vec<Segment> {
287 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
288 let mut line = Vec::new();
289
290 line.push(Segment::styled("│ ".to_string(), border_style.clone()));
292
293 let mut content_w = 0usize;
295 for seg in &content {
296 content_w += seg.cell_length();
297 }
298 line.extend(content);
299
300 let inner_w = total_width.saturating_sub(4); let pad = inner_w.saturating_sub(content_w);
303 if pad > 0 {
304 line.push(Segment::new(" ".repeat(pad)));
305 }
306
307 line.push(Segment::styled(" │".to_string(), border_style));
309 line
310}
311
312fn outer_blank(total_width: usize) -> Vec<Segment> {
314 outer_content_line(Vec::new(), total_width)
315}
316
317fn top_border(total_width: usize) -> Vec<Segment> {
319 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
320 let title_style = theme_style(theme::names::TRACEBACK_TITLE);
321
322 let title = " Traceback (most recent call last) ";
323 let dashes_total = total_width.saturating_sub(title.len() + 4); let left_dashes = dashes_total / 2;
325 let right_dashes = dashes_total - left_dashes;
326
327 let mut segs = Vec::new();
328 segs.push(Segment::styled("╭─".to_string(), border_style.clone()));
329 segs.push(Segment::styled(
330 "─".repeat(left_dashes.saturating_sub(1)),
331 border_style.clone(),
332 ));
333 segs.push(Segment::styled(title.to_string(), title_style));
334 segs.push(Segment::styled(
335 "─".repeat(right_dashes.saturating_sub(1)),
336 border_style.clone(),
337 ));
338 segs.push(Segment::styled("─╮".to_string(), border_style));
339 segs
340}
341
342fn bottom_border(total_width: usize) -> Vec<Segment> {
344 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
345 let dashes = total_width.saturating_sub(2);
346 vec![Segment::styled(
347 format!("╰{}╯", "─".repeat(dashes)),
348 border_style,
349 )]
350}
351
352fn read_source_lines(
354 filename: &str,
355 lineno: usize,
356 extra_lines: usize,
357) -> (usize, Vec<(usize, String)>) {
358 let content = match fs::read_to_string(Path::new(filename)) {
360 Ok(s) => s,
361 Err(_) => return (0, Vec::new()),
362 };
363
364 let all_lines: Vec<&str> = content.lines().collect();
365 if all_lines.is_empty() {
366 return (0, Vec::new());
367 }
368
369 let start = if lineno > extra_lines {
370 lineno - extra_lines
371 } else {
372 1
373 };
374 let end = (lineno + extra_lines).min(all_lines.len());
376
377 let mut result = Vec::new();
378 for i in start..=end {
379 let line_str = all_lines.get(i.saturating_sub(1)).copied().unwrap_or("");
380 result.push((i, line_str.to_string()));
381 }
382
383 (lineno, result)
384}
385
386fn is_suppressed(filename: &str, suppress: &[String]) -> bool {
388 for pattern in suppress {
389 if filename.starts_with(pattern) || filename.contains(pattern) {
390 return true;
391 }
392 }
393 false
394}
395
396impl Renderable for Traceback {
401 fn render(&self, options: &ConsoleOptions) -> RenderResult {
402 let total_width = self.width.unwrap_or(options.max_width.min(120));
403 let content_width = total_width.saturating_sub(4); let border_style = theme_style(theme::names::TRACEBACK_BORDER);
407 let filename_style = theme_style(theme::names::TRACEBACK_FILENAME);
408 let line_no_style = theme_style(theme::names::TRACEBACK_LINE_NO);
409 let error_mark_style = theme_style(theme::names::TRACEBACK_ERROR_MARK);
410 let error_style = theme_style(theme::names::TRACEBACK_ERROR);
411 let locals_header_style = theme_style(theme::names::TRACEBACK_LOCALS_HEADER);
412
413 let mut out_lines: Vec<Vec<Segment>> = Vec::new();
415
416 out_lines.push(top_border(total_width));
418
419 out_lines.push(outer_blank(total_width));
421
422 let mut rendered_count = 0usize;
424 let mut suppressed_count = 0usize;
425
426 for stack in &self.trace.stacks {
428 let frames_iter: Box<dyn Iterator<Item = &Frame>> = if stack.is_cause {
430 Box::new(stack.frames.iter())
432 } else {
433 Box::new(stack.frames.iter())
434 };
435
436 let max_frames = self.max_frames.unwrap_or(usize::MAX);
437
438 for frame in frames_iter {
439 if is_suppressed(&frame.filename, &self.suppress) {
441 suppressed_count += 1;
442 continue;
443 }
444
445 if rendered_count >= max_frames {
447 suppressed_count += 1;
448 continue;
449 }
450 rendered_count += 1;
451
452 {
455 let loc = format!(
456 "{}:{}",
457 frame.filename,
458 frame.lineno
459 );
460 let func = if frame.name.is_empty() {
461 String::new()
462 } else {
463 format!(" in {}", frame.name)
464 };
465
466 let mut header_segs = Vec::new();
467 header_segs.push(Segment::styled(
468 format!(" {}", loc),
469 filename_style.clone(),
470 ));
471 header_segs.push(Segment::styled(func, Style::new()));
472 out_lines.push(outer_content_line(header_segs, total_width));
473 }
474
475 let (error_line_num, source_lines) =
477 read_source_lines(&frame.filename, frame.lineno, self.extra_lines);
478
479 if !source_lines.is_empty() {
480 let indent = 2usize;
482 let sub_box_total = content_width.saturating_sub(indent * 2);
483 let sub_box_inner = sub_box_total.saturating_sub(2); let max_ln = source_lines
488 .iter()
489 .map(|(ln, _)| *ln)
490 .max()
491 .unwrap_or(0);
492 let ln_width = max_ln.to_string().len().max(2);
493
494 let marker_cells = 2;
496
497 let prefix_cells = marker_cells + 1 + ln_width + 3; let code_cells = sub_box_inner.saturating_sub(prefix_cells);
502
503 {
505 let mut segs = Vec::new();
506 segs.push(Segment::styled(
507 format!("{}╭{}╮", " ".repeat(indent), "─".repeat(sub_box_inner)),
508 border_style.clone(),
509 ));
510 out_lines.push(outer_content_line(segs, total_width));
511 }
512
513 for (line_num, line_text) in &source_lines {
515 let is_error = *line_num == error_line_num;
516
517 let marker = if is_error { "❱" } else { " " };
518 let marker_str = format!("{:<width$}", marker, width = marker_cells);
519
520 let ln_str = format!("{:>width$}", line_num, width = ln_width);
521 let code = truncate_to_width(line_text, code_cells);
522
523 let raw_line = format!(
524 "{}{} {} │ {} ",
525 marker_str,
526 " ".repeat(1),
527 ln_str,
528 code,
529 );
530
531 let inner_w = sub_box_inner.saturating_sub(2); let raw_width = UnicodeWidthStr::width(raw_line.as_str());
535 let pad_w = inner_w.saturating_sub(raw_width);
536 let _padded = if pad_w > 0 {
537 format!("{}{}", raw_line, " ".repeat(pad_w))
538 } else {
539 raw_line
540 };
541
542 let mut segs = Vec::new();
544
545 segs.push(Segment::new(" ".repeat(indent)));
547
548 segs.push(Segment::styled("│".to_string(), border_style.clone()));
550
551 if is_error {
553 segs.push(Segment::styled(
554 marker_str.to_string(),
555 error_mark_style.clone(),
556 ));
557 } else {
558 segs.push(Segment::new(marker_str));
559 }
560
561 let ln_part = format!(" {} ", ln_str);
563 segs.push(Segment::styled(ln_part, line_no_style.clone()));
564
565 segs.push(Segment::styled(" │ ", border_style.clone()));
567
568 segs.push(Segment::new(code.to_string()));
570
571 let after_marker_w = marker_cells + 1 + ln_width + 3 + UnicodeWidthStr::width(code.as_str());
574 let remain = sub_box_inner
575 .saturating_sub(2) .saturating_sub(after_marker_w);
577 if remain > 0 {
578 segs.push(Segment::new(" ".repeat(remain)));
579 }
580
581 segs.push(Segment::styled("│".to_string(), border_style.clone()));
583
584 out_lines.push(outer_content_line(segs, total_width));
585 }
586
587 {
589 let mut segs = Vec::new();
590 segs.push(Segment::styled(
591 format!("{}╰{}╯", " ".repeat(indent), "─".repeat(sub_box_inner)),
592 border_style.clone(),
593 ));
594 out_lines.push(outer_content_line(segs, total_width));
595 }
596 } else if let Some(ref line_text) = frame.line {
597 let indent = 2usize;
599 let mut segs = Vec::new();
600 segs.push(Segment::new(format!(
601 "{}❱ {}",
602 " ".repeat(indent),
603 line_text
604 )));
605 out_lines.push(outer_content_line(segs, total_width));
606 }
607
608 if self.show_locals {
610 if let Some(ref locals) = frame.locals {
611 if !locals.is_empty() {
612 let indent = 2usize;
614 let sub_box_total = content_width.saturating_sub(indent * 2);
615 let sub_box_inner = sub_box_total.saturating_sub(2);
616
617 let header_text = " locals ";
619
620 {
622 let mut segs = Vec::new();
623 segs.push(Segment::styled(
624 format!("{}╭─", " ".repeat(indent)),
625 border_style.clone(),
626 ));
627 segs.push(Segment::styled(
628 header_text.to_string(),
629 locals_header_style.clone(),
630 ));
631 let dash_count = sub_box_inner
632 .saturating_sub(header_text.len() + 1);
633 segs.push(Segment::styled(
634 format!("─{}╮", "─".repeat(dash_count)),
635 border_style.clone(),
636 ));
637 out_lines.push(outer_content_line(segs, total_width));
638 }
639
640 let inner_w = sub_box_inner.saturating_sub(2); let max_shown = self.locals_max_length;
643 let filtered_locals: Vec<(&String, &String)> = locals
644 .iter()
645 .filter(|(k, _)| {
646 if self.locals_hide_dunder
647 && k.starts_with("__")
648 && k.ends_with("__")
649 {
650 return false;
651 }
652 if self.locals_hide_sunder && k.starts_with('_') {
653 return false;
654 }
655 true
656 })
657 .take(max_shown)
658 .collect();
659
660 for (key, val) in &filtered_locals {
661 let max_str_len = self.locals_max_string;
662 let display_val = if val.len() > max_str_len {
663 format!("{}...", &val[..max_str_len])
664 } else {
665 val.to_string()
666 };
667 let line_text = format!("{} = {}", key, display_val);
668 let raw_w = UnicodeWidthStr::width(line_text.as_str());
669 let pad_w = inner_w.saturating_sub(raw_w);
670 let padded = if pad_w > 0 {
671 format!("{}{}", line_text, " ".repeat(pad_w))
672 } else {
673 truncate_to_width(&line_text, inner_w)
674 };
675
676 let mut segs = Vec::new();
677 segs.push(Segment::new(" ".repeat(indent)));
678 segs.push(Segment::styled(
679 "│".to_string(),
680 border_style.clone(),
681 ));
682 segs.push(Segment::new(format!(" {}", padded)));
683 let extra_pad = inner_w.saturating_sub(
685 UnicodeWidthStr::width(padded.as_str()),
686 );
687 if extra_pad > 0 {
688 segs.push(Segment::new(" ".repeat(extra_pad)));
689 }
690 segs.push(Segment::styled(
691 " │".to_string(),
692 border_style.clone(),
693 ));
694 out_lines.push(outer_content_line(segs, total_width));
695 }
696
697 {
699 let mut segs = Vec::new();
700 segs.push(Segment::styled(
701 format!(
702 "{}╰{}╯",
703 " ".repeat(indent),
704 "─".repeat(sub_box_inner),
705 ),
706 border_style.clone(),
707 ));
708 out_lines.push(outer_content_line(segs, total_width));
709 }
710 }
711 }
712 }
713
714 out_lines.push(outer_blank(total_width));
716 }
717
718 if suppressed_count > 0 {
720 let msg = format!(" ... {} frames hidden ...", suppressed_count);
721 let mut segs = Vec::new();
722 segs.push(Segment::styled(msg, Style::new().dim(true)));
723 out_lines.push(outer_content_line(segs, total_width));
724 out_lines.push(outer_blank(total_width));
725 suppressed_count = 0;
726 }
727
728 if let Some(ref exc_type) = stack.exc_type {
730 let exc_value = stack.exc_value.as_deref().unwrap_or("");
731 let msg = if exc_value.is_empty() {
732 format!(" {}", exc_type)
733 } else {
734 format!(" {}: {}", exc_type, exc_value)
735 };
736 let mut segs = Vec::new();
737 segs.push(Segment::styled(msg, error_style.clone()));
738 out_lines.push(outer_content_line(segs, total_width));
739 out_lines.push(outer_blank(total_width));
740 }
741
742 for note in &stack.notes {
744 let mut segs = Vec::new();
745 segs.push(Segment::styled(
746 format!(" note: {}", note),
747 Style::new().italic(true),
748 ));
749 out_lines.push(outer_content_line(segs, total_width));
750 }
751 }
752
753 out_lines.push(bottom_border(total_width));
755
756 RenderResult { lines: out_lines, items: Vec::new() }
757 }
758}
759
760fn truncate_to_width(s: &str, max_width: usize) -> String {
765 if max_width == 0 {
766 return String::new();
767 }
768 let mut w = 0usize;
769 let mut result = String::new();
770 for ch in s.chars() {
771 let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
772 if w + cw > max_width {
773 break;
774 }
775 w += cw;
776 result.push(ch);
777 }
778 result
779}
780
781pub fn install() {
790 std::panic::set_hook(Box::new(|panic_info| {
791 use std::io::Write;
792
793 let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
795 s.to_string()
796 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
797 s.clone()
798 } else {
799 "unknown panic".to_string()
800 };
801
802 let (file, line, col) = if let Some(loc) = panic_info.location() {
804 (
805 loc.file().to_string(),
806 loc.line() as usize,
807 loc.column() as usize,
808 )
809 } else {
810 ("unknown".to_string(), 0, 0)
811 };
812
813 let mut frame = Frame::new(file.clone(), line, "unknown".to_string());
816 frame.line = Some(msg.clone());
817
818 let exc_value = format!("panic at {}:{}:{}", file, line, col);
819 let traceback = Traceback::from_exception("Panic", exc_value, vec![frame])
820 .extra_lines(0);
821
822 let opts = ConsoleOptions {
824 max_width: 120,
825 ..ConsoleOptions::default()
826 };
827 let result = traceback.render(&opts);
828 let ansi = result.to_ansi();
829
830 let _ = writeln!(std::io::stderr(), "{}", ansi);
831 }));
832}
833
834#[cfg(test)]
839mod tests {
840 use super::*;
841
842 #[test]
843 fn test_frame_new() {
844 let f = Frame::new("main.rs", 42, "foo");
845 assert_eq!(f.filename, "main.rs");
846 assert_eq!(f.lineno, 42);
847 assert_eq!(f.name, "foo");
848 assert!(f.line.is_none());
849 assert!(f.locals.is_none());
850 }
851
852 #[test]
853 fn test_frame_builder() {
854 let mut locals = HashMap::new();
855 locals.insert("x".to_string(), "42".to_string());
856
857 let f = Frame::new("lib.rs", 10, "bar")
858 .line("let x = 42;")
859 .locals(locals.clone());
860
861 assert_eq!(f.line.unwrap(), "let x = 42;");
862 assert_eq!(f.locals.unwrap()["x"], "42");
863 }
864
865 #[test]
866 fn test_stack_new() {
867 let s = Stack::new();
868 assert!(s.exc_type.is_none());
869 assert!(s.exc_value.is_none());
870 assert!(!s.is_cause);
871 assert!(s.frames.is_empty());
872 }
873
874 #[test]
875 fn test_stack_builder() {
876 let s = Stack::new()
877 .exc_type("ValueError")
878 .exc_value("bad value")
879 .add_frame(Frame::new("test.rs", 5, "broken"));
880
881 assert_eq!(s.exc_type.unwrap(), "ValueError");
882 assert_eq!(s.exc_value.unwrap(), "bad value");
883 assert_eq!(s.frames.len(), 1);
884 }
885
886 #[test]
887 fn test_trace_new() {
888 let t = Trace::new();
889 assert!(t.stacks.is_empty());
890 }
891
892 #[test]
893 fn test_trace_from_stack() {
894 let s = Stack::new();
895 let t = Trace::from_stack(s);
896 assert_eq!(t.stacks.len(), 1);
897 }
898
899 #[test]
900 fn test_traceback_from_exception() {
901 let tb = Traceback::from_exception(
902 "Error",
903 "something went wrong",
904 vec![
905 Frame::new("main.rs", 1, "main"),
906 Frame::new("lib.rs", 42, "helper"),
907 ],
908 );
909 assert_eq!(tb.trace.stacks.len(), 1);
910 let stack = &tb.trace.stacks[0];
911 assert_eq!(stack.exc_type.as_deref(), Some("Error"));
912 assert_eq!(stack.exc_value.as_deref(), Some("something went wrong"));
913 assert_eq!(stack.frames.len(), 2);
914 }
915
916 #[test]
917 fn test_traceback_builder_methods() {
918 let tb = Traceback::new(Trace::new())
919 .width(100)
920 .code_width(80)
921 .extra_lines(5)
922 .theme("monokai")
923 .word_wrap(true)
924 .show_locals(true)
925 .indent_guides(true)
926 .locals_max_length(20)
927 .locals_max_string(120)
928 .locals_max_depth(10)
929 .locals_hide_dunder(false)
930 .locals_hide_sunder(true)
931 .suppress(vec!["std".to_string()])
932 .max_frames(10);
933
934 assert_eq!(tb.width, Some(100));
935 assert_eq!(tb.code_width, Some(80));
936 assert_eq!(tb.extra_lines, 5);
937 assert!(tb.word_wrap);
938 assert!(tb.show_locals);
939 assert!(!tb.locals_hide_dunder);
940 assert!(tb.locals_hide_sunder);
941 }
942
943 #[test]
944 fn test_truncate_to_width() {
945 assert_eq!(truncate_to_width("hello", 3), "hel");
946 assert_eq!(truncate_to_width("hi", 10), "hi");
947 assert_eq!(truncate_to_width("", 5), "");
948 assert_eq!(truncate_to_width("hello", 0), "");
949 }
950
951 #[test]
952 fn test_is_suppressed() {
953 let suppress = vec!["std".to_string(), "core".to_string()];
954 assert!(is_suppressed(
955 "/rustc/.../library/std/src/panic.rs",
956 &suppress,
957 ));
958 assert!(is_suppressed(
959 "/rustc/.../library/core/src/result.rs",
960 &suppress,
961 ));
962 assert!(!is_suppressed(
963 "/home/user/project/src/main.rs",
964 &suppress,
965 ));
966 }
967
968 #[test]
969 fn test_render_empty_traceback() {
970 let tb = Traceback::new(Trace::new()).width(60);
971 let opts = ConsoleOptions {
972 max_width: 60,
973 ..ConsoleOptions::default()
974 };
975 let result = tb.render(&opts);
976 assert!(!result.lines.is_empty());
978 let ansi = result.to_ansi();
980 assert!(ansi.contains("Traceback"));
981 assert!(ansi.contains("╭"));
982 assert!(ansi.contains("╰"));
983 }
984
985 #[test]
986 fn test_render_single_frame() {
987 let tb = Traceback::from_exception(
988 "TestError",
989 "testing",
990 vec![Frame::new("fake.rs", 10, "test_fn")],
991 )
992 .width(80);
993 let opts = ConsoleOptions {
994 max_width: 80,
995 ..ConsoleOptions::default()
996 };
997 let result = tb.render(&opts);
998 let ansi = result.to_ansi();
999 assert!(ansi.contains("Traceback"));
1000 assert!(ansi.contains("TestError"));
1001 assert!(ansi.contains("testing"));
1002 assert!(ansi.contains("fake.rs"));
1003 }
1004
1005 #[test]
1006 fn test_render_with_locals() {
1007 let mut locals = HashMap::new();
1008 locals.insert("x".to_string(), "42".to_string());
1009 locals.insert("name".to_string(), "hello".to_string());
1010
1011 let tb = Traceback::from_exception(
1012 "Error",
1013 "msg",
1014 vec![Frame::new("test.rs", 5, "func").locals(locals)],
1015 )
1016 .width(80)
1017 .show_locals(true);
1018
1019 let opts = ConsoleOptions {
1020 max_width: 80,
1021 ..ConsoleOptions::default()
1022 };
1023 let result = tb.render(&opts);
1024 let ansi = result.to_ansi();
1025 assert!(ansi.contains("x") || ansi.contains("name"));
1027 }
1028
1029 #[test]
1030 fn test_render_suppressed_frame() {
1031 let tb = Traceback::from_exception(
1032 "Err",
1033 "msg",
1034 vec![
1035 Frame::new("/rustc/lib.rs", 1, "hidden_fn"),
1036 Frame::new("main.rs", 10, "main"),
1037 ],
1038 )
1039 .width(80)
1040 .suppress(vec!["/rustc".to_string()]);
1041
1042 let opts = ConsoleOptions {
1043 max_width: 80,
1044 ..ConsoleOptions::default()
1045 };
1046 let result = tb.render(&opts);
1047 let ansi = result.to_ansi();
1048 assert!(ansi.contains("1 frames hidden") || ansi.contains("frames hidden"));
1049 assert!(ansi.contains("main.rs"));
1050 }
1051
1052 #[test]
1053 fn test_max_frames() {
1054 let tb = Traceback::from_exception(
1055 "Err",
1056 "msg",
1057 vec![
1058 Frame::new("a.rs", 1, "a"),
1059 Frame::new("b.rs", 2, "b"),
1060 Frame::new("c.rs", 3, "c"),
1061 ],
1062 )
1063 .width(80)
1064 .max_frames(2);
1065
1066 let opts = ConsoleOptions {
1067 max_width: 80,
1068 ..ConsoleOptions::default()
1069 };
1070 let result = tb.render(&opts);
1071 let ansi = result.to_ansi();
1072 assert!(ansi.contains("frames hidden") || ansi.contains("hidden"));
1074 }
1075
1076 #[test]
1077 fn test_theme_style_resolution() {
1078 let style = theme_style(theme::names::TRACEBACK_BORDER);
1079 assert!(!style.is_plain());
1081 }
1082
1083 #[test]
1084 fn test_locals_filtering_dunder() {
1085 let mut locals = HashMap::new();
1086 locals.insert("__private__".to_string(), "secret".to_string());
1087 locals.insert("normal".to_string(), "visible".to_string());
1088
1089 let tb = Traceback::from_exception("E", "msg", vec![
1090 Frame::new("t.rs", 1, "f").locals(locals),
1091 ])
1092 .width(80)
1093 .show_locals(true)
1094 .locals_hide_dunder(true);
1095
1096 let opts = ConsoleOptions {
1097 max_width: 80,
1098 ..ConsoleOptions::default()
1099 };
1100 let result = tb.render(&opts);
1101 let ansi = result.to_ansi();
1102
1103 let _has_private = ansi.contains("__private__");
1105 let has_normal = ansi.contains("normal");
1106
1107 assert!(has_normal);
1110 }
1111
1112 #[test]
1113 fn test_locals_filtering_sunder() {
1114 let mut locals = HashMap::new();
1115 locals.insert("_hidden".to_string(), "invisible".to_string());
1116 locals.insert("visible".to_string(), "yes".to_string());
1117
1118 let tb = Traceback::from_exception("E", "msg", vec![
1119 Frame::new("t.rs", 1, "f").locals(locals),
1120 ])
1121 .width(80)
1122 .show_locals(true)
1123 .locals_hide_sunder(true);
1124
1125 let opts = ConsoleOptions {
1126 max_width: 80,
1127 ..ConsoleOptions::default()
1128 };
1129 let result = tb.render(&opts);
1130 let ansi = result.to_ansi();
1131
1132 assert!(!ansi.contains("_hidden"));
1134 assert!(ansi.contains("visible"));
1135 }
1136
1137 #[test]
1138 fn test_install_hook() {
1139 install();
1141 let _ = std::panic::take_hook();
1143 }
1144
1145 #[test]
1146 fn test_multiple_stacks() {
1147 let mut stack1 = Stack::new();
1148 stack1.exc_type = Some("IOError".to_string());
1149 stack1.exc_value = Some("file not found".to_string());
1150 stack1.frames.push(Frame::new("io.rs", 10, "read_file"));
1151
1152 let mut stack2 = Stack::new();
1153 stack2.exc_type = Some("ValueError".to_string());
1154 stack2.exc_value = Some("bad data".to_string());
1155 stack2.is_cause = true;
1156 stack2.frames.push(Frame::new("main.rs", 20, "process"));
1157
1158 let trace = Trace {
1159 stacks: vec![stack1, stack2],
1160 };
1161
1162 let tb = Traceback::new(trace).width(80);
1163 let opts = ConsoleOptions {
1164 max_width: 80,
1165 ..ConsoleOptions::default()
1166 };
1167 let result = tb.render(&opts);
1168 let ansi = result.to_ansi();
1169
1170 assert!(ansi.contains("IOError"));
1171 assert!(ansi.contains("ValueError"));
1172 }
1173}