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)]
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)]
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 { stacks: vec![stack] }
133 }
134}
135
136#[derive(Debug, Clone)]
144pub struct Traceback {
145 trace: Trace,
146 width: Option<usize>,
147 code_width: Option<usize>,
148 extra_lines: usize,
149 theme_name: Option<String>,
150 word_wrap: bool,
151 show_locals: bool,
152 indent_guides: bool,
153 locals_max_length: usize,
154 locals_max_string: usize,
155 locals_max_depth: usize,
156 locals_hide_dunder: bool,
157 locals_hide_sunder: bool,
158 suppress: Vec<String>,
159 max_frames: Option<usize>,
160}
161
162impl Traceback {
163 pub fn new(trace: Trace) -> Self {
165 Self {
166 trace,
167 width: None,
168 code_width: None,
169 extra_lines: 3,
170 theme_name: None,
171 word_wrap: false,
172 show_locals: false,
173 indent_guides: false,
174 locals_max_length: 10,
175 locals_max_string: 80,
176 locals_max_depth: 5,
177 locals_hide_dunder: true,
178 locals_hide_sunder: false,
179 suppress: Vec::new(),
180 max_frames: None,
181 }
182 }
183
184 pub fn from_exception(
187 exc_type: impl Into<String>,
188 exc_value: impl Into<String>,
189 frames: Vec<Frame>,
190 ) -> Self {
191 let mut stack = Stack::new();
192 stack.exc_type = Some(exc_type.into());
193 stack.exc_value = Some(exc_value.into());
194 stack.frames = frames;
195 let trace = Trace::from_stack(stack);
196 Self::new(trace)
197 }
198
199 pub fn width(mut self, width: usize) -> Self {
203 self.width = Some(width);
204 self
205 }
206
207 pub fn code_width(mut self, width: usize) -> Self {
209 self.code_width = Some(width);
210 self
211 }
212
213 pub fn extra_lines(mut self, n: usize) -> Self {
215 self.extra_lines = n;
216 self
217 }
218
219 pub fn theme(mut self, theme: impl Into<String>) -> Self {
221 self.theme_name = Some(theme.into());
222 self
223 }
224
225 pub fn word_wrap(mut self, wrap: bool) -> Self {
227 self.word_wrap = wrap;
228 self
229 }
230
231 pub fn show_locals(mut self, show: bool) -> Self {
233 self.show_locals = show;
234 self
235 }
236
237 pub fn indent_guides(mut self, guides: bool) -> Self {
239 self.indent_guides = guides;
240 self
241 }
242
243 pub fn locals_max_length(mut self, n: usize) -> Self {
245 self.locals_max_length = n;
246 self
247 }
248
249 pub fn locals_max_string(mut self, n: usize) -> Self {
251 self.locals_max_string = n;
252 self
253 }
254
255 pub fn locals_max_depth(mut self, n: usize) -> Self {
257 self.locals_max_depth = n;
258 self
259 }
260
261 pub fn locals_hide_dunder(mut self, hide: bool) -> Self {
263 self.locals_hide_dunder = hide;
264 self
265 }
266
267 pub fn locals_hide_sunder(mut self, hide: bool) -> Self {
269 self.locals_hide_sunder = hide;
270 self
271 }
272
273 pub fn suppress(mut self, suppress: Vec<String>) -> Self {
275 self.suppress = suppress;
276 self
277 }
278
279 pub fn max_frames(mut self, n: usize) -> Self {
281 self.max_frames = Some(n);
282 self
283 }
284}
285
286fn theme_style(name: &str) -> Style {
293 crate::theme::default_theme()
294 .get(name)
295 .cloned()
296 .unwrap_or_default()
297}
298
299fn outer_content_line(content: Vec<Segment>, total_width: usize) -> Vec<Segment> {
305 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
306 let mut line = Vec::new();
307
308 line.push(Segment::styled("│ ".to_string(), border_style.clone()));
310
311 let mut content_w = 0usize;
313 for seg in &content {
314 content_w += seg.cell_length();
315 }
316 line.extend(content);
317
318 let inner_w = total_width.saturating_sub(4); let pad = inner_w.saturating_sub(content_w);
321 if pad > 0 {
322 line.push(Segment::new(" ".repeat(pad)));
323 }
324
325 line.push(Segment::styled(" │".to_string(), border_style));
327 line
328}
329
330fn outer_blank(total_width: usize) -> Vec<Segment> {
332 outer_content_line(Vec::new(), total_width)
333}
334
335fn top_border(total_width: usize) -> Vec<Segment> {
337 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
338 let title_style = theme_style(theme::names::TRACEBACK_TITLE);
339
340 let title = " Traceback (most recent call last) ";
341 let dashes_total = total_width.saturating_sub(title.len() + 4); let left_dashes = dashes_total / 2;
343 let right_dashes = dashes_total - left_dashes;
344
345 let mut segs = Vec::new();
346 segs.push(Segment::styled("╭─".to_string(), border_style.clone()));
347 segs.push(Segment::styled(
348 "─".repeat(left_dashes.saturating_sub(1)),
349 border_style.clone(),
350 ));
351 segs.push(Segment::styled(title.to_string(), title_style));
352 segs.push(Segment::styled(
353 "─".repeat(right_dashes.saturating_sub(1)),
354 border_style.clone(),
355 ));
356 segs.push(Segment::styled("─╮".to_string(), border_style));
357 segs
358}
359
360fn bottom_border(total_width: usize) -> Vec<Segment> {
362 let border_style = theme_style(theme::names::TRACEBACK_BORDER);
363 let dashes = total_width.saturating_sub(2);
364 vec![Segment::styled(
365 format!("╰{}╯", "─".repeat(dashes)),
366 border_style,
367 )]
368}
369
370fn read_source_lines(
372 filename: &str,
373 lineno: usize,
374 extra_lines: usize,
375) -> (usize, Vec<(usize, String)>) {
376 let content = match fs::read_to_string(Path::new(filename)) {
378 Ok(s) => s,
379 Err(_) => return (0, Vec::new()),
380 };
381
382 let all_lines: Vec<&str> = content.lines().collect();
383 if all_lines.is_empty() {
384 return (0, Vec::new());
385 }
386
387 let start = if lineno > extra_lines {
388 lineno - extra_lines
389 } else {
390 1
391 };
392 let end = (lineno + extra_lines).min(all_lines.len());
394
395 let mut result = Vec::new();
396 for i in start..=end {
397 let line_str = all_lines.get(i.saturating_sub(1)).copied().unwrap_or("");
398 result.push((i, line_str.to_string()));
399 }
400
401 (lineno, result)
402}
403
404fn is_suppressed(filename: &str, suppress: &[String]) -> bool {
406 for pattern in suppress {
407 if filename.starts_with(pattern) || filename.contains(pattern) {
408 return true;
409 }
410 }
411 false
412}
413
414impl Renderable for Traceback {
419 fn render(&self, options: &ConsoleOptions) -> RenderResult {
420 let total_width = self.width.unwrap_or(options.max_width.min(120));
421 let content_width = total_width.saturating_sub(4); let border_style = theme_style(theme::names::TRACEBACK_BORDER);
425 let filename_style = theme_style(theme::names::TRACEBACK_FILENAME);
426 let line_no_style = theme_style(theme::names::TRACEBACK_LINE_NO);
427 let error_mark_style = theme_style(theme::names::TRACEBACK_ERROR_MARK);
428 let error_style = theme_style(theme::names::TRACEBACK_ERROR);
429 let locals_header_style = theme_style(theme::names::TRACEBACK_LOCALS_HEADER);
430
431 let mut out_lines: Vec<Vec<Segment>> = Vec::new();
433
434 out_lines.push(top_border(total_width));
436
437 out_lines.push(outer_blank(total_width));
439
440 let mut rendered_count = 0usize;
442 let mut suppressed_count = 0usize;
443
444 for stack in &self.trace.stacks {
446 let frames_iter: Box<dyn Iterator<Item = &Frame>> = if stack.is_cause {
448 Box::new(stack.frames.iter())
450 } else {
451 Box::new(stack.frames.iter())
452 };
453
454 let max_frames = self.max_frames.unwrap_or(usize::MAX);
455
456 for frame in frames_iter {
457 if is_suppressed(&frame.filename, &self.suppress) {
459 suppressed_count += 1;
460 continue;
461 }
462
463 if rendered_count >= max_frames {
465 suppressed_count += 1;
466 continue;
467 }
468 rendered_count += 1;
469
470 {
473 let loc = format!(
474 "{}:{}",
475 frame.filename,
476 frame.lineno
477 );
478 let func = if frame.name.is_empty() {
479 String::new()
480 } else {
481 format!(" in {}", frame.name)
482 };
483
484 let mut header_segs = Vec::new();
485 header_segs.push(Segment::styled(
486 format!(" {}", loc),
487 filename_style.clone(),
488 ));
489 header_segs.push(Segment::styled(func, Style::new()));
490 out_lines.push(outer_content_line(header_segs, total_width));
491 }
492
493 let (error_line_num, source_lines) =
495 read_source_lines(&frame.filename, frame.lineno, self.extra_lines);
496
497 if !source_lines.is_empty() {
498 let indent = 2usize;
500 let sub_box_total = content_width.saturating_sub(indent * 2);
501 let sub_box_inner = sub_box_total.saturating_sub(2); let max_ln = source_lines
506 .iter()
507 .map(|(ln, _)| *ln)
508 .max()
509 .unwrap_or(0);
510 let ln_width = max_ln.to_string().len().max(2);
511
512 let marker_cells = 2;
514
515 let prefix_cells = marker_cells + 1 + ln_width + 3; let code_cells = sub_box_inner.saturating_sub(prefix_cells);
520
521 {
523 let mut segs = Vec::new();
524 segs.push(Segment::styled(
525 format!("{}╭{}╮", " ".repeat(indent), "─".repeat(sub_box_inner)),
526 border_style.clone(),
527 ));
528 out_lines.push(outer_content_line(segs, total_width));
529 }
530
531 for (line_num, line_text) in &source_lines {
533 let is_error = *line_num == error_line_num;
534
535 let marker = if is_error { "❱" } else { " " };
536 let marker_str = format!("{:<width$}", marker, width = marker_cells);
537
538 let ln_str = format!("{:>width$}", line_num, width = ln_width);
539 let code = truncate_to_width(line_text, code_cells);
540
541 let raw_line = format!(
542 "{}{} {} │ {} ",
543 marker_str,
544 " ".repeat(1),
545 ln_str,
546 code,
547 );
548
549 let inner_w = sub_box_inner.saturating_sub(2); let raw_width = UnicodeWidthStr::width(raw_line.as_str());
553 let pad_w = inner_w.saturating_sub(raw_width);
554 let _padded = if pad_w > 0 {
555 format!("{}{}", raw_line, " ".repeat(pad_w))
556 } else {
557 raw_line
558 };
559
560 let mut segs = Vec::new();
562
563 segs.push(Segment::new(" ".repeat(indent)));
565
566 segs.push(Segment::styled("│".to_string(), border_style.clone()));
568
569 if is_error {
571 segs.push(Segment::styled(
572 marker_str.to_string(),
573 error_mark_style.clone(),
574 ));
575 } else {
576 segs.push(Segment::new(marker_str));
577 }
578
579 let ln_part = format!(" {} ", ln_str);
581 segs.push(Segment::styled(ln_part, line_no_style.clone()));
582
583 segs.push(Segment::styled(" │ ", border_style.clone()));
585
586 segs.push(Segment::new(code.to_string()));
588
589 let after_marker_w = marker_cells + 1 + ln_width + 3 + UnicodeWidthStr::width(code.as_str());
592 let remain = sub_box_inner
593 .saturating_sub(2) .saturating_sub(after_marker_w);
595 if remain > 0 {
596 segs.push(Segment::new(" ".repeat(remain)));
597 }
598
599 segs.push(Segment::styled("│".to_string(), border_style.clone()));
601
602 out_lines.push(outer_content_line(segs, total_width));
603 }
604
605 {
607 let mut segs = Vec::new();
608 segs.push(Segment::styled(
609 format!("{}╰{}╯", " ".repeat(indent), "─".repeat(sub_box_inner)),
610 border_style.clone(),
611 ));
612 out_lines.push(outer_content_line(segs, total_width));
613 }
614 } else if let Some(ref line_text) = frame.line {
615 let indent = 2usize;
617 let mut segs = Vec::new();
618 segs.push(Segment::new(format!(
619 "{}❱ {}",
620 " ".repeat(indent),
621 line_text
622 )));
623 out_lines.push(outer_content_line(segs, total_width));
624 }
625
626 if self.show_locals {
628 if let Some(ref locals) = frame.locals {
629 if !locals.is_empty() {
630 let indent = 2usize;
632 let sub_box_total = content_width.saturating_sub(indent * 2);
633 let sub_box_inner = sub_box_total.saturating_sub(2);
634
635 let header_text = " locals ";
637
638 {
640 let mut segs = Vec::new();
641 segs.push(Segment::styled(
642 format!("{}╭─", " ".repeat(indent)),
643 border_style.clone(),
644 ));
645 segs.push(Segment::styled(
646 header_text.to_string(),
647 locals_header_style.clone(),
648 ));
649 let dash_count = sub_box_inner
650 .saturating_sub(header_text.len() + 1);
651 segs.push(Segment::styled(
652 format!("─{}╮", "─".repeat(dash_count)),
653 border_style.clone(),
654 ));
655 out_lines.push(outer_content_line(segs, total_width));
656 }
657
658 let inner_w = sub_box_inner.saturating_sub(2); let max_shown = self.locals_max_length;
661 let filtered_locals: Vec<(&String, &String)> = locals
662 .iter()
663 .filter(|(k, _)| {
664 if self.locals_hide_dunder
665 && k.starts_with("__")
666 && k.ends_with("__")
667 {
668 return false;
669 }
670 if self.locals_hide_sunder && k.starts_with('_') {
671 return false;
672 }
673 true
674 })
675 .take(max_shown)
676 .collect();
677
678 for (key, val) in &filtered_locals {
679 let max_str_len = self.locals_max_string;
680 let display_val = if val.len() > max_str_len {
681 format!("{}...", &val[..max_str_len])
682 } else {
683 val.to_string()
684 };
685 let line_text = format!("{} = {}", key, display_val);
686 let raw_w = UnicodeWidthStr::width(line_text.as_str());
687 let pad_w = inner_w.saturating_sub(raw_w);
688 let padded = if pad_w > 0 {
689 format!("{}{}", line_text, " ".repeat(pad_w))
690 } else {
691 truncate_to_width(&line_text, inner_w)
692 };
693
694 let mut segs = Vec::new();
695 segs.push(Segment::new(" ".repeat(indent)));
696 segs.push(Segment::styled(
697 "│".to_string(),
698 border_style.clone(),
699 ));
700 segs.push(Segment::new(format!(" {}", padded)));
701 let extra_pad = inner_w.saturating_sub(
703 UnicodeWidthStr::width(padded.as_str()),
704 );
705 if extra_pad > 0 {
706 segs.push(Segment::new(" ".repeat(extra_pad)));
707 }
708 segs.push(Segment::styled(
709 " │".to_string(),
710 border_style.clone(),
711 ));
712 out_lines.push(outer_content_line(segs, total_width));
713 }
714
715 {
717 let mut segs = Vec::new();
718 segs.push(Segment::styled(
719 format!(
720 "{}╰{}╯",
721 " ".repeat(indent),
722 "─".repeat(sub_box_inner),
723 ),
724 border_style.clone(),
725 ));
726 out_lines.push(outer_content_line(segs, total_width));
727 }
728 }
729 }
730 }
731
732 out_lines.push(outer_blank(total_width));
734 }
735
736 if suppressed_count > 0 {
738 let msg = format!(" ... {} frames hidden ...", suppressed_count);
739 let mut segs = Vec::new();
740 segs.push(Segment::styled(msg, Style::new().dim(true)));
741 out_lines.push(outer_content_line(segs, total_width));
742 out_lines.push(outer_blank(total_width));
743 suppressed_count = 0;
744 }
745
746 if let Some(ref exc_type) = stack.exc_type {
748 let exc_value = stack.exc_value.as_deref().unwrap_or("");
749 let msg = if exc_value.is_empty() {
750 format!(" {}", exc_type)
751 } else {
752 format!(" {}: {}", exc_type, exc_value)
753 };
754 let mut segs = Vec::new();
755 segs.push(Segment::styled(msg, error_style.clone()));
756 out_lines.push(outer_content_line(segs, total_width));
757 out_lines.push(outer_blank(total_width));
758 }
759
760 for note in &stack.notes {
762 let mut segs = Vec::new();
763 segs.push(Segment::styled(
764 format!(" note: {}", note),
765 Style::new().italic(true),
766 ));
767 out_lines.push(outer_content_line(segs, total_width));
768 }
769 }
770
771 out_lines.push(bottom_border(total_width));
773
774 RenderResult { lines: out_lines, items: Vec::new() }
775 }
776}
777
778fn truncate_to_width(s: &str, max_width: usize) -> String {
783 if max_width == 0 {
784 return String::new();
785 }
786 let mut w = 0usize;
787 let mut result = String::new();
788 for ch in s.chars() {
789 let cw = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
790 if w + cw > max_width {
791 break;
792 }
793 w += cw;
794 result.push(ch);
795 }
796 result
797}
798
799pub fn install() {
808 std::panic::set_hook(Box::new(|panic_info| {
809 use std::io::Write;
810
811 let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
813 s.to_string()
814 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
815 s.clone()
816 } else {
817 "unknown panic".to_string()
818 };
819
820 let (file, line, col) = if let Some(loc) = panic_info.location() {
822 (
823 loc.file().to_string(),
824 loc.line() as usize,
825 loc.column() as usize,
826 )
827 } else {
828 ("unknown".to_string(), 0, 0)
829 };
830
831 let mut frame = Frame::new(file.clone(), line, "unknown".to_string());
834 frame.line = Some(msg.clone());
835
836 let exc_value = format!("panic at {}:{}:{}", file, line, col);
837 let traceback = Traceback::from_exception("Panic", exc_value, vec![frame])
838 .extra_lines(0);
839
840 let opts = ConsoleOptions {
842 max_width: 120,
843 ..ConsoleOptions::default()
844 };
845 let result = traceback.render(&opts);
846 let ansi = result.to_ansi();
847
848 let _ = writeln!(std::io::stderr(), "{}", ansi);
849 }));
850}
851
852#[cfg(test)]
857mod tests {
858 use super::*;
859
860 #[test]
861 fn test_frame_new() {
862 let f = Frame::new("main.rs", 42, "foo");
863 assert_eq!(f.filename, "main.rs");
864 assert_eq!(f.lineno, 42);
865 assert_eq!(f.name, "foo");
866 assert!(f.line.is_none());
867 assert!(f.locals.is_none());
868 }
869
870 #[test]
871 fn test_frame_builder() {
872 let mut locals = HashMap::new();
873 locals.insert("x".to_string(), "42".to_string());
874
875 let f = Frame::new("lib.rs", 10, "bar")
876 .line("let x = 42;")
877 .locals(locals.clone());
878
879 assert_eq!(f.line.unwrap(), "let x = 42;");
880 assert_eq!(f.locals.unwrap()["x"], "42");
881 }
882
883 #[test]
884 fn test_stack_new() {
885 let s = Stack::new();
886 assert!(s.exc_type.is_none());
887 assert!(s.exc_value.is_none());
888 assert!(!s.is_cause);
889 assert!(s.frames.is_empty());
890 }
891
892 #[test]
893 fn test_stack_builder() {
894 let s = Stack::new()
895 .exc_type("ValueError")
896 .exc_value("bad value")
897 .add_frame(Frame::new("test.rs", 5, "broken"));
898
899 assert_eq!(s.exc_type.unwrap(), "ValueError");
900 assert_eq!(s.exc_value.unwrap(), "bad value");
901 assert_eq!(s.frames.len(), 1);
902 }
903
904 #[test]
905 fn test_trace_new() {
906 let t = Trace::new();
907 assert!(t.stacks.is_empty());
908 }
909
910 #[test]
911 fn test_trace_from_stack() {
912 let s = Stack::new();
913 let t = Trace::from_stack(s);
914 assert_eq!(t.stacks.len(), 1);
915 }
916
917 #[test]
918 fn test_traceback_from_exception() {
919 let tb = Traceback::from_exception(
920 "Error",
921 "something went wrong",
922 vec![
923 Frame::new("main.rs", 1, "main"),
924 Frame::new("lib.rs", 42, "helper"),
925 ],
926 );
927 assert_eq!(tb.trace.stacks.len(), 1);
928 let stack = &tb.trace.stacks[0];
929 assert_eq!(stack.exc_type.as_deref(), Some("Error"));
930 assert_eq!(stack.exc_value.as_deref(), Some("something went wrong"));
931 assert_eq!(stack.frames.len(), 2);
932 }
933
934 #[test]
935 fn test_traceback_builder_methods() {
936 let tb = Traceback::new(Trace::new())
937 .width(100)
938 .code_width(80)
939 .extra_lines(5)
940 .theme("monokai")
941 .word_wrap(true)
942 .show_locals(true)
943 .indent_guides(true)
944 .locals_max_length(20)
945 .locals_max_string(120)
946 .locals_max_depth(10)
947 .locals_hide_dunder(false)
948 .locals_hide_sunder(true)
949 .suppress(vec!["std".to_string()])
950 .max_frames(10);
951
952 assert_eq!(tb.width, Some(100));
953 assert_eq!(tb.code_width, Some(80));
954 assert_eq!(tb.extra_lines, 5);
955 assert!(tb.word_wrap);
956 assert!(tb.show_locals);
957 assert!(!tb.locals_hide_dunder);
958 assert!(tb.locals_hide_sunder);
959 }
960
961 #[test]
962 fn test_truncate_to_width() {
963 assert_eq!(truncate_to_width("hello", 3), "hel");
964 assert_eq!(truncate_to_width("hi", 10), "hi");
965 assert_eq!(truncate_to_width("", 5), "");
966 assert_eq!(truncate_to_width("hello", 0), "");
967 }
968
969 #[test]
970 fn test_is_suppressed() {
971 let suppress = vec!["std".to_string(), "core".to_string()];
972 assert!(is_suppressed(
973 "/rustc/.../library/std/src/panic.rs",
974 &suppress,
975 ));
976 assert!(is_suppressed(
977 "/rustc/.../library/core/src/result.rs",
978 &suppress,
979 ));
980 assert!(!is_suppressed(
981 "/home/user/project/src/main.rs",
982 &suppress,
983 ));
984 }
985
986 #[test]
987 fn test_render_empty_traceback() {
988 let tb = Traceback::new(Trace::new()).width(60);
989 let opts = ConsoleOptions {
990 max_width: 60,
991 ..ConsoleOptions::default()
992 };
993 let result = tb.render(&opts);
994 assert!(!result.lines.is_empty());
996 let ansi = result.to_ansi();
998 assert!(ansi.contains("Traceback"));
999 assert!(ansi.contains("╭"));
1000 assert!(ansi.contains("╰"));
1001 }
1002
1003 #[test]
1004 fn test_render_single_frame() {
1005 let tb = Traceback::from_exception(
1006 "TestError",
1007 "testing",
1008 vec![Frame::new("fake.rs", 10, "test_fn")],
1009 )
1010 .width(80);
1011 let opts = ConsoleOptions {
1012 max_width: 80,
1013 ..ConsoleOptions::default()
1014 };
1015 let result = tb.render(&opts);
1016 let ansi = result.to_ansi();
1017 assert!(ansi.contains("Traceback"));
1018 assert!(ansi.contains("TestError"));
1019 assert!(ansi.contains("testing"));
1020 assert!(ansi.contains("fake.rs"));
1021 }
1022
1023 #[test]
1024 fn test_render_with_locals() {
1025 let mut locals = HashMap::new();
1026 locals.insert("x".to_string(), "42".to_string());
1027 locals.insert("name".to_string(), "hello".to_string());
1028
1029 let tb = Traceback::from_exception(
1030 "Error",
1031 "msg",
1032 vec![Frame::new("test.rs", 5, "func").locals(locals)],
1033 )
1034 .width(80)
1035 .show_locals(true);
1036
1037 let opts = ConsoleOptions {
1038 max_width: 80,
1039 ..ConsoleOptions::default()
1040 };
1041 let result = tb.render(&opts);
1042 let ansi = result.to_ansi();
1043 assert!(ansi.contains("x") || ansi.contains("name"));
1045 }
1046
1047 #[test]
1048 fn test_render_suppressed_frame() {
1049 let tb = Traceback::from_exception(
1050 "Err",
1051 "msg",
1052 vec![
1053 Frame::new("/rustc/lib.rs", 1, "hidden_fn"),
1054 Frame::new("main.rs", 10, "main"),
1055 ],
1056 )
1057 .width(80)
1058 .suppress(vec!["/rustc".to_string()]);
1059
1060 let opts = ConsoleOptions {
1061 max_width: 80,
1062 ..ConsoleOptions::default()
1063 };
1064 let result = tb.render(&opts);
1065 let ansi = result.to_ansi();
1066 assert!(ansi.contains("1 frames hidden") || ansi.contains("frames hidden"));
1067 assert!(ansi.contains("main.rs"));
1068 }
1069
1070 #[test]
1071 fn test_max_frames() {
1072 let tb = Traceback::from_exception(
1073 "Err",
1074 "msg",
1075 vec![
1076 Frame::new("a.rs", 1, "a"),
1077 Frame::new("b.rs", 2, "b"),
1078 Frame::new("c.rs", 3, "c"),
1079 ],
1080 )
1081 .width(80)
1082 .max_frames(2);
1083
1084 let opts = ConsoleOptions {
1085 max_width: 80,
1086 ..ConsoleOptions::default()
1087 };
1088 let result = tb.render(&opts);
1089 let ansi = result.to_ansi();
1090 assert!(ansi.contains("frames hidden") || ansi.contains("hidden"));
1092 }
1093
1094 #[test]
1095 fn test_theme_style_resolution() {
1096 let style = theme_style(theme::names::TRACEBACK_BORDER);
1097 assert!(!style.is_plain());
1099 }
1100
1101 #[test]
1102 fn test_locals_filtering_dunder() {
1103 let mut locals = HashMap::new();
1104 locals.insert("__private__".to_string(), "secret".to_string());
1105 locals.insert("normal".to_string(), "visible".to_string());
1106
1107 let tb = Traceback::from_exception("E", "msg", vec![
1108 Frame::new("t.rs", 1, "f").locals(locals),
1109 ])
1110 .width(80)
1111 .show_locals(true)
1112 .locals_hide_dunder(true);
1113
1114 let opts = ConsoleOptions {
1115 max_width: 80,
1116 ..ConsoleOptions::default()
1117 };
1118 let result = tb.render(&opts);
1119 let ansi = result.to_ansi();
1120
1121 let _has_private = ansi.contains("__private__");
1123 let has_normal = ansi.contains("normal");
1124
1125 assert!(has_normal);
1128 }
1129
1130 #[test]
1131 fn test_locals_filtering_sunder() {
1132 let mut locals = HashMap::new();
1133 locals.insert("_hidden".to_string(), "invisible".to_string());
1134 locals.insert("visible".to_string(), "yes".to_string());
1135
1136 let tb = Traceback::from_exception("E", "msg", vec![
1137 Frame::new("t.rs", 1, "f").locals(locals),
1138 ])
1139 .width(80)
1140 .show_locals(true)
1141 .locals_hide_sunder(true);
1142
1143 let opts = ConsoleOptions {
1144 max_width: 80,
1145 ..ConsoleOptions::default()
1146 };
1147 let result = tb.render(&opts);
1148 let ansi = result.to_ansi();
1149
1150 assert!(!ansi.contains("_hidden"));
1152 assert!(ansi.contains("visible"));
1153 }
1154
1155 #[test]
1156 fn test_install_hook() {
1157 install();
1159 let _ = std::panic::take_hook();
1161 }
1162
1163 #[test]
1164 fn test_multiple_stacks() {
1165 let mut stack1 = Stack::new();
1166 stack1.exc_type = Some("IOError".to_string());
1167 stack1.exc_value = Some("file not found".to_string());
1168 stack1.frames.push(Frame::new("io.rs", 10, "read_file"));
1169
1170 let mut stack2 = Stack::new();
1171 stack2.exc_type = Some("ValueError".to_string());
1172 stack2.exc_value = Some("bad data".to_string());
1173 stack2.is_cause = true;
1174 stack2.frames.push(Frame::new("main.rs", 20, "process"));
1175
1176 let trace = Trace {
1177 stacks: vec![stack1, stack2],
1178 };
1179
1180 let tb = Traceback::new(trace).width(80);
1181 let opts = ConsoleOptions {
1182 max_width: 80,
1183 ..ConsoleOptions::default()
1184 };
1185 let result = tb.render(&opts);
1186 let ansi = result.to_ansi();
1187
1188 assert!(ansi.contains("IOError"));
1189 assert!(ansi.contains("ValueError"));
1190 }
1191}