Skip to main content

socorro_cli/models/
processed_crash.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use super::{ModuleInfo, StackFrame, common::deserialize_string_or_number};
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    pub modules: Vec<ModuleInfo>,
91}
92
93impl ProcessedCrash {
94    pub fn to_summary(&self, depth: usize, all_threads: bool) -> CrashSummary {
95        let crashing_thread_idx = self
96            .crashing_thread
97            .or_else(|| self.crash_info.as_ref().and_then(|ci| ci.crashing_thread))
98            .or_else(|| {
99                self.json_dump.as_ref().and_then(|jd| {
100                    jd.get("crashing_thread")
101                        .and_then(|v| v.as_u64())
102                        .map(|v| v as usize)
103                })
104            });
105
106        let json_dump_threads: Option<Vec<Thread>> = self
107            .json_dump
108            .as_ref()
109            .and_then(|jd| jd.get("threads"))
110            .and_then(|t| serde_json::from_value(t.clone()).ok());
111
112        let threads_data = self.threads.as_ref().or(json_dump_threads.as_ref());
113
114        let (thread_name, frames, thread_summaries) = if let Some(threads) = threads_data {
115            let mut all_thread_summaries = Vec::new();
116
117            if all_threads {
118                for (idx, thread) in threads.iter().enumerate() {
119                    let frames: Vec<StackFrame> =
120                        thread.frames.iter().take(depth).cloned().collect();
121                    all_thread_summaries.push(ThreadSummary {
122                        thread_index: idx,
123                        thread_name: thread.thread_name.clone(),
124                        frames,
125                        is_crashing: Some(idx) == crashing_thread_idx,
126                    });
127                }
128            }
129
130            if let Some(idx) = crashing_thread_idx {
131                if let Some(thread) = threads.get(idx) {
132                    let frames: Vec<StackFrame> =
133                        thread.frames.iter().take(depth).cloned().collect();
134                    (thread.thread_name.clone(), frames, all_thread_summaries)
135                } else {
136                    (None, Vec::new(), all_thread_summaries)
137                }
138            } else {
139                (None, Vec::new(), all_thread_summaries)
140            }
141        } else {
142            (None, Vec::new(), Vec::new())
143        };
144
145        let modules: Vec<ModuleInfo> = self
146            .json_dump
147            .as_ref()
148            .and_then(|jd| jd.get("modules"))
149            .and_then(|m| serde_json::from_value(m.clone()).ok())
150            .unwrap_or_default();
151
152        let json_dump_crash_info: Option<CrashInfo> = self
153            .json_dump
154            .as_ref()
155            .and_then(|jd| jd.get("crash_info"))
156            .and_then(|ci| serde_json::from_value(ci.clone()).ok());
157
158        let crash_info = self.crash_info.as_ref().or(json_dump_crash_info.as_ref());
159
160        CrashSummary {
161            crash_id: self.uuid.clone(),
162            signature: self
163                .signature
164                .clone()
165                .unwrap_or_else(|| "Unknown".to_string()),
166            reason: crash_info.and_then(|ci| ci.crash_type.clone()),
167            address: crash_info.and_then(|ci| ci.address.clone()),
168            moz_crash_reason: self.moz_crash_reason.clone(),
169            abort_message: self.abort_message.clone(),
170            product: self
171                .product
172                .clone()
173                .unwrap_or_else(|| "Unknown".to_string()),
174            version: self
175                .version
176                .clone()
177                .unwrap_or_else(|| "Unknown".to_string()),
178            build_id: self.build.clone(),
179            release_channel: self.release_channel.clone(),
180            platform: format!(
181                "{}{}",
182                self.os_name.as_deref().unwrap_or("Unknown"),
183                self.os_version
184                    .as_ref()
185                    .map(|v| format!(" {}", v))
186                    .unwrap_or_default()
187            ),
188            android_version: self.android_version.clone(),
189            android_model: self.android_model.clone(),
190            crashing_thread_name: thread_name,
191            frames,
192            all_threads: thread_summaries,
193            modules,
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    fn sample_crash_json() -> &'static str {
203        r#"{
204            "uuid": "247653e8-7a18-4836-97d1-42a720260120",
205            "signature": "mozilla::AudioDecoderInputTrack::EnsureTimeStretcher",
206            "product": "Fenix",
207            "version": "147.0.1",
208            "os_name": "Android",
209            "os_version": "36",
210            "crashing_thread": 1,
211            "moz_crash_reason": "MOZ_RELEASE_ASSERT(mTimeStretcher->Init())",
212            "crash_info": {
213                "type": "SIGSEGV",
214                "address": "0x0",
215                "crashing_thread": 1
216            },
217            "json_dump": {
218                "modules": [
219                    {
220                        "filename": "xul.dll",
221                        "debug_file": "xul.pdb",
222                        "debug_id": "F51BCD2A59EB2A194C4C44205044422E1",
223                        "code_id": "69934c4ba31f000",
224                        "version": "148.0.0.3"
225                    },
226                    {
227                        "filename": "ntdll.dll",
228                        "debug_file": "ntdll.pdb",
229                        "debug_id": "180BF1B90AA75697D0EFEA5E5630AC7E1",
230                        "code_id": "7ec9c15d1f8000",
231                        "version": "6.2.19041.6456"
232                    },
233                    {
234                        "filename": "mozglue.dll",
235                        "debug_file": "mozglue.pdb",
236                        "debug_id": "AABBCCDD11223344",
237                        "code_id": "abc123",
238                        "version": "148.0"
239                    }
240                ]
241            },
242            "threads": [
243                {
244                    "thread": 0,
245                    "thread_name": "MainThread",
246                    "frames": [
247                        {"frame": 0, "function": "main", "file": "main.cpp", "line": 10, "module": "xul.dll"}
248                    ]
249                },
250                {
251                    "thread": 1,
252                    "thread_name": "GraphRunner",
253                    "frames": [
254                        {"frame": 0, "function": "EnsureTimeStretcher", "file": "AudioDecoderInputTrack.cpp", "line": 624, "module": "xul.dll"},
255                        {"frame": 1, "function": "AppendData", "file": "AudioDecoderInputTrack.cpp", "line": 423, "module": "ntdll.dll"}
256                    ]
257                }
258            ]
259        }"#
260    }
261
262    #[test]
263    fn test_deserialize_processed_crash() {
264        let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
265        assert_eq!(crash.uuid, "247653e8-7a18-4836-97d1-42a720260120");
266        assert_eq!(
267            crash.signature,
268            Some("mozilla::AudioDecoderInputTrack::EnsureTimeStretcher".to_string())
269        );
270        assert_eq!(crash.product, Some("Fenix".to_string()));
271        assert_eq!(crash.version, Some("147.0.1".to_string()));
272        assert_eq!(crash.crashing_thread, Some(1));
273    }
274
275    #[test]
276    fn test_to_summary_basic() {
277        let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
278        let summary = crash.to_summary(10, false);
279
280        assert_eq!(summary.crash_id, "247653e8-7a18-4836-97d1-42a720260120");
281        assert_eq!(
282            summary.signature,
283            "mozilla::AudioDecoderInputTrack::EnsureTimeStretcher"
284        );
285        assert_eq!(summary.product, "Fenix");
286        assert_eq!(summary.version, "147.0.1");
287        assert_eq!(summary.reason, Some("SIGSEGV".to_string()));
288        assert_eq!(summary.address, Some("0x0".to_string()));
289        assert_eq!(
290            summary.moz_crash_reason,
291            Some("MOZ_RELEASE_ASSERT(mTimeStretcher->Init())".to_string())
292        );
293    }
294
295    #[test]
296    fn test_to_summary_crashing_thread_frames() {
297        let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
298        let summary = crash.to_summary(10, false);
299
300        assert_eq!(
301            summary.crashing_thread_name,
302            Some("GraphRunner".to_string())
303        );
304        assert_eq!(summary.frames.len(), 2);
305        assert_eq!(
306            summary.frames[0].function,
307            Some("EnsureTimeStretcher".to_string())
308        );
309    }
310
311    #[test]
312    fn test_to_summary_depth_limit() {
313        let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
314        let summary = crash.to_summary(1, false);
315
316        assert_eq!(summary.frames.len(), 1);
317        assert_eq!(
318            summary.frames[0].function,
319            Some("EnsureTimeStretcher".to_string())
320        );
321    }
322
323    #[test]
324    fn test_to_summary_all_threads() {
325        let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
326        let summary = crash.to_summary(10, true);
327
328        assert_eq!(summary.all_threads.len(), 2);
329        assert!(!summary.all_threads[0].is_crashing);
330        assert!(summary.all_threads[1].is_crashing);
331        assert_eq!(
332            summary.all_threads[0].thread_name,
333            Some("MainThread".to_string())
334        );
335        assert_eq!(
336            summary.all_threads[1].thread_name,
337            Some("GraphRunner".to_string())
338        );
339    }
340
341    #[test]
342    fn test_crashing_thread_from_crash_info() {
343        // Test fallback to crash_info.crashing_thread when crashing_thread is not set
344        let json = r#"{
345            "uuid": "test-crash",
346            "crash_info": {
347                "type": "SIGSEGV",
348                "crashing_thread": 0
349            },
350            "threads": [
351                {"thread": 0, "thread_name": "Main", "frames": [{"frame": 0, "function": "foo"}]}
352            ]
353        }"#;
354        let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
355        let summary = crash.to_summary(10, false);
356
357        assert_eq!(summary.crashing_thread_name, Some("Main".to_string()));
358    }
359
360    #[test]
361    fn test_crashing_thread_from_json_dump() {
362        // Test fallback to json_dump.crashing_thread
363        let json = r#"{
364            "uuid": "test-crash",
365            "json_dump": {
366                "crashing_thread": 0,
367                "threads": [
368                    {"thread": 0, "thread_name": "DumpThread", "frames": [{"frame": 0, "function": "bar"}]}
369                ]
370            }
371        }"#;
372        let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
373        let summary = crash.to_summary(10, false);
374
375        assert_eq!(summary.crashing_thread_name, Some("DumpThread".to_string()));
376    }
377
378    #[test]
379    fn test_missing_optional_fields() {
380        let json = r#"{"uuid": "minimal-crash"}"#;
381        let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
382        let summary = crash.to_summary(10, false);
383
384        assert_eq!(summary.crash_id, "minimal-crash");
385        assert_eq!(summary.signature, "Unknown");
386        assert_eq!(summary.product, "Unknown");
387        assert!(summary.frames.is_empty());
388        assert!(summary.modules.is_empty());
389    }
390
391    #[test]
392    fn test_to_summary_extracts_modules() {
393        let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
394        let summary = crash.to_summary(10, false);
395
396        assert_eq!(summary.modules.len(), 3);
397        assert_eq!(summary.modules[0].filename, "xul.dll");
398        assert_eq!(summary.modules[0].debug_file, Some("xul.pdb".to_string()));
399        assert_eq!(
400            summary.modules[0].debug_id,
401            Some("F51BCD2A59EB2A194C4C44205044422E1".to_string())
402        );
403        assert_eq!(
404            summary.modules[0].code_id,
405            Some("69934c4ba31f000".to_string())
406        );
407        assert_eq!(summary.modules[0].version, Some("148.0.0.3".to_string()));
408    }
409
410    #[test]
411    fn test_to_summary_modules_missing_json_dump() {
412        let json = r#"{
413            "uuid": "no-json-dump",
414            "threads": [
415                {"thread": 0, "frames": [{"frame": 0, "function": "foo"}]}
416            ]
417        }"#;
418        let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
419        let summary = crash.to_summary(10, false);
420
421        assert!(summary.modules.is_empty());
422    }
423
424    #[test]
425    fn test_to_summary_modules_missing_modules_key() {
426        let json = r#"{
427            "uuid": "no-modules",
428            "json_dump": {
429                "crashing_thread": 0,
430                "threads": [
431                    {"thread": 0, "frames": [{"frame": 0, "function": "foo"}]}
432                ]
433            }
434        }"#;
435        let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
436        let summary = crash.to_summary(10, false);
437
438        assert!(summary.modules.is_empty());
439    }
440
441    #[test]
442    fn test_to_summary_modules_optional_fields() {
443        let json = r#"{
444            "uuid": "partial-modules",
445            "json_dump": {
446                "modules": [
447                    {"filename": "bare.dll"}
448                ]
449            }
450        }"#;
451        let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
452        let summary = crash.to_summary(10, false);
453
454        assert_eq!(summary.modules.len(), 1);
455        assert_eq!(summary.modules[0].filename, "bare.dll");
456        assert!(summary.modules[0].debug_file.is_none());
457        assert!(summary.modules[0].debug_id.is_none());
458        assert!(summary.modules[0].code_id.is_none());
459        assert!(summary.modules[0].version.is_none());
460    }
461}