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 {
48 Self {
49 filename: filename.into(),
50 lineno,
51 name: name.into(),
52 line: None,
53 locals: None,
54 last_instruction: None,
55 }
56 }
57
58 pub fn line(mut self, line: impl Into<String>) -> Self {
60 self.line = Some(line.into());
61 self
62 }
63
64 pub fn locals(mut self, locals: HashMap<String, String>) -> Self {
66 self.locals = Some(locals);
67 self
68 }
69}
70
71#[derive(Debug, Clone, Default)]
73pub struct Stack {
74 pub exc_type: Option<String>,
75 pub exc_value: Option<String>,
76 pub syntax_error: Option<String>,
77 pub is_cause: bool,
78 pub frames: Vec<Frame>,
79 pub notes: Vec<String>,
80 pub is_group: bool,
81 pub exceptions: Vec<Stack>,
82}
83
84impl Stack {
85 pub fn new() -> Self {
87 Self {
88 exc_type: None,
89 exc_value: None,
90 syntax_error: None,
91 is_cause: false,
92 frames: Vec::new(),
93 notes: Vec::new(),
94 is_group: false,
95 exceptions: Vec::new(),
96 }
97 }
98
99 pub fn exc_type(mut self, t: impl Into<String>) -> Self {
101 self.exc_type = Some(t.into());
102 self
103 }
104
105 pub fn exc_value(mut self, v: impl Into<String>) -> Self {
107 self.exc_value = Some(v.into());
108 self
109 }
110
111 pub fn add_frame(mut self, frame: Frame) -> Self {
113 self.frames.push(frame);
114 self
115 }
116}
117
118#[derive(Debug, Clone, Default)]
120pub struct Trace {
121 pub stacks: Vec<Stack>,
122}
123
124impl Trace {
125 pub fn new() -> Self {
127 Self { stacks: Vec::new() }
128 }
129
130 pub fn from_stack(stack: Stack) -> Self {
132 Self {
133 stacks: vec![stack],
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
146pub struct Traceback {
147 trace: Trace,
148 width: Option<usize>,
149 code_width: Option<usize>,
150 extra_lines: usize,
151 theme_name: Option<String>,
152 word_wrap: bool,
153 show_locals: bool,
154 indent_guides: bool,
155 locals_max_length: usize,
156 locals_max_string: usize,
157 locals_max_depth: usize,
158 locals_hide_dunder: bool,
159 locals_hide_sunder: bool,
160 suppress: Vec<String>,
161 max_frames: Option<usize>,
162}
163
164impl Traceback {
165 pub fn new(trace: Trace) -> Self {
167 Self {
168 trace,
169 width: None,
170 code_width: None,
171 extra_lines: 3,
172 theme_name: None,
173 word_wrap: false,
174 show_locals: false,
175 indent_guides: false,
176 locals_max_length: 10,
177 locals_max_string: 80,
178 locals_max_depth: 5,
179 locals_hide_dunder: true,
180 locals_hide_sunder: false,
181 suppress: Vec::new(),
182 max_frames: None,
183 }
184 }
185
186 pub fn from_exception(
189 exc_type: impl Into<String>,
190 exc_value: impl Into<String>,
191 frames: Vec<Frame>,
192 ) -> Self {
193 let mut stack = Stack::new();
194 stack.exc_type = Some(exc_type.into());
195 stack.exc_value = Some(exc_value.into());
196 stack.frames = frames;
197 let trace = Trace::from_stack(stack);
198 Self::new(trace)
199 }
200
201 pub fn width(mut self, width: usize) -> Self {
205 self.width = Some(width);
206 self
207 }
208
209 pub fn code_width(mut self, width: usize) -> Self {
211 self.code_width = Some(width);
212 self
213 }
214
215 pub fn extra_lines(mut self, n: usize) -> Self {
217 self.extra_lines = n;
218 self
219 }
220
221 pub fn theme(mut self, theme: impl Into<String>) -> Self {
223 self.theme_name = Some(theme.into());
224 self
225 }
226
227 pub fn word_wrap(mut self, wrap: bool) -> Self {
229 self.word_wrap = wrap;
230 self
231 }
232
233 pub fn show_locals(mut self, show: bool) -> Self {
235 self.show_locals = show;
236 self
237 }
238
239 pub fn indent_guides(mut self, guides: bool) -> Self {
241 self.indent_guides = guides;
242 self
243 }
244
245 pub fn locals_max_length(mut self, n: usize) -> Self {
247 self.locals_max_length = n;
248 self
249 }
250
251 pub fn locals_max_string(mut self, n: usize) -> Self {
253 self.locals_max_string = n;
254 self
255 }
256
257 pub fn locals_max_depth(mut self, n: usize) -> Self {
259 self.locals_max_depth = n;
260 self
261 }
262
263 pub fn locals_hide_dunder(mut self, hide: bool) -> Self {
265 self.locals_hide_dunder = hide;
266 self
267 }
268
269 pub fn locals_hide_sunder(mut self, hide: bool) -> Self {
271 self.locals_hide_sunder = hide;
272 self
273 }
274
275 pub fn suppress(mut self, suppress: Vec<String>) -> Self {
277 self.suppress = suppress;
278 self
279 }
280
281 pub fn max_frames(mut self, n: usize) -> Self {
283 self.max_frames = Some(n);
284 self
285 }
286}
287
288fn theme_style(name: &str) -> Style {
295 crate::theme::default_theme()
296 .get(name)
297 .cloned()
298 .unwrap_or_default()
299}
300
301fn outer_content_line(content: Vec<Segment>, total_width: usize) -> Vec<Segment> {
307 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
308 let mut line = Vec::new();
309
310 line.push(Segment::styled("│ ".to_string(), border_style.clone()));
312
313 let mut content_w = 0usize;
315 for seg in &content {
316 content_w += seg.cell_length();
317 }
318 line.extend(content);
319
320 let inner_w = total_width.saturating_sub(4); let pad = inner_w.saturating_sub(content_w);
323 if pad > 0 {
324 line.push(Segment::new(" ".repeat(pad)));
325 }
326
327 line.push(Segment::styled(" │".to_string(), border_style));
329 line
330}
331
332fn outer_blank(total_width: usize) -> Vec<Segment> {
334 outer_content_line(Vec::new(), total_width)
335}
336
337fn top_border(total_width: usize) -> Vec<Segment> {
339 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
340 let title_style = theme_style(theme::names::TRACEBACK_TITLE);
341
342 let title = " Traceback (most recent call last) ";
343 let dashes_total = total_width.saturating_sub(title.len() + 4); let left_dashes = dashes_total / 2;
345 let right_dashes = dashes_total - left_dashes;
346
347 vec![
348 Segment::styled("╭─".to_string(), border_style.clone()),
349 Segment::styled(
350 "─".repeat(left_dashes.saturating_sub(1)),
351 border_style.clone(),
352 ),
353 Segment::styled(title.to_string(), title_style),
354 Segment::styled(
355 "─".repeat(right_dashes.saturating_sub(1)),
356 border_style.clone(),
357 ),
358 Segment::styled("─╮".to_string(), border_style),
359 ]
360}
361
362fn bottom_border(total_width: usize) -> Vec<Segment> {
364 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
365 let dashes = total_width.saturating_sub(2);
366 vec![Segment::styled(
367 format!("╰{}╯", "─".repeat(dashes)),
368 border_style,
369 )]
370}
371
372fn read_source_lines(
374 filename: &str,
375 lineno: usize,
376 extra_lines: usize,
377) -> (usize, Vec<(usize, String)>) {
378 let content = match fs::read_to_string(Path::new(filename)) {
380 Ok(s) => s,
381 Err(_) => return (0, Vec::new()),
382 };
383
384 let all_lines: Vec<&str> = content.lines().collect();
385 if all_lines.is_empty() {
386 return (0, Vec::new());
387 }
388
389 let start = if lineno > extra_lines {
390 lineno - extra_lines
391 } else {
392 1
393 };
394 let end = (lineno + extra_lines).min(all_lines.len());
396
397 let mut result = Vec::new();
398 for i in start..=end {
399 let line_str = all_lines.get(i.saturating_sub(1)).copied().unwrap_or("");
400 result.push((i, line_str.to_string()));
401 }
402
403 (lineno, result)
404}
405
406fn is_suppressed(filename: &str, suppress: &[String]) -> bool {
408 for pattern in suppress {
409 if filename.starts_with(pattern) || filename.contains(pattern) {
410 return true;
411 }
412 }
413 false
414}
415
416impl Renderable for Traceback {
421 fn render(&self, options: &ConsoleOptions) -> RenderResult {
422 let total_width = self.width.unwrap_or(options.max_width.min(120));
423 let content_width = total_width.saturating_sub(4); let border_style = theme_style(theme::names::TRACEBACK_BORDER);
427 let filename_style = theme_style(theme::names::TRACEBACK_FILENAME);
428 let line_no_style = theme_style(theme::names::TRACEBACK_LINE_NO);
429 let error_mark_style = theme_style(theme::names::TRACEBACK_ERROR_MARK);
430 let error_style = theme_style(theme::names::TRACEBACK_ERROR);
431 let locals_header_style = theme_style(theme::names::TRACEBACK_LOCALS_HEADER);
432
433 let mut out_lines: Vec<Vec<Segment>> = Vec::new();
435
436 out_lines.push(top_border(total_width));
438
439 out_lines.push(outer_blank(total_width));
441
442 let mut rendered_count = 0usize;
444 let mut suppressed_count = 0usize;
445
446 for stack in &self.trace.stacks {
448 let frames_iter: Box<dyn Iterator<Item = &Frame>> = if stack.is_cause {
450 Box::new(stack.frames.iter())
452 } else {
453 Box::new(stack.frames.iter())
454 };
455
456 let max_frames = self.max_frames.unwrap_or(usize::MAX);
457
458 for frame in frames_iter {
459 if is_suppressed(&frame.filename, &self.suppress) {
461 suppressed_count += 1;
462 continue;
463 }
464
465 if rendered_count >= max_frames {
467 suppressed_count += 1;
468 continue;
469 }
470 rendered_count += 1;
471
472 {
475 let loc = format!("{}:{}", frame.filename, frame.lineno);
476 let func = if frame.name.is_empty() {
477 String::new()
478 } else {
479 format!(" in {}", frame.name)
480 };
481
482 let mut header_segs = Vec::new();
483 header_segs.push(Segment::styled(
484 format!(" {}", loc),
485 filename_style.clone(),
486 ));
487 header_segs.push(Segment::styled(func, Style::new()));
488 out_lines.push(outer_content_line(header_segs, total_width));
489 }
490
491 let (error_line_num, source_lines) =
493 read_source_lines(&frame.filename, frame.lineno, self.extra_lines);
494
495 if !source_lines.is_empty() {
496 let indent = 2usize;
498 let sub_box_total = content_width.saturating_sub(indent * 2);
499 let sub_box_inner = sub_box_total.saturating_sub(2); let max_ln = source_lines.iter().map(|(ln, _)| *ln).max().unwrap_or(0);
504 let ln_width = max_ln.to_string().len().max(2);
505
506 let marker_cells = 2;
508
509 let prefix_cells = marker_cells + 1 + ln_width + 3; let code_cells = sub_box_inner.saturating_sub(prefix_cells);
514
515 {
517 let mut segs = Vec::new();
518 segs.push(Segment::styled(
519 format!("{}╭{}╮", " ".repeat(indent), "─".repeat(sub_box_inner)),
520 border_style.clone(),
521 ));
522 out_lines.push(outer_content_line(segs, total_width));
523 }
524
525 for (line_num, line_text) in &source_lines {
527 let is_error = *line_num == error_line_num;
528
529 let marker = if is_error { "❱" } else { " " };
530 let marker_str = format!("{:<width$}", marker, width = marker_cells);
531
532 let ln_str = format!("{:>width$}", line_num, width = ln_width);
533 let code = truncate_to_width(line_text, code_cells);
534
535 let raw_line = format!("{}{} {} │ {} ", marker_str, " ", ln_str, code);
536
537 let inner_w = sub_box_inner.saturating_sub(2); let raw_width = UnicodeWidthStr::width(raw_line.as_str());
541 let pad_w = inner_w.saturating_sub(raw_width);
542 let _padded = if pad_w > 0 {
543 format!("{}{}", raw_line, " ".repeat(pad_w))
544 } else {
545 raw_line
546 };
547
548 let mut segs = Vec::new();
550
551 segs.push(Segment::new(" ".repeat(indent)));
553
554 segs.push(Segment::styled("│".to_string(), border_style.clone()));
556
557 if is_error {
559 segs.push(Segment::styled(
560 marker_str.to_string(),
561 error_mark_style.clone(),
562 ));
563 } else {
564 segs.push(Segment::new(marker_str));
565 }
566
567 let ln_part = format!(" {} ", ln_str);
569 segs.push(Segment::styled(ln_part, line_no_style.clone()));
570
571 segs.push(Segment::styled(" │ ", border_style.clone()));
573
574 segs.push(Segment::new(code.to_string()));
576
577 let after_marker_w =
580 marker_cells + 1 + ln_width + 3 + UnicodeWidthStr::width(code.as_str());
581 let remain = sub_box_inner
582 .saturating_sub(2) .saturating_sub(after_marker_w);
584 if remain > 0 {
585 segs.push(Segment::new(" ".repeat(remain)));
586 }
587
588 segs.push(Segment::styled("│".to_string(), border_style.clone()));
590
591 out_lines.push(outer_content_line(segs, total_width));
592 }
593
594 {
596 let mut segs = Vec::new();
597 segs.push(Segment::styled(
598 format!("{}╰{}╯", " ".repeat(indent), "─".repeat(sub_box_inner)),
599 border_style.clone(),
600 ));
601 out_lines.push(outer_content_line(segs, total_width));
602 }
603 } else if let Some(ref line_text) = frame.line {
604 let indent = 2usize;
606 let mut segs = Vec::new();
607 segs.push(Segment::new(format!(
608 "{}❱ {}",
609 " ".repeat(indent),
610 line_text
611 )));
612 out_lines.push(outer_content_line(segs, total_width));
613 }
614
615 if self.show_locals {
617 if let Some(ref locals) = frame.locals {
618 if !locals.is_empty() {
619 let indent = 2usize;
621 let sub_box_total = content_width.saturating_sub(indent * 2);
622 let sub_box_inner = sub_box_total.saturating_sub(2);
623
624 let header_text = " locals ";
626
627 {
629 let mut segs = Vec::new();
630 segs.push(Segment::styled(
631 format!("{}╭─", " ".repeat(indent)),
632 border_style.clone(),
633 ));
634 segs.push(Segment::styled(
635 header_text.to_string(),
636 locals_header_style.clone(),
637 ));
638 let dash_count =
639 sub_box_inner.saturating_sub(header_text.len() + 1);
640 segs.push(Segment::styled(
641 format!("─{}╮", "─".repeat(dash_count)),
642 border_style.clone(),
643 ));
644 out_lines.push(outer_content_line(segs, total_width));
645 }
646
647 let inner_w = sub_box_inner.saturating_sub(2); let max_shown = self.locals_max_length;
650 let filtered_locals: Vec<(&String, &String)> = locals
651 .iter()
652 .filter(|(k, _)| {
653 if self.locals_hide_dunder
654 && k.starts_with("__")
655 && k.ends_with("__")
656 {
657 return false;
658 }
659 if self.locals_hide_sunder && k.starts_with('_') {
660 return false;
661 }
662 true
663 })
664 .take(max_shown)
665 .collect();
666
667 for (key, val) in &filtered_locals {
668 let max_str_len = self.locals_max_string;
669 let display_val = if val.len() > max_str_len {
670 format!("{}...", &val[..max_str_len])
671 } else {
672 val.to_string()
673 };
674 let line_text = format!("{} = {}", key, display_val);
675 let raw_w = UnicodeWidthStr::width(line_text.as_str());
676 let pad_w = inner_w.saturating_sub(raw_w);
677 let padded = if pad_w > 0 {
678 format!("{}{}", line_text, " ".repeat(pad_w))
679 } else {
680 truncate_to_width(&line_text, inner_w)
681 };
682
683 let mut segs = Vec::new();
684 segs.push(Segment::new(" ".repeat(indent)));
685 segs.push(Segment::styled("│".to_string(), border_style.clone()));
686 segs.push(Segment::new(format!(" {}", padded)));
687 let extra_pad =
689 inner_w.saturating_sub(UnicodeWidthStr::width(padded.as_str()));
690 if extra_pad > 0 {
691 segs.push(Segment::new(" ".repeat(extra_pad)));
692 }
693 segs.push(Segment::styled(" │".to_string(), border_style.clone()));
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 segs = vec![Segment::styled(msg, Style::new().dim(true))];
722 out_lines.push(outer_content_line(segs, total_width));
723 out_lines.push(outer_blank(total_width));
724 suppressed_count = 0;
725 }
726
727 if let Some(ref exc_type) = stack.exc_type {
729 let exc_value = stack.exc_value.as_deref().unwrap_or("");
730 let msg = if exc_value.is_empty() {
731 format!(" {}", exc_type)
732 } else {
733 format!(" {}: {}", exc_type, exc_value)
734 };
735 let segs = vec![Segment::styled(msg, error_style.clone())];
736 out_lines.push(outer_content_line(segs, total_width));
737 out_lines.push(outer_blank(total_width));
738 }
739
740 for note in &stack.notes {
742 let mut segs = Vec::new();
743 segs.push(Segment::styled(
744 format!(" note: {}", note),
745 Style::new().italic(true),
746 ));
747 out_lines.push(outer_content_line(segs, total_width));
748 }
749 }
750
751 out_lines.push(bottom_border(total_width));
753
754 RenderResult {
755 lines: out_lines,
756 items: Vec::new(),
757 }
758 }
759}
760
761fn truncate_to_width(s: &str, max_width: usize) -> String {
766 if max_width == 0 {
767 return String::new();
768 }
769 let mut w = 0usize;
770 let mut result = String::new();
771 for ch in s.chars() {
772 let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
773 if w + cw > max_width {
774 break;
775 }
776 w += cw;
777 result.push(ch);
778 }
779 result
780}
781
782pub fn install() {
791 std::panic::set_hook(Box::new(|panic_info| {
792 use std::io::Write;
793
794 let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
796 s.to_string()
797 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
798 s.clone()
799 } else {
800 "unknown panic".to_string()
801 };
802
803 let (file, line, col) = if let Some(loc) = panic_info.location() {
805 (
806 loc.file().to_string(),
807 loc.line() as usize,
808 loc.column() as usize,
809 )
810 } else {
811 ("unknown".to_string(), 0, 0)
812 };
813
814 let mut frame = Frame::new(file.clone(), line, "unknown".to_string());
817 frame.line = Some(msg.clone());
818
819 let exc_value = format!("panic at {}:{}:{}", file, line, col);
820 let traceback = Traceback::from_exception("Panic", exc_value, vec![frame]).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("/home/user/project/src/main.rs", &suppress,));
963 }
964
965 #[test]
966 fn test_render_empty_traceback() {
967 let tb = Traceback::new(Trace::new()).width(60);
968 let opts = ConsoleOptions {
969 max_width: 60,
970 ..ConsoleOptions::default()
971 };
972 let result = tb.render(&opts);
973 assert!(!result.lines.is_empty());
975 let ansi = result.to_ansi();
977 assert!(ansi.contains("Traceback"));
978 assert!(ansi.contains("╭"));
979 assert!(ansi.contains("╰"));
980 }
981
982 #[test]
983 fn test_render_single_frame() {
984 let tb = Traceback::from_exception(
985 "TestError",
986 "testing",
987 vec![Frame::new("fake.rs", 10, "test_fn")],
988 )
989 .width(80);
990 let opts = ConsoleOptions {
991 max_width: 80,
992 ..ConsoleOptions::default()
993 };
994 let result = tb.render(&opts);
995 let ansi = result.to_ansi();
996 assert!(ansi.contains("Traceback"));
997 assert!(ansi.contains("TestError"));
998 assert!(ansi.contains("testing"));
999 assert!(ansi.contains("fake.rs"));
1000 }
1001
1002 #[test]
1003 fn test_render_with_locals() {
1004 let mut locals = HashMap::new();
1005 locals.insert("x".to_string(), "42".to_string());
1006 locals.insert("name".to_string(), "hello".to_string());
1007
1008 let tb = Traceback::from_exception(
1009 "Error",
1010 "msg",
1011 vec![Frame::new("test.rs", 5, "func").locals(locals)],
1012 )
1013 .width(80)
1014 .show_locals(true);
1015
1016 let opts = ConsoleOptions {
1017 max_width: 80,
1018 ..ConsoleOptions::default()
1019 };
1020 let result = tb.render(&opts);
1021 let ansi = result.to_ansi();
1022 assert!(ansi.contains("x") || ansi.contains("name"));
1024 }
1025
1026 #[test]
1027 fn test_render_suppressed_frame() {
1028 let tb = Traceback::from_exception(
1029 "Err",
1030 "msg",
1031 vec![
1032 Frame::new("/rustc/lib.rs", 1, "hidden_fn"),
1033 Frame::new("main.rs", 10, "main"),
1034 ],
1035 )
1036 .width(80)
1037 .suppress(vec!["/rustc".to_string()]);
1038
1039 let opts = ConsoleOptions {
1040 max_width: 80,
1041 ..ConsoleOptions::default()
1042 };
1043 let result = tb.render(&opts);
1044 let ansi = result.to_ansi();
1045 assert!(ansi.contains("1 frames hidden") || ansi.contains("frames hidden"));
1046 assert!(ansi.contains("main.rs"));
1047 }
1048
1049 #[test]
1050 fn test_max_frames() {
1051 let tb = Traceback::from_exception(
1052 "Err",
1053 "msg",
1054 vec![
1055 Frame::new("a.rs", 1, "a"),
1056 Frame::new("b.rs", 2, "b"),
1057 Frame::new("c.rs", 3, "c"),
1058 ],
1059 )
1060 .width(80)
1061 .max_frames(2);
1062
1063 let opts = ConsoleOptions {
1064 max_width: 80,
1065 ..ConsoleOptions::default()
1066 };
1067 let result = tb.render(&opts);
1068 let ansi = result.to_ansi();
1069 assert!(ansi.contains("frames hidden") || ansi.contains("hidden"));
1071 }
1072
1073 #[test]
1074 fn test_theme_style_resolution() {
1075 let style = theme_style(theme::names::TRACEBACK_BORDER);
1076 assert!(!style.is_plain());
1078 }
1079
1080 #[test]
1081 fn test_locals_filtering_dunder() {
1082 let mut locals = HashMap::new();
1083 locals.insert("__private__".to_string(), "secret".to_string());
1084 locals.insert("normal".to_string(), "visible".to_string());
1085
1086 let tb =
1087 Traceback::from_exception("E", "msg", vec![Frame::new("t.rs", 1, "f").locals(locals)])
1088 .width(80)
1089 .show_locals(true)
1090 .locals_hide_dunder(true);
1091
1092 let opts = ConsoleOptions {
1093 max_width: 80,
1094 ..ConsoleOptions::default()
1095 };
1096 let result = tb.render(&opts);
1097 let ansi = result.to_ansi();
1098
1099 let _has_private = ansi.contains("__private__");
1101 let has_normal = ansi.contains("normal");
1102
1103 assert!(has_normal);
1106 }
1107
1108 #[test]
1109 fn test_locals_filtering_sunder() {
1110 let mut locals = HashMap::new();
1111 locals.insert("_hidden".to_string(), "invisible".to_string());
1112 locals.insert("visible".to_string(), "yes".to_string());
1113
1114 let tb =
1115 Traceback::from_exception("E", "msg", vec![Frame::new("t.rs", 1, "f").locals(locals)])
1116 .width(80)
1117 .show_locals(true)
1118 .locals_hide_sunder(true);
1119
1120 let opts = ConsoleOptions {
1121 max_width: 80,
1122 ..ConsoleOptions::default()
1123 };
1124 let result = tb.render(&opts);
1125 let ansi = result.to_ansi();
1126
1127 assert!(!ansi.contains("_hidden"));
1129 assert!(ansi.contains("visible"));
1130 }
1131
1132 #[test]
1133 fn test_install_hook() {
1134 install();
1136 let _ = std::panic::take_hook();
1138 }
1139
1140 #[test]
1141 fn test_multiple_stacks() {
1142 let mut stack1 = Stack::new();
1143 stack1.exc_type = Some("IOError".to_string());
1144 stack1.exc_value = Some("file not found".to_string());
1145 stack1.frames.push(Frame::new("io.rs", 10, "read_file"));
1146
1147 let mut stack2 = Stack::new();
1148 stack2.exc_type = Some("ValueError".to_string());
1149 stack2.exc_value = Some("bad data".to_string());
1150 stack2.is_cause = true;
1151 stack2.frames.push(Frame::new("main.rs", 20, "process"));
1152
1153 let trace = Trace {
1154 stacks: vec![stack1, stack2],
1155 };
1156
1157 let tb = Traceback::new(trace).width(80);
1158 let opts = ConsoleOptions {
1159 max_width: 80,
1160 ..ConsoleOptions::default()
1161 };
1162 let result = tb.render(&opts);
1163 let ansi = result.to_ansi();
1164
1165 assert!(ansi.contains("IOError"));
1166 assert!(ansi.contains("ValueError"));
1167 }
1168}