1use std::error::Error;
14
15use crate::console::{Console, ConsoleOptions};
16use crate::panel::Panel;
17use crate::protocol::Renderable;
18use crate::r#box::HEAVY;
19use crate::segment::Segment;
20use crate::style::Style;
21use crate::text::Text;
22
23pub struct Traceback {
26 message: String,
27 causes: Vec<String>,
28}
29
30impl Traceback {
31 pub fn new(error: &dyn Error) -> Self {
33 let mut causes = Vec::new();
34 let mut source = error.source();
35 while let Some(err) = source {
36 causes.push(err.to_string());
37 source = err.source();
38 }
39 Traceback {
40 message: error.to_string(),
41 causes,
42 }
43 }
44
45 pub fn from_message(message: impl Into<String>) -> Self {
47 Traceback {
48 message: message.into(),
49 causes: Vec::new(),
50 }
51 }
52
53 fn content(&self) -> Text {
55 let error_style = Style::parse("bold red").expect("valid style");
56 let label_style = Style::parse("dim").expect("valid style");
57 let cause_style = Style::parse("red").expect("valid style");
58
59 let mut text = Text::new("");
60 text.append(&self.message, Some(error_style.into()));
61 if !self.causes.is_empty() {
62 text.append("\n\nCaused by:", Some(label_style.into()));
63 for (index, cause) in self.causes.iter().enumerate() {
64 text.append(&format!("\n {}: ", index + 1), None);
65 text.append(cause, Some(cause_style.clone().into()));
66 }
67 }
68 text
69 }
70
71 fn panel(&self) -> Panel {
72 Panel::new(Box::new(self.content()))
73 .box_set(HEAVY)
74 .border_style(Style::parse("red").expect("valid style"))
75 .title("Traceback (most recent call last)")
76 }
77}
78
79impl Renderable for Traceback {
80 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
81 self.panel().rich_render(console, options)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::color::ColorSystem;
89 use std::fmt;
90
91 #[derive(Debug)]
92 struct Inner;
93 impl fmt::Display for Inner {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(f, "disk full")
96 }
97 }
98 impl Error for Inner {}
99
100 #[derive(Debug)]
101 struct Outer;
102 static INNER: Inner = Inner;
103 impl fmt::Display for Outer {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "failed to save file")
106 }
107 }
108 impl Error for Outer {
109 fn source(&self) -> Option<&(dyn Error + 'static)> {
110 Some(&INNER)
111 }
112 }
113
114 fn render(tb: &Traceback) -> String {
115 Console::builder()
116 .force_terminal(true)
117 .color_system(Some(ColorSystem::Truecolor))
118 .width(40)
119 .no_color(false)
120 .build()
121 .render_to_string(tb)
122 }
123
124 #[test]
125 fn renders_error_chain() {
126 let out = render(&Traceback::new(&Outer));
127 assert!(out.contains("Traceback (most recent call last)"));
128 assert!(out.contains("failed to save file"));
129 assert!(out.contains("Caused by:"));
130 assert!(out.contains("disk full"));
131 assert!(out.contains("\x1b[31m"), "expected red border/message");
133 }
134
135 #[test]
136 fn from_message_has_no_chain() {
137 let out = render(&Traceback::from_message("something broke"));
138 assert!(out.contains("something broke"));
139 assert!(!out.contains("Caused by:"));
140 }
141}