1use super::{common::deserialize_string_or_number, StackFrame};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Serialize, Deserialize)]
9pub struct ProcessedCrash {
10 pub uuid: String,
11 #[serde(default)]
12 pub signature: Option<String>,
13 #[serde(default)]
14 pub product: Option<String>,
15 #[serde(default)]
16 pub version: Option<String>,
17 #[serde(default)]
18 pub os_name: Option<String>,
19 #[serde(default, deserialize_with = "deserialize_string_or_number")]
20 pub build: Option<String>,
21 #[serde(default)]
22 pub release_channel: Option<String>,
23 #[serde(default)]
24 pub os_version: Option<String>,
25
26 #[serde(default)]
27 pub crash_info: Option<CrashInfo>,
28 #[serde(default)]
29 pub moz_crash_reason: Option<String>,
30 #[serde(default)]
31 pub abort_message: Option<String>,
32
33 #[serde(default)]
34 pub android_model: Option<String>,
35 #[serde(default)]
36 pub android_version: Option<String>,
37
38 #[serde(default)]
39 pub crashing_thread: Option<usize>,
40 #[serde(default)]
41 pub threads: Option<Vec<Thread>>,
42 #[serde(default)]
43 pub json_dump: Option<serde_json::Value>,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct CrashInfo {
48 #[serde(rename = "type")]
49 pub crash_type: Option<String>,
50 pub address: Option<String>,
51 pub crashing_thread: Option<usize>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55pub struct Thread {
56 pub thread: Option<usize>,
57 pub thread_name: Option<String>,
58 pub frames: Vec<StackFrame>,
59}
60
61#[derive(Debug, Clone)]
62pub struct ThreadSummary {
63 pub thread_index: usize,
64 pub thread_name: Option<String>,
65 pub frames: Vec<StackFrame>,
66 pub is_crashing: bool,
67}
68
69#[derive(Debug)]
70pub struct CrashSummary {
71 pub crash_id: String,
72 pub signature: String,
73 pub reason: Option<String>,
74 pub address: Option<String>,
75 pub moz_crash_reason: Option<String>,
76 pub abort_message: Option<String>,
77
78 pub product: String,
79 pub version: String,
80 pub build_id: Option<String>,
81 pub release_channel: Option<String>,
82 pub platform: String,
83
84 pub android_version: Option<String>,
85 pub android_model: Option<String>,
86
87 pub crashing_thread_name: Option<String>,
88 pub frames: Vec<StackFrame>,
89 pub all_threads: Vec<ThreadSummary>,
90}
91
92impl ProcessedCrash {
93 pub fn to_summary(&self, depth: usize, all_threads: bool) -> CrashSummary {
94 let crashing_thread_idx = self
95 .crashing_thread
96 .or_else(|| self.crash_info.as_ref().and_then(|ci| ci.crashing_thread))
97 .or_else(|| {
98 self.json_dump.as_ref().and_then(|jd| {
99 jd.get("crashing_thread")
100 .and_then(|v| v.as_u64())
101 .map(|v| v as usize)
102 })
103 });
104
105 let json_dump_threads: Option<Vec<Thread>> = self
106 .json_dump
107 .as_ref()
108 .and_then(|jd| jd.get("threads"))
109 .and_then(|t| serde_json::from_value(t.clone()).ok());
110
111 let threads_data = self.threads.as_ref().or(json_dump_threads.as_ref());
112
113 let (thread_name, frames, thread_summaries) = if let Some(threads) = threads_data {
114 let mut all_thread_summaries = Vec::new();
115
116 if all_threads {
117 for (idx, thread) in threads.iter().enumerate() {
118 let frames: Vec<StackFrame> =
119 thread.frames.iter().take(depth).cloned().collect();
120 all_thread_summaries.push(ThreadSummary {
121 thread_index: idx,
122 thread_name: thread.thread_name.clone(),
123 frames,
124 is_crashing: Some(idx) == crashing_thread_idx,
125 });
126 }
127 }
128
129 if let Some(idx) = crashing_thread_idx {
130 if let Some(thread) = threads.get(idx) {
131 let frames: Vec<StackFrame> =
132 thread.frames.iter().take(depth).cloned().collect();
133 (thread.thread_name.clone(), frames, all_thread_summaries)
134 } else {
135 (None, Vec::new(), all_thread_summaries)
136 }
137 } else {
138 (None, Vec::new(), all_thread_summaries)
139 }
140 } else {
141 (None, Vec::new(), Vec::new())
142 };
143
144 let json_dump_crash_info: Option<CrashInfo> = self
145 .json_dump
146 .as_ref()
147 .and_then(|jd| jd.get("crash_info"))
148 .and_then(|ci| serde_json::from_value(ci.clone()).ok());
149
150 let crash_info = self.crash_info.as_ref().or(json_dump_crash_info.as_ref());
151
152 CrashSummary {
153 crash_id: self.uuid.clone(),
154 signature: self
155 .signature
156 .clone()
157 .unwrap_or_else(|| "Unknown".to_string()),
158 reason: crash_info.and_then(|ci| ci.crash_type.clone()),
159 address: crash_info.and_then(|ci| ci.address.clone()),
160 moz_crash_reason: self.moz_crash_reason.clone(),
161 abort_message: self.abort_message.clone(),
162 product: self
163 .product
164 .clone()
165 .unwrap_or_else(|| "Unknown".to_string()),
166 version: self
167 .version
168 .clone()
169 .unwrap_or_else(|| "Unknown".to_string()),
170 build_id: self.build.clone(),
171 release_channel: self.release_channel.clone(),
172 platform: format!(
173 "{}{}",
174 self.os_name.as_deref().unwrap_or("Unknown"),
175 self.os_version
176 .as_ref()
177 .map(|v| format!(" {}", v))
178 .unwrap_or_default()
179 ),
180 android_version: self.android_version.clone(),
181 android_model: self.android_model.clone(),
182 crashing_thread_name: thread_name,
183 frames,
184 all_threads: thread_summaries,
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn sample_crash_json() -> &'static str {
194 r#"{
195 "uuid": "247653e8-7a18-4836-97d1-42a720260120",
196 "signature": "mozilla::AudioDecoderInputTrack::EnsureTimeStretcher",
197 "product": "Fenix",
198 "version": "147.0.1",
199 "os_name": "Android",
200 "os_version": "36",
201 "crashing_thread": 1,
202 "moz_crash_reason": "MOZ_RELEASE_ASSERT(mTimeStretcher->Init())",
203 "crash_info": {
204 "type": "SIGSEGV",
205 "address": "0x0",
206 "crashing_thread": 1
207 },
208 "threads": [
209 {
210 "thread": 0,
211 "thread_name": "MainThread",
212 "frames": [
213 {"frame": 0, "function": "main", "file": "main.cpp", "line": 10}
214 ]
215 },
216 {
217 "thread": 1,
218 "thread_name": "GraphRunner",
219 "frames": [
220 {"frame": 0, "function": "EnsureTimeStretcher", "file": "AudioDecoderInputTrack.cpp", "line": 624},
221 {"frame": 1, "function": "AppendData", "file": "AudioDecoderInputTrack.cpp", "line": 423}
222 ]
223 }
224 ]
225 }"#
226 }
227
228 #[test]
229 fn test_deserialize_processed_crash() {
230 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
231 assert_eq!(crash.uuid, "247653e8-7a18-4836-97d1-42a720260120");
232 assert_eq!(
233 crash.signature,
234 Some("mozilla::AudioDecoderInputTrack::EnsureTimeStretcher".to_string())
235 );
236 assert_eq!(crash.product, Some("Fenix".to_string()));
237 assert_eq!(crash.version, Some("147.0.1".to_string()));
238 assert_eq!(crash.crashing_thread, Some(1));
239 }
240
241 #[test]
242 fn test_to_summary_basic() {
243 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
244 let summary = crash.to_summary(10, false);
245
246 assert_eq!(summary.crash_id, "247653e8-7a18-4836-97d1-42a720260120");
247 assert_eq!(
248 summary.signature,
249 "mozilla::AudioDecoderInputTrack::EnsureTimeStretcher"
250 );
251 assert_eq!(summary.product, "Fenix");
252 assert_eq!(summary.version, "147.0.1");
253 assert_eq!(summary.reason, Some("SIGSEGV".to_string()));
254 assert_eq!(summary.address, Some("0x0".to_string()));
255 assert_eq!(
256 summary.moz_crash_reason,
257 Some("MOZ_RELEASE_ASSERT(mTimeStretcher->Init())".to_string())
258 );
259 }
260
261 #[test]
262 fn test_to_summary_crashing_thread_frames() {
263 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
264 let summary = crash.to_summary(10, false);
265
266 assert_eq!(
267 summary.crashing_thread_name,
268 Some("GraphRunner".to_string())
269 );
270 assert_eq!(summary.frames.len(), 2);
271 assert_eq!(
272 summary.frames[0].function,
273 Some("EnsureTimeStretcher".to_string())
274 );
275 }
276
277 #[test]
278 fn test_to_summary_depth_limit() {
279 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
280 let summary = crash.to_summary(1, false);
281
282 assert_eq!(summary.frames.len(), 1);
283 assert_eq!(
284 summary.frames[0].function,
285 Some("EnsureTimeStretcher".to_string())
286 );
287 }
288
289 #[test]
290 fn test_to_summary_all_threads() {
291 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
292 let summary = crash.to_summary(10, true);
293
294 assert_eq!(summary.all_threads.len(), 2);
295 assert!(!summary.all_threads[0].is_crashing);
296 assert!(summary.all_threads[1].is_crashing);
297 assert_eq!(
298 summary.all_threads[0].thread_name,
299 Some("MainThread".to_string())
300 );
301 assert_eq!(
302 summary.all_threads[1].thread_name,
303 Some("GraphRunner".to_string())
304 );
305 }
306
307 #[test]
308 fn test_crashing_thread_from_crash_info() {
309 let json = r#"{
311 "uuid": "test-crash",
312 "crash_info": {
313 "type": "SIGSEGV",
314 "crashing_thread": 0
315 },
316 "threads": [
317 {"thread": 0, "thread_name": "Main", "frames": [{"frame": 0, "function": "foo"}]}
318 ]
319 }"#;
320 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
321 let summary = crash.to_summary(10, false);
322
323 assert_eq!(summary.crashing_thread_name, Some("Main".to_string()));
324 }
325
326 #[test]
327 fn test_crashing_thread_from_json_dump() {
328 let json = r#"{
330 "uuid": "test-crash",
331 "json_dump": {
332 "crashing_thread": 0,
333 "threads": [
334 {"thread": 0, "thread_name": "DumpThread", "frames": [{"frame": 0, "function": "bar"}]}
335 ]
336 }
337 }"#;
338 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
339 let summary = crash.to_summary(10, false);
340
341 assert_eq!(summary.crashing_thread_name, Some("DumpThread".to_string()));
342 }
343
344 #[test]
345 fn test_missing_optional_fields() {
346 let json = r#"{"uuid": "minimal-crash"}"#;
347 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
348 let summary = crash.to_summary(10, false);
349
350 assert_eq!(summary.crash_id, "minimal-crash");
351 assert_eq!(summary.signature, "Unknown");
352 assert_eq!(summary.product, "Unknown");
353 assert!(summary.frames.is_empty());
354 }
355}