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::{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        // Test fallback to crash_info.crashing_thread when crashing_thread is not set
310        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        // Test fallback to json_dump.crashing_thread
329        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}