oopsie_core/erased/
backtrace.rs1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
9#[non_exhaustive]
10pub struct ErasedBacktrace {
11 frames: Box<[ErasedFrame]>,
12}
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct ErasedFrame {
18 name: Option<Box<str>>,
19 filename: Option<Box<str>>,
20 line: Option<u32>,
21 column: Option<u32>,
22}
23
24impl ErasedFrame {
25 #[must_use]
27 #[inline]
28 pub fn name(&self) -> Option<&str> {
29 self.name.as_deref()
30 }
31
32 #[must_use]
34 #[inline]
35 pub fn filename(&self) -> Option<&str> {
36 self.filename.as_deref()
37 }
38
39 #[must_use]
41 #[inline]
42 pub const fn line(&self) -> Option<u32> {
43 self.line
44 }
45
46 #[must_use]
48 #[inline]
49 pub const fn column(&self) -> Option<u32> {
50 self.column
51 }
52}
53
54impl ErasedBacktrace {
55 #[must_use]
62 pub fn from_backtrace(bt: &crate::Backtrace) -> Self {
63 bt.resolve();
64
65 let mut frames = Vec::new();
66 for frame in bt.frames() {
67 let symbols = frame.symbols();
68 if symbols.is_empty() {
71 frames.push(ErasedFrame {
72 name: None,
73 filename: None,
74 line: None,
75 column: None,
76 });
77 }
78 frames.extend(symbols.iter().map(|sym| ErasedFrame {
79 name: sym.name().map(|n| n.to_string().into_boxed_str()),
80 filename: sym.filename().map(|p| p.to_string_lossy().into()),
81 line: sym.lineno(),
82 column: sym.colno(),
83 }));
84 }
85 Self {
86 frames: frames.into(),
87 }
88 }
89
90 #[must_use]
92 #[inline]
93 pub fn frames(&self) -> &[ErasedFrame] {
94 &self.frames
95 }
96}
97
98impl From<&crate::Backtrace> for ErasedBacktrace {
99 #[inline]
100 fn from(bt: &crate::Backtrace) -> Self {
101 Self::from_backtrace(bt)
102 }
103}
104impl From<crate::Backtrace> for ErasedBacktrace {
105 #[inline]
106 fn from(bt: crate::Backtrace) -> Self {
107 Self::from_backtrace(&bt)
108 }
109}
110
111impl fmt::Display for ErasedBacktrace {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 for (i, frame) in self.frames.iter().enumerate() {
114 write!(f, "{:>3}: ", i + 1)?;
115 if let Some(name) = &frame.name {
116 writeln!(f, "{name}")?;
117 } else {
118 writeln!(f, "<unknown>")?;
119 }
120 if let Some(filename) = &frame.filename {
121 write!(f, " at {filename}")?;
122 if let Some(line) = frame.line {
123 write!(f, ":{line}")?;
124 if let Some(col) = frame.column {
125 write!(f, ":{col}")?;
126 }
127 }
128 writeln!(f)?;
129 }
130 }
131 Ok(())
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn frame(
140 name: Option<&str>,
141 filename: Option<&str>,
142 line: Option<u32>,
143 column: Option<u32>,
144 ) -> ErasedFrame {
145 ErasedFrame {
146 name: name.map(|s| s.to_owned().into_boxed_str()),
147 filename: filename.map(Into::into),
148 line,
149 column,
150 }
151 }
152
153 fn single(f: ErasedFrame) -> ErasedBacktrace {
154 ErasedBacktrace {
155 frames: Box::from([f]),
156 }
157 }
158
159 #[test]
160 fn display_unknown_frame_without_location() {
161 let bt = single(frame(None, None, None, None));
163 assert_eq!(bt.to_string(), " 1: <unknown>\n");
164 }
165
166 #[test]
167 fn display_full_location_with_column() {
168 let bt = single(frame(Some("a::b"), Some("/x/y.rs"), Some(12), Some(3)));
169 assert_eq!(bt.to_string(), " 1: a::b\n at /x/y.rs:12:3\n");
170 }
171
172 #[test]
173 fn display_location_with_line_no_column() {
174 let bt = single(frame(Some("a::b"), Some("/x/y.rs"), Some(12), None));
175 assert_eq!(bt.to_string(), " 1: a::b\n at /x/y.rs:12\n");
176 }
177
178 #[test]
179 fn display_location_with_filename_no_line() {
180 let bt = single(frame(Some("a::b"), Some("/x/y.rs"), None, None));
182 assert_eq!(bt.to_string(), " 1: a::b\n at /x/y.rs\n");
183 }
184
185 #[test]
186 fn from_backtrace_retains_raw_internal_frames() {
187 crate::set_rust_backtrace_override(crate::RustBacktrace::Enabled);
188 let bt = <crate::Backtrace as crate::Capturable>::capture();
189 crate::clear_rust_backtrace_override();
190
191 let erased = ErasedBacktrace::from_backtrace(&bt);
192
193 assert!(
197 erased.frames().iter().any(|fr| {
198 fr.filename
199 .as_deref()
200 .is_some_and(|f| f.starts_with(crate::__private::CORE_SRC_PATH))
201 }),
202 "from_backtrace should retain raw internal frames, not strip them at capture"
203 );
204 }
205
206 #[test]
207 fn from_backtrace_keeps_at_least_one_erased_frame_per_source_frame() {
208 crate::set_rust_backtrace_override(crate::RustBacktrace::Enabled);
209 let bt = <crate::Backtrace as crate::Capturable>::capture();
210 crate::clear_rust_backtrace_override();
211
212 let erased = ErasedBacktrace::from_backtrace(&bt);
213 assert!(
214 erased.frames().len() >= bt.frames().len(),
215 "every source frame must survive erasure (unresolved ones as placeholders): \
216 {} erased < {} source",
217 erased.frames().len(),
218 bt.frames().len()
219 );
220 }
221}