Merge lp:~ev/apport/grouped_reports into lp:~apport-hackers/apport/trunk
- grouped_reports
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~ev/apport/grouped_reports |
Merge into: | lp:~apport-hackers/apport/trunk |
Diff against target: |
1323 lines (+404/-228) 7 files modified
apport/ui.py (+195/-124) gtk/apport-gtk (+79/-38) gtk/apport-gtk.ui (+16/-22) kde/apport-kde (+1/-1) test/test_backend_apt_dpkg.py (+1/-1) test/test_ui.py (+104/-37) test/test_ui_gtk.py (+8/-5) |
To merge this branch: | bzr merge lp:~ev/apport/grouped_reports |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Apport upstream developers | Pending | ||
Review via email: mp+144591@code.launchpad.net |
Commit message
Description of the change
I would like to clean up the tests a little more (and have more of a think about how we can avoid loading all the reports so many times), but I think this is basically ready for review.
This branch makes two major changes to apport.
The first introduces the concept of grouped or silent error reports. To quote Matthew,
"When an error occurs that's unlikely to affect you noticably, you won't be notified separately. Instead, there will be an extra "Report previous internal errors too" checkbox in the alert for the next error that you *do* need to know about. This will substantially reduce the number of alerts shown."
https:/
The second change does away with the error dialogs when encountering malformed reports or unhandled errors in the apport code. It replaces these with a new pair of fields, InvalidReason and InvalidMachineR
"If no details are available (for example, the crash file is unreadable), below the timestamp and (if available) the process name should appear the paragraph “No details were recorded for this error.”"
Thanks
Martin Pitt (pitti) wrote : | # |
Unmerged revisions
- 2596. By Evan
-
Merge with trunk.
- 2595. By Evan
-
Split does not take keyword arguments.
- 2594. By Evan
-
Fix failing test from new collect_info interface
- 2593. By Evan
-
Fix the kernel oops test GTK test.
- 2592. By Evan
-
Fix UI tests.
- 2591. By Evan
-
Collect packages for grouped reports. Correctly pass in the current package to collect_info.
- 2590. By Evan
-
Pass in the current package.
- 2589. By Evan
-
Update the tests to handle the new handle_duplicate function signature.
- 2588. By Evan
-
Fix the UI tests to match the new collect_info function signature. Correctly initialise the response from ui_present_
report_ details. - 2587. By Evan
-
Properly initialise the grouped_reports dictionary.
Preview Diff
1 | === modified file 'apport/ui.py' |
2 | --- apport/ui.py 2013-03-19 10:38:05 +0000 |
3 | +++ apport/ui.py 2013-04-27 08:52:25 +0000 |
4 | @@ -15,8 +15,8 @@ |
5 | |
6 | __version__ = '2.9.2' |
7 | |
8 | -import glob, sys, os.path, optparse, traceback, locale, gettext, re |
9 | -import errno, zlib |
10 | +import glob, sys, os.path, optparse, traceback, locale, gettext, re, itertools |
11 | +import errno, zlib, struct |
12 | import subprocess, threading, webbrowser |
13 | import signal |
14 | import time |
15 | @@ -150,6 +150,11 @@ |
16 | #if report.get('Signal') == '6' and 'AssertionMessage' not in report: |
17 | # report['UnreportableReason'] = _('The program crashed on an assertion failure, but the message could not be retrieved. Apport does not support reporting these crashes.') |
18 | |
19 | + update_report(report, reportfile) |
20 | + |
21 | + |
22 | +def update_report(report, reportfile): |
23 | + '''Write the report to disk.''' |
24 | if reportfile: |
25 | try: |
26 | with open(reportfile, 'ab') as f: |
27 | @@ -166,6 +171,16 @@ |
28 | os.chmod(reportfile, 0o640) |
29 | |
30 | |
31 | +def mark_invalid(report, reportfile, reason, machine_reason): |
32 | + '''Mark the report as invalid while continuing to process further reports. |
33 | + The invalid report will appear in the UI with the 'reason' argument as its |
34 | + text and will be uploaded to daisy.ubuntu.com for statistical purposes. |
35 | + ''' |
36 | + report['InvalidReason'] = reason |
37 | + report['InvalidMachineReason'] = machine_reason |
38 | + update_report(report, reportfile) |
39 | + |
40 | + |
41 | class UserInterface: |
42 | '''Apport user interface API. |
43 | |
44 | @@ -180,7 +195,9 @@ |
45 | self.gettext_domain = 'apport' |
46 | self.report = None |
47 | self.report_file = None |
48 | + self.grouped_reports = {} |
49 | self.cur_package = None |
50 | + self.packages = {} |
51 | |
52 | try: |
53 | self.crashdb = apport.crashdb.get_crashdb(None) |
54 | @@ -211,17 +228,58 @@ |
55 | reports = apport.fileutils.get_new_system_reports() |
56 | else: |
57 | reports = apport.fileutils.get_new_reports() |
58 | - for f in reports: |
59 | - if not self.load_report(f): |
60 | + |
61 | + # Determine which reports should be grouped with the first regular |
62 | + # crash rather than shown in a dialog on their own. These are referred |
63 | + # to as 'silent' errors. |
64 | + grouped_reports = {} |
65 | + for f in reports: |
66 | + self.load_report(f) |
67 | + if not self.load_report(f) or not self.get_desktop_entry(): |
68 | + # We may not be able to load the report, but we should still |
69 | + # show that it occurred and send the reason for statistical |
70 | + # purposes. |
71 | + grouped_reports[f] = self.report |
72 | + self.packages[f] = self.cur_package |
73 | + |
74 | + grouped_reports_reported = False |
75 | + for f in reports: |
76 | + if f in grouped_reports or not self.load_report(f): |
77 | continue |
78 | + |
79 | if self.report['ProblemType'] == 'Hang': |
80 | self.finish_hang(f) |
81 | else: |
82 | - self.run_crash(f) |
83 | + if not grouped_reports_reported: |
84 | + grouped_reports_reported = True |
85 | + self.grouped_reports = grouped_reports |
86 | + self.run_crash(f) |
87 | + |
88 | + # Make sure we don't report these again. Note that these |
89 | + # grouped reports will not be marked as seen if we do not |
90 | + # have a regular application error report to present. |
91 | + for f in grouped_reports.keys(): |
92 | + apport.fileutils.mark_report_seen(f) |
93 | + |
94 | + self.grouped_reports = None |
95 | + else: |
96 | + self.run_crash(f) |
97 | result = True |
98 | |
99 | return result |
100 | |
101 | + def get_reports(self): |
102 | + '''Return an iterator over all the reports and their respective |
103 | + files. |
104 | + ''' |
105 | + r = [(self.report_file, self.report)] |
106 | + |
107 | + if self.grouped_reports: |
108 | + grouped = self.grouped_reports.items() |
109 | + return itertools.chain(r, grouped) |
110 | + else: |
111 | + return r |
112 | + |
113 | def run_crash(self, report_file, confirm=True): |
114 | '''Present and report a particular crash. |
115 | |
116 | @@ -246,44 +304,17 @@ |
117 | if 'Ignore' in self.report: |
118 | return |
119 | |
120 | - # check for absent CoreDumps (removed if they exceed size limit) |
121 | - if self.report.get('ProblemType') == 'Crash' and 'Signal' in self.report and 'CoreDump' not in self.report and 'Stacktrace' not in self.report: |
122 | - subject = os.path.basename(self.report.get('ExecutablePath', _('unknown program'))) |
123 | - heading = _('Sorry, the program "%s" closed unexpectedly') % subject |
124 | - self.ui_error_message( |
125 | - _('Problem in %s') % subject, |
126 | - '%s\n\n%s' % (heading, _('Your computer does not have ' |
127 | - 'enough free memory to automatically analyze the problem ' |
128 | - 'and send a report to the developers.'))) |
129 | - return |
130 | + for path, report in self.get_reports(): |
131 | + # check for absent CoreDumps (removed if they exceed size limit) |
132 | + if report.get('ProblemType') == 'Crash' and 'Signal' in report and 'CoreDump' not in report and 'Stacktrace' not in report: |
133 | + reason = _('This problem report is damaged and cannot be processed.') |
134 | + machine_reason = 'No core dump' |
135 | + mark_invalid(report, path, reason, machine_reason) |
136 | |
137 | allowed_to_report = apport.fileutils.allowed_to_report() |
138 | response = self.ui_present_report_details(allowed_to_report) |
139 | if response['report'] or response['examine']: |
140 | - try: |
141 | - if 'Dependencies' not in self.report: |
142 | - self.collect_info() |
143 | - except (IOError, zlib.error) as e: |
144 | - # can happen with broken core dumps |
145 | - self.report = None |
146 | - self.ui_error_message( |
147 | - _('Invalid problem report'), '%s\n\n%s' % ( |
148 | - _('This problem report is damaged and cannot be processed.'), |
149 | - repr(e))) |
150 | - self.ui_shutdown() |
151 | - return |
152 | - except ValueError: # package does not exist |
153 | - self.ui_error_message(_('Invalid problem report'), |
154 | - _('The report belongs to a package that is not installed.')) |
155 | - self.ui_shutdown() |
156 | - return |
157 | - except Exception as e: |
158 | - apport.error(repr(e)) |
159 | - self.ui_error_message(_('Invalid problem report'), |
160 | - _('An error occurred while attempting to process this' |
161 | - ' problem report:') + '\n\n' + str(e)) |
162 | - self.ui_shutdown() |
163 | - return |
164 | + self.collect_all_info() |
165 | |
166 | if self.report is None: |
167 | # collect() does that on invalid reports |
168 | @@ -299,21 +330,22 @@ |
169 | if not response['report']: |
170 | return |
171 | |
172 | - apport.fileutils.mark_report_upload(report_file) |
173 | - # We check for duplicates and unreportable crashes here, rather |
174 | - # than before we show the dialog, as we want to submit these to the |
175 | - # crash database, but not Launchpad. |
176 | - if self.crashdb.accepts(self.report): |
177 | - # FIXME: This behaviour is not really correct, but necessary as |
178 | - # long as we only support a single crashdb and have whoopsie |
179 | - # hardcoded. Once we have multiple crash dbs, we need to check |
180 | - # accepts() earlier, and not even present the data if none of |
181 | - # the DBs wants the report. See LP#957177 for details. |
182 | - if self.handle_duplicate(): |
183 | - return |
184 | - if self.check_unreportable(): |
185 | - return |
186 | - self.file_report() |
187 | + for path, report in self.get_reports(): |
188 | + apport.fileutils.mark_report_upload(path) |
189 | + # We check for duplicates and unreportable crashes here, rather |
190 | + # than before we show the dialog, as we want to submit these to the |
191 | + # crash database, but not Launchpad. |
192 | + if self.crashdb.accepts(report): |
193 | + # FIXME: This behaviour is not really correct, but necessary as |
194 | + # long as we only support a single crashdb and have whoopsie |
195 | + # hardcoded. Once we have multiple crash dbs, we need to check |
196 | + # accepts() earlier, and not even present the data if none of |
197 | + # the DBs wants the report. See LP#957177 for details. |
198 | + if self.handle_duplicate(report): |
199 | + return |
200 | + if self.check_unreportable(report): |
201 | + return |
202 | + self.file_report(report) |
203 | except IOError as e: |
204 | # fail gracefully if file is not readable for us |
205 | if e.errno in (errno.EPERM, errno.EACCES): |
206 | @@ -450,7 +482,8 @@ |
207 | self.cur_package = self.options.package |
208 | |
209 | try: |
210 | - self.collect_info(symptom_script) |
211 | + self.collect_info(self.report, self.report_file, self.cur_package, |
212 | + symptom_script) |
213 | except ValueError as e: |
214 | if 'package' in str(e) and 'does not exist' in str(e): |
215 | if not self.cur_package: |
216 | @@ -463,12 +496,12 @@ |
217 | else: |
218 | raise |
219 | |
220 | - if self.check_unreportable(): |
221 | + if self.check_unreportable(self.report): |
222 | return |
223 | |
224 | self.add_extra_tags() |
225 | |
226 | - if self.handle_duplicate(): |
227 | + if self.handle_duplicate(self.report): |
228 | return True |
229 | |
230 | # not useful for bug reports, and has potentially sensitive information |
231 | @@ -488,7 +521,7 @@ |
232 | allowed_to_report = apport.fileutils.allowed_to_report() |
233 | response = self.ui_present_report_details(allowed_to_report) |
234 | if response['report']: |
235 | - self.file_report() |
236 | + self.file_report(self.report) |
237 | |
238 | return True |
239 | |
240 | @@ -539,7 +572,8 @@ |
241 | if not os.path.exists(os.path.join(apport.report._hook_dir, 'source_%s.py' % p)): |
242 | print('Package %s not installed and no hook available, ignoring' % p) |
243 | continue |
244 | - self.collect_info(ignore_uninstalled=True) |
245 | + self.collect_info(self.report, self.report_file, self.cur_package, |
246 | + ignore_uninstalled=True) |
247 | info_collected = True |
248 | |
249 | if not info_collected: |
250 | @@ -935,7 +969,45 @@ |
251 | |
252 | return True |
253 | |
254 | - def collect_info(self, symptom_script=None, ignore_uninstalled=False, |
255 | + def exception_wrapped_collect_info(self, report, report_file=None, |
256 | + cur_package=None, symptom_script=None, |
257 | + ignore_uninstalled=False, |
258 | + on_finished=None): |
259 | + try: |
260 | + self.collect_info(report, report_file, cur_package, symptom_script, |
261 | + ignore_uninstalled, on_finished) |
262 | + except (IOError, zlib.error, struct.error) as e: |
263 | + # can happen with broken core dumps |
264 | + reason = _('This problem report is damaged and cannot be processed.') |
265 | + machine_reason = excstr(e) |
266 | + mark_invalid(report, report_file, reason, machine_reason) |
267 | + except ValueError as e: # package does not exist |
268 | + reason = _('The report belongs to a package that is not installed.') |
269 | + machine_reason = excstr(e) |
270 | + mark_invalid(report, report_file, reason, machine_reason) |
271 | + except Exception as e: |
272 | + reason = _('This problem report is damaged and cannot be processed.') |
273 | + machine_reason = excstr(e) |
274 | + mark_invalid(report, report_file, reason, machine_reason) |
275 | + |
276 | + def collect_all_info(self, on_finished=None): |
277 | + '''Collect additional information from an application report and from |
278 | + all grouped reports.''' |
279 | + if 'Dependencies' not in self.report: |
280 | + self.exception_wrapped_collect_info(self.report, |
281 | + self.report_file, |
282 | + self.cur_package) |
283 | + |
284 | + for path in self.grouped_reports: |
285 | + report = self.grouped_reports[path] |
286 | + if 'Dependencies' not in report: |
287 | + self.exception_wrapped_collect_info(report, path, self.packages[path]) |
288 | + |
289 | + if on_finished: |
290 | + on_finished() |
291 | + |
292 | + def collect_info(self, report, report_file=None, cur_package=None, |
293 | + symptom_script=None, ignore_uninstalled=False, |
294 | on_finished=None): |
295 | '''Collect additional information. |
296 | |
297 | @@ -949,25 +1021,25 @@ |
298 | run_symptom()). |
299 | ''' |
300 | # check if binary changed since the crash happened |
301 | - if 'ExecutablePath' in self.report and 'ExecutableTimestamp' in self.report: |
302 | - orig_time = int(self.report['ExecutableTimestamp']) |
303 | - del self.report['ExecutableTimestamp'] |
304 | - cur_time = int(os.stat(self.report['ExecutablePath']).st_mtime) |
305 | + if 'ExecutablePath' in report and 'ExecutableTimestamp' in report: |
306 | + orig_time = int(report['ExecutableTimestamp']) |
307 | + del report['ExecutableTimestamp'] |
308 | + cur_time = int(os.stat(report['ExecutablePath']).st_mtime) |
309 | |
310 | if orig_time != cur_time: |
311 | - self.report['UnreportableReason'] = ( |
312 | + report['UnreportableReason'] = ( |
313 | _('The problem happened with the program %s which changed ' |
314 | - 'since the crash occurred.') % self.report['ExecutablePath']) |
315 | + 'since the crash occurred.') % report['ExecutablePath']) |
316 | return |
317 | |
318 | - if not self.cur_package and 'ExecutablePath' not in self.report \ |
319 | + if not cur_package and 'ExecutablePath' not in report \ |
320 | and not symptom_script: |
321 | # this happens if we file a bug without specifying a PID or a |
322 | # package |
323 | - self.report.add_os_info() |
324 | + report.add_os_info() |
325 | else: |
326 | # check if we already ran, skip if so |
327 | - if (self.report.get('ProblemType') == 'Crash' and 'Stacktrace' in self.report) or (self.report.get('ProblemType') != 'Crash' and 'Dependencies' in self.report): |
328 | + if (report.get('ProblemType') == 'Crash' and 'Stacktrace' in report) or (report.get('ProblemType') != 'Crash' and 'Dependencies' in report): |
329 | if on_finished: |
330 | on_finished() |
331 | return |
332 | @@ -978,12 +1050,12 @@ |
333 | |
334 | hookui = HookUI(self) |
335 | |
336 | - if 'Stacktrace' not in self.report: |
337 | + if 'Stacktrace' not in report: |
338 | # save original environment, in case hooks change it |
339 | orig_env = os.environ.copy() |
340 | icthread = apport.REThread.REThread(target=thread_collect_info, |
341 | name='thread_collect_info', |
342 | - args=(self.report, self.report_file, self.cur_package, |
343 | + args=(report, report_file, cur_package, |
344 | hookui, symptom_script, ignore_uninstalled)) |
345 | icthread.start() |
346 | while icthread.isAlive(): |
347 | @@ -1007,8 +1079,8 @@ |
348 | return |
349 | |
350 | # check bug patterns |
351 | - if self.report['ProblemType'] == 'KernelCrash' or self.report['ProblemType'] == 'KernelOops' or 'Package' in self.report: |
352 | - bpthread = apport.REThread.REThread(target=self.report.search_bug_patterns, |
353 | + if report['ProblemType'] == 'KernelCrash' or report['ProblemType'] == 'KernelOops' or 'Package' in report: |
354 | + bpthread = apport.REThread.REThread(target=report.search_bug_patterns, |
355 | args=(self.crashdb.get_bugpattern_baseurl(),)) |
356 | bpthread.start() |
357 | while bpthread.isAlive(): |
358 | @@ -1019,12 +1091,12 @@ |
359 | sys.exit(1) |
360 | bpthread.exc_raise() |
361 | if bpthread.return_value(): |
362 | - self.report['KnownReport'] = bpthread.return_value() |
363 | + report['KnownReport'] = bpthread.return_value() |
364 | |
365 | # check crash database if problem is known |
366 | - if self.report['ProblemType'] != 'Bug': |
367 | + if report['ProblemType'] != 'Bug': |
368 | known_thread = apport.REThread.REThread(target=self.crashdb.known, |
369 | - args=(self.report,)) |
370 | + args=(report,)) |
371 | known_thread.start() |
372 | while known_thread.isAlive(): |
373 | self.ui_pulse_info_collection_progress() |
374 | @@ -1036,13 +1108,13 @@ |
375 | val = known_thread.return_value() |
376 | if val is not None: |
377 | if val is True: |
378 | - self.report['KnownReport'] = '1' |
379 | + report['KnownReport'] = '1' |
380 | else: |
381 | - self.report['KnownReport'] = val |
382 | + report['KnownReport'] = val |
383 | |
384 | # anonymize; needs to happen after duplicate checking, otherwise we |
385 | # might damage the stack trace |
386 | - anonymize_thread = apport.REThread.REThread(target=self.report.anonymize) |
387 | + anonymize_thread = apport.REThread.REThread(target=report.anonymize) |
388 | anonymize_thread.start() |
389 | while anonymize_thread.isAlive(): |
390 | self.ui_pulse_info_collection_progress() |
391 | @@ -1055,10 +1127,10 @@ |
392 | self.ui_stop_info_collection_progress() |
393 | |
394 | # check that we were able to determine package names |
395 | - if 'UnreportableReason' not in self.report: |
396 | - if ('SourcePackage' not in self.report or |
397 | - (not self.report['ProblemType'].startswith('Kernel') |
398 | - and 'Package' not in self.report)): |
399 | + if 'UnreportableReason' not in report: |
400 | + if ('SourcePackage' not in report or |
401 | + (not report['ProblemType'].startswith('Kernel') |
402 | + and 'Package' not in report)): |
403 | self.ui_error_message(_('Invalid problem report'), |
404 | _('Could not determine the package or source package name.')) |
405 | # TODO This is not called consistently, is it really needed? |
406 | @@ -1112,26 +1184,26 @@ |
407 | os.write(w, str(e)) |
408 | sys.exit(1) |
409 | |
410 | - def file_report(self): |
411 | + def file_report(self, report): |
412 | '''Upload the current report and guide the user to the reporting web page.''' |
413 | # FIXME: This behaviour is not really correct, but necessary as |
414 | # long as we only support a single crashdb and have whoopsie |
415 | # hardcoded. Once we have multiple crash dbs, we need to check |
416 | # accepts() earlier, and not even present the data if none of |
417 | # the DBs wants the report. See LP#957177 for details. |
418 | - if not self.crashdb.accepts(self.report): |
419 | + if not self.crashdb.accepts(report): |
420 | return |
421 | # drop PackageArchitecture if equal to Architecture |
422 | - if self.report.get('PackageArchitecture') == self.report.get('Architecture'): |
423 | + if report.get('PackageArchitecture') == report.get('Architecture'): |
424 | try: |
425 | - del self.report['PackageArchitecture'] |
426 | + del report['PackageArchitecture'] |
427 | except KeyError: |
428 | pass |
429 | |
430 | # StacktraceAddressSignature is redundant and does not need to clutter |
431 | # the database |
432 | try: |
433 | - del self.report['StacktraceAddressSignature'] |
434 | + del report['StacktraceAddressSignature'] |
435 | except KeyError: |
436 | pass |
437 | |
438 | @@ -1144,7 +1216,7 @@ |
439 | |
440 | self.ui_start_upload_progress() |
441 | upthread = apport.REThread.REThread(target=self.crashdb.upload, |
442 | - args=(self.report, progress_callback)) |
443 | + args=(report, progress_callback)) |
444 | upthread.start() |
445 | while upthread.isAlive(): |
446 | self.ui_set_upload_progress(__upload_progress) |
447 | @@ -1161,7 +1233,7 @@ |
448 | user, password = data |
449 | self.crashdb.set_credentials(user, password) |
450 | upthread = apport.REThread.REThread(target=self.crashdb.upload, |
451 | - args=(self.report, progress_callback)) |
452 | + args=(report, progress_callback)) |
453 | upthread.start() |
454 | except (TypeError, SyntaxError, ValueError): |
455 | raise |
456 | @@ -1176,7 +1248,7 @@ |
457 | ticket = upthread.return_value() |
458 | self.ui_stop_upload_progress() |
459 | |
460 | - url = self.crashdb.get_comment_url(self.report, ticket) |
461 | + url = self.crashdb.get_comment_url(report, ticket) |
462 | if url: |
463 | self.open_url(url) |
464 | |
465 | @@ -1187,27 +1259,26 @@ |
466 | be processed, otherwise self.report is initialized and True is |
467 | returned. |
468 | ''' |
469 | + self.report = apport.Report() |
470 | try: |
471 | - self.report = apport.Report() |
472 | with open(path, 'rb') as f: |
473 | self.report.load(f, binary='compressed') |
474 | if 'ProblemType' not in self.report: |
475 | raise ValueError('Report does not contain "ProblemType" field') |
476 | - except MemoryError: |
477 | - self.report = None |
478 | - self.ui_error_message(_('Memory exhaustion'), |
479 | - _('Your system does not have enough memory to process this crash report.')) |
480 | + except MemoryError as e: |
481 | + reason = _('Your system does not have enough memory to process this crash report.') |
482 | + machine_reason = excstr(e) |
483 | + mark_invalid(self.report, path, reason, machine_reason) |
484 | return False |
485 | except IOError as e: |
486 | - self.report = None |
487 | - self.ui_error_message(_('Invalid problem report'), e.strerror) |
488 | + reason = _('Invalid problem report') |
489 | + machine_reason = excstr(e) |
490 | + mark_invalid(self.report, path, reason, machine_reason) |
491 | return False |
492 | except (TypeError, ValueError, AssertionError, zlib.error) as e: |
493 | - self.report = None |
494 | - self.ui_error_message(_('Invalid problem report'), |
495 | - '%s\n\n%s' % ( |
496 | - _('This problem report is damaged and cannot be processed.'), |
497 | - repr(e))) |
498 | + reason = _('This problem report is damaged and cannot be processed.') |
499 | + machine_reason = excstr(e) |
500 | + mark_invalid(self.report, path, reason, machine_reason) |
501 | return False |
502 | |
503 | if 'Package' in self.report: |
504 | @@ -1219,38 +1290,38 @@ |
505 | if self.report['ProblemType'] == 'Crash': |
506 | exe_path = self.report.get('ExecutablePath', '') |
507 | if not os.path.exists(exe_path): |
508 | - msg = _('This problem report applies to a program which is not installed any more.') |
509 | - if exe_path: |
510 | - msg = '%s (%s)' % (msg, self.report['ExecutablePath']) |
511 | - self.report = None |
512 | - self.ui_info_message(_('Invalid problem report'), msg) |
513 | + reason = _('This problem report applies to a program which is not installed any more.') |
514 | + machine_reason = "ENOENT: '%s'" % exe_path |
515 | + mark_invalid(self.report, path, reason, machine_reason) |
516 | return False |
517 | |
518 | if 'InterpreterPath' in self.report: |
519 | if not os.path.exists(self.report['InterpreterPath']): |
520 | - msg = _('This problem report applies to a program which is not installed any more.') |
521 | - self.ui_info_message(_('Invalid problem report'), '%s (%s)' |
522 | - % (msg, self.report['InterpreterPath'])) |
523 | + reason = _('This problem report applies to a program which is not installed any more.') |
524 | + machine_reason = "ENOENT: '%s'" % self.report['InterpreterPath'] |
525 | + mark_invalid(self.report, path, reason, machine_reason) |
526 | return False |
527 | |
528 | return True |
529 | |
530 | - def check_unreportable(self): |
531 | + def check_unreportable(self, report): |
532 | '''Check if the current report is unreportable. |
533 | |
534 | If so, display an info message and return True. |
535 | ''' |
536 | - if not self.crashdb.accepts(self.report): |
537 | + if not self.crashdb.accepts(report): |
538 | return False |
539 | - if 'UnreportableReason' in self.report: |
540 | - if type(self.report['UnreportableReason']) == bytes: |
541 | - self.report['UnreportableReason'] = self.report['UnreportableReason'].decode('UTF-8') |
542 | - if 'Package' in self.report: |
543 | - title = _('Problem in %s') % self.report['Package'].split()[0] |
544 | + if 'InvalidReason' in report: |
545 | + return True |
546 | + if 'UnreportableReason' in report: |
547 | + if type(report['UnreportableReason']) == bytes: |
548 | + report['UnreportableReason'] = report['UnreportableReason'].decode('UTF-8') |
549 | + if 'Package' in report: |
550 | + title = _('Problem in %s') % report['Package'].split()[0] |
551 | else: |
552 | title = '' |
553 | self.ui_info_message(title, _('The problem cannot be reported:\n\n%s') % |
554 | - self.report['UnreportableReason']) |
555 | + report['UnreportableReason']) |
556 | return True |
557 | return False |
558 | |
559 | @@ -1291,26 +1362,26 @@ |
560 | return None |
561 | return result |
562 | |
563 | - def handle_duplicate(self): |
564 | + def handle_duplicate(self, report): |
565 | '''Check if current report matches a bug pattern. |
566 | |
567 | If so, tell the user about it, open the existing bug in a browser, and |
568 | return True. |
569 | ''' |
570 | - if not self.crashdb.accepts(self.report): |
571 | + if not self.crashdb.accepts(report): |
572 | return False |
573 | - if 'KnownReport' not in self.report: |
574 | + if 'KnownReport' not in report: |
575 | return False |
576 | |
577 | # if we have an URL, open it; otherwise this is just a marker that we |
578 | # know about it |
579 | - if self.report['KnownReport'].startswith('http'): |
580 | + if report['KnownReport'].startswith('http'): |
581 | self.ui_info_message(_('Problem already known'), |
582 | _('This problem was already reported in the bug report displayed \ |
583 | in the web browser. Please check if you can add any further information that \ |
584 | might be helpful for the developers.')) |
585 | |
586 | - self.open_url(self.report['KnownReport']) |
587 | + self.open_url(report['KnownReport']) |
588 | else: |
589 | self.ui_info_message(_('Problem already known'), |
590 | _('This problem was already reported to developers. Thank you!')) |
591 | |
592 | === modified file 'gtk/apport-gtk' |
593 | --- gtk/apport-gtk 2012-11-23 14:23:46 +0000 |
594 | +++ gtk/apport-gtk 2013-04-27 08:52:25 +0000 |
595 | @@ -11,7 +11,7 @@ |
596 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for |
597 | # the full text of the license. |
598 | |
599 | -import os.path, sys, subprocess, os, re, errno |
600 | +import os.path, sys, subprocess, os, re, errno, itertools |
601 | |
602 | from gi.repository import GObject, GLib, Wnck, GdkX11, Gdk |
603 | try: |
604 | @@ -63,11 +63,13 @@ |
605 | column = Gtk.TreeViewColumn('Report', Gtk.CellRendererText(), text=0) |
606 | self.w('details_treeview').append_column(column) |
607 | self.spinner = self.add_spinner_over_treeview(self.w('details_treeview')) |
608 | + self.w('details_treeview').set_row_separator_func(self.on_row_separator, None) |
609 | |
610 | self.md = None |
611 | |
612 | self.desktop_info = None |
613 | |
614 | + |
615 | # |
616 | # ui_* implementation of abstract UserInterface classes |
617 | # |
618 | @@ -100,42 +102,56 @@ |
619 | if not self.w('details_treeview').get_property('visible'): |
620 | return |
621 | |
622 | - if shown_keys: |
623 | - keys = set(self.report.keys()) & set(shown_keys) |
624 | - else: |
625 | - keys = self.report.keys() |
626 | - # show the most interesting items on top |
627 | - keys = sorted(keys) |
628 | - for k in ('Traceback', 'StackTrace', 'Title', 'ProblemType', 'Package', 'ExecutablePath'): |
629 | - if k in keys: |
630 | - keys.remove(k) |
631 | - keys.insert(0, k) |
632 | - |
633 | self.tree_model.clear() |
634 | - for key in keys: |
635 | - keyiter = self.tree_model.insert_before(None, None) |
636 | - self.tree_model.set_value(keyiter, 0, key) |
637 | - |
638 | - valiter = self.tree_model.insert_before(keyiter, None) |
639 | - if not hasattr(self.report[key], 'gzipvalue') and \ |
640 | - hasattr(self.report[key], 'isspace') and \ |
641 | - not self.report._is_binary(self.report[key]): |
642 | - v = self.report[key] |
643 | - if len(v) > 4000: |
644 | - v = v[:4000] |
645 | + |
646 | + for report in itertools.chain([self.report], self.grouped_reports.values()): |
647 | + if shown_keys: |
648 | + keys = set(report.keys()) & set(shown_keys) |
649 | + else: |
650 | + keys = report.keys() |
651 | + # show the most interesting items on top |
652 | + keys = sorted(keys) |
653 | + for k in ('Traceback', 'StackTrace', 'Title', 'ProblemType', 'Package', 'ExecutablePath'): |
654 | + if k in keys: |
655 | + keys.remove(k) |
656 | + keys.insert(0, k) |
657 | + |
658 | + if report != self.report: |
659 | + # insert separator |
660 | + keyiter = self.tree_model.insert_before(None, None) |
661 | + self.tree_model.set_value(keyiter, 0, '') |
662 | + |
663 | + if 'InvalidReason' in keys: |
664 | + # This report is invalid, but we need to tell the user that |
665 | + # we're going to send it for statistical purposes. |
666 | + keyiter = self.tree_model.insert_before(None, None) |
667 | + self.tree_model.set_value(keyiter, 0, report['InvalidReason']) |
668 | + continue |
669 | + |
670 | + for key in keys: |
671 | + keyiter = self.tree_model.insert_before(None, None) |
672 | + self.tree_model.set_value(keyiter, 0, key) |
673 | + |
674 | + valiter = self.tree_model.insert_before(keyiter, None) |
675 | + if not hasattr(report[key], 'gzipvalue') and \ |
676 | + hasattr(report[key], 'isspace') and \ |
677 | + not report._is_binary(report[key]): |
678 | + v = report[key] |
679 | + if len(v) > 4000: |
680 | + v = v[:4000] |
681 | + if type(v) == bytes: |
682 | + v += b'\n[...]' |
683 | + else: |
684 | + v += '\n[...]' |
685 | if type(v) == bytes: |
686 | - v += b'\n[...]' |
687 | - else: |
688 | - v += '\n[...]' |
689 | - if type(v) == bytes: |
690 | - v = v.decode('UTF-8', errors='replace') |
691 | - self.tree_model.set_value(valiter, 0, v) |
692 | - # expand the row if the value has less than 5 lines |
693 | - if len(list(filter(lambda c: c == '\n', self.report[key]))) < 4: |
694 | - self.w('details_treeview').expand_row( |
695 | - self.tree_model.get_path(keyiter), False) |
696 | - else: |
697 | - self.tree_model.set_value(valiter, 0, _('(binary data)')) |
698 | + v = v.decode('UTF-8', errors='replace') |
699 | + self.tree_model.set_value(valiter, 0, v) |
700 | + # expand the row if the value has less than 5 lines |
701 | + if len(list(filter(lambda c: c == '\n', report[key]))) < 4: |
702 | + self.w('details_treeview').expand_row( |
703 | + self.tree_model.get_path(keyiter), False) |
704 | + else: |
705 | + self.tree_model.set_value(valiter, 0, _('(binary data)')) |
706 | |
707 | def get_system_application_title(self): |
708 | '''Get dialog title for a non-.desktop application. |
709 | @@ -207,6 +223,14 @@ |
710 | self.w('send_error_report').set_active(False) |
711 | self.w('send_error_report').hide() |
712 | |
713 | + if self.grouped_reports and allowed_to_report: |
714 | + self.w('previous_internal_errors').set_active(True) |
715 | + self.w('previous_internal_errors').show() |
716 | + self.w('ignore_future_problems').hide() |
717 | + else: |
718 | + self.w('previous_internal_errors').set_active(False) |
719 | + self.w('previous_internal_errors').hide() |
720 | + |
721 | self.w('examine').set_visible(self.can_examine_locally()) |
722 | |
723 | self.w('continue_button').set_label(_('Continue')) |
724 | @@ -346,8 +370,18 @@ |
725 | if len(sys.argv) == 1: |
726 | d.set_focus_on_map(False) |
727 | |
728 | - return_value = { 'report' : False, 'blacklist' : False, |
729 | - 'restart' : False, 'examine' : False } |
730 | + return_value = { |
731 | + # Should the problem be reported? |
732 | + 'report' : False, |
733 | + # Should future instances of this type be ignored? |
734 | + 'blacklist' : False, |
735 | + # Should the application be restarted? |
736 | + 'restart' : False, |
737 | + # Has an interactive debugging session been requested? |
738 | + 'examine' : False, |
739 | + # Should grouped reports also be reported? |
740 | + 'grouped': False |
741 | + } |
742 | def dialog_crash_dismissed(widget): |
743 | self.w('dialog_crash_new').hide() |
744 | if widget is self.w('dialog_crash_new'): |
745 | @@ -364,6 +398,8 @@ |
746 | return_value['blacklist'] = True |
747 | if widget == self.w('continue_button') and self.desktop_info: |
748 | return_value['restart'] = True |
749 | + if self.w('previous_internal_errors').get_active(): |
750 | + return_value['grouped'] = True |
751 | Gtk.main_quit() |
752 | |
753 | self.w('dialog_crash_new').connect('destroy', dialog_crash_dismissed) |
754 | @@ -561,6 +597,11 @@ |
755 | # Event handlers |
756 | # |
757 | |
758 | + def on_row_separator(self, model, iterator, data): |
759 | + if model[iterator][0] == '': |
760 | + return True |
761 | + return False |
762 | + |
763 | def on_show_details_clicked(self, widget): |
764 | sw = self.w('details_scrolledwindow') |
765 | if sw.get_property('visible'): |
766 | @@ -574,7 +615,7 @@ |
767 | if not self.collect_called: |
768 | self.collect_called = True |
769 | self.ui_update_view(['ExecutablePath']) |
770 | - GLib.idle_add(lambda: self.collect_info(on_finished=self.ui_update_view)) |
771 | + GLib.idle_add(lambda: self.collect_all_info(on_finished=self.ui_update_view)) |
772 | return True |
773 | |
774 | def on_progress_window_close_event(self, widget, event=None): |
775 | |
776 | === modified file 'gtk/apport-gtk.ui' |
777 | --- gtk/apport-gtk.ui 2012-03-02 12:03:31 +0000 |
778 | +++ gtk/apport-gtk.ui 2013-04-27 08:52:25 +0000 |
779 | @@ -27,11 +27,9 @@ |
780 | <child> |
781 | <object class="GtkButton" id="button7"> |
782 | <property name="label">gtk-cancel</property> |
783 | - <property name="use_action_appearance">False</property> |
784 | <property name="visible">True</property> |
785 | <property name="can_focus">True</property> |
786 | <property name="receives_default">True</property> |
787 | - <property name="use_action_appearance">False</property> |
788 | <property name="use_stock">True</property> |
789 | </object> |
790 | <packing> |
791 | @@ -43,11 +41,9 @@ |
792 | <child> |
793 | <object class="GtkButton" id="button13"> |
794 | <property name="label">gtk-ok</property> |
795 | - <property name="use_action_appearance">False</property> |
796 | <property name="visible">True</property> |
797 | <property name="can_focus">True</property> |
798 | <property name="receives_default">True</property> |
799 | - <property name="use_action_appearance">False</property> |
800 | <property name="use_stock">True</property> |
801 | </object> |
802 | <packing> |
803 | @@ -213,11 +209,9 @@ |
804 | <child> |
805 | <object class="GtkCheckButton" id="send_error_report"> |
806 | <property name="label" translatable="yes">Send an error report to help fix this problem</property> |
807 | - <property name="use_action_appearance">False</property> |
808 | <property name="visible">True</property> |
809 | <property name="can_focus">True</property> |
810 | <property name="receives_default">False</property> |
811 | - <property name="use_action_appearance">False</property> |
812 | <property name="xalign">0</property> |
813 | <property name="active">True</property> |
814 | <property name="draw_indicator">True</property> |
815 | @@ -231,11 +225,9 @@ |
816 | <child> |
817 | <object class="GtkCheckButton" id="ignore_future_problems"> |
818 | <property name="label" translatable="yes">Ignore future problems of this program version</property> |
819 | - <property name="use_action_appearance">False</property> |
820 | <property name="visible">True</property> |
821 | <property name="can_focus">True</property> |
822 | <property name="receives_default">False</property> |
823 | - <property name="use_action_appearance">False</property> |
824 | <property name="xalign">0</property> |
825 | <property name="draw_indicator">True</property> |
826 | </object> |
827 | @@ -245,6 +237,22 @@ |
828 | <property name="position">1</property> |
829 | </packing> |
830 | </child> |
831 | + <child> |
832 | + <object class="GtkCheckButton" id="previous_internal_errors"> |
833 | + <property name="label" translatable="yes">Report previous internal errors too</property> |
834 | + <property name="visible">True</property> |
835 | + <property name="can_focus">True</property> |
836 | + <property name="receives_default">False</property> |
837 | + <property name="xalign">0.0099999997764825821</property> |
838 | + <property name="yalign">0.51999998092651367</property> |
839 | + <property name="draw_indicator">True</property> |
840 | + </object> |
841 | + <packing> |
842 | + <property name="expand">False</property> |
843 | + <property name="fill">True</property> |
844 | + <property name="position">2</property> |
845 | + </packing> |
846 | + </child> |
847 | </object> |
848 | <packing> |
849 | <property name="expand">False</property> |
850 | @@ -285,11 +293,9 @@ |
851 | <child> |
852 | <object class="GtkButton" id="show_details"> |
853 | <property name="label" translatable="yes">Show Details</property> |
854 | - <property name="use_action_appearance">False</property> |
855 | <property name="visible">True</property> |
856 | <property name="can_focus">True</property> |
857 | <property name="receives_default">True</property> |
858 | - <property name="use_action_appearance">False</property> |
859 | <signal name="clicked" handler="on_show_details_clicked" swapped="no"/> |
860 | </object> |
861 | <packing> |
862 | @@ -301,11 +307,9 @@ |
863 | <child> |
864 | <object class="GtkButton" id="examine"> |
865 | <property name="label" translatable="yes">_Examine locally</property> |
866 | - <property name="use_action_appearance">False</property> |
867 | <property name="visible">True</property> |
868 | <property name="can_focus">True</property> |
869 | <property name="receives_default">True</property> |
870 | - <property name="use_action_appearance">False</property> |
871 | <property name="use_underline">True</property> |
872 | </object> |
873 | <packing> |
874 | @@ -317,10 +321,8 @@ |
875 | <child> |
876 | <object class="GtkButton" id="cancel_button"> |
877 | <property name="label">gtk-cancel</property> |
878 | - <property name="use_action_appearance">False</property> |
879 | <property name="can_focus">True</property> |
880 | <property name="receives_default">True</property> |
881 | - <property name="use_action_appearance">False</property> |
882 | <property name="use_stock">True</property> |
883 | </object> |
884 | <packing> |
885 | @@ -346,11 +348,9 @@ |
886 | <child> |
887 | <object class="GtkButton" id="closed_button"> |
888 | <property name="label" translatable="yes">Leave Closed</property> |
889 | - <property name="use_action_appearance">False</property> |
890 | <property name="visible">True</property> |
891 | <property name="can_focus">True</property> |
892 | <property name="receives_default">True</property> |
893 | - <property name="use_action_appearance">False</property> |
894 | </object> |
895 | <packing> |
896 | <property name="expand">False</property> |
897 | @@ -361,11 +361,9 @@ |
898 | <child> |
899 | <object class="GtkButton" id="continue_button"> |
900 | <property name="label" translatable="yes">Continue</property> |
901 | - <property name="use_action_appearance">False</property> |
902 | <property name="visible">True</property> |
903 | <property name="can_focus">True</property> |
904 | <property name="receives_default">True</property> |
905 | - <property name="use_action_appearance">False</property> |
906 | </object> |
907 | <packing> |
908 | <property name="expand">False</property> |
909 | @@ -466,12 +464,10 @@ |
910 | <child> |
911 | <object class="GtkButton" id="button_cancel_collecting"> |
912 | <property name="label">gtk-cancel</property> |
913 | - <property name="use_action_appearance">False</property> |
914 | <property name="visible">True</property> |
915 | <property name="can_focus">True</property> |
916 | <property name="can_default">True</property> |
917 | <property name="receives_default">False</property> |
918 | - <property name="use_action_appearance">False</property> |
919 | <property name="use_stock">True</property> |
920 | <signal name="clicked" handler="on_progress_window_close_event" swapped="no"/> |
921 | </object> |
922 | @@ -569,12 +565,10 @@ |
923 | <child> |
924 | <object class="GtkButton" id="button_cancel_upload"> |
925 | <property name="label">gtk-cancel</property> |
926 | - <property name="use_action_appearance">False</property> |
927 | <property name="visible">True</property> |
928 | <property name="can_focus">True</property> |
929 | <property name="can_default">True</property> |
930 | <property name="receives_default">False</property> |
931 | - <property name="use_action_appearance">False</property> |
932 | <property name="use_stock">True</property> |
933 | <signal name="clicked" handler="on_progress_window_close_event" swapped="no"/> |
934 | </object> |
935 | |
936 | === modified file 'kde/apport-kde' |
937 | --- kde/apport-kde 2012-11-23 14:23:46 +0000 |
938 | +++ kde/apport-kde 2013-04-27 08:52:25 +0000 |
939 | @@ -278,7 +278,7 @@ |
940 | self.treeview.setVisible(visible) |
941 | if visible and not self.collect_called: |
942 | self.ui.ui_update_view(self, ['ExecutablePath']) |
943 | - QTimer.singleShot(0, lambda: self.ui.collect_info(on_finished=self.collect_done)) |
944 | + QTimer.singleShot(0, lambda: self.ui.collect_info(self.ui.report, on_finished=self.collect_done)) |
945 | self.collect_called = True |
946 | if visible: |
947 | self.setMaximumSize(16777215, 16777215) |
948 | |
949 | === modified file 'test/test_backend_apt_dpkg.py' |
950 | --- test/test_backend_apt_dpkg.py 2012-12-10 08:35:28 +0000 |
951 | +++ test/test_backend_apt_dpkg.py 2013-04-27 08:52:25 +0000 |
952 | @@ -762,7 +762,7 @@ |
953 | assert readelf.returncode == 0 |
954 | for line in out.splitlines(): |
955 | if line.startswith(' Machine:'): |
956 | - machine = line.split(maxsplit=1)[1] |
957 | + machine = line.split(' ', 1)[1] |
958 | break |
959 | else: |
960 | self.fail('could not fine Machine: in readelf output') |
961 | |
962 | === modified file 'test/test_ui.py' |
963 | --- test/test_ui.py 2013-04-03 09:05:41 +0000 |
964 | +++ test/test_ui.py 2013-04-27 08:52:25 +0000 |
965 | @@ -10,11 +10,11 @@ |
966 | from io import BytesIO |
967 | |
968 | import apport.ui |
969 | -from apport.ui import _ |
970 | import apport.report |
971 | import problem_report |
972 | import apport.crashdb_impl.memory |
973 | import stat |
974 | +from mock import patch |
975 | |
976 | |
977 | class TestSuiteUserInterface(apport.ui.UserInterface): |
978 | @@ -53,7 +53,7 @@ |
979 | # these store the choices the ui_present_* calls do |
980 | self.present_package_error_response = None |
981 | self.present_kernel_error_response = None |
982 | - self.present_details_response = None |
983 | + self.present_details_response = {} |
984 | self.question_yesno_response = None |
985 | self.question_choice_response = None |
986 | self.question_file_response = None |
987 | @@ -264,11 +264,7 @@ |
988 | self.update_report_file() |
989 | self.ui.load_report(self.report_file.name) |
990 | |
991 | - self.assertTrue(self.ui.report is None) |
992 | - self.assertEqual(self.ui.msg_title, _('Invalid problem report')) |
993 | - self.assertEqual(self.ui.msg_severity, 'info') |
994 | - |
995 | - self.ui.clear_msg() |
996 | + self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason']) |
997 | |
998 | # invalid base64 encoding |
999 | self.report_file.seek(0) |
1000 | @@ -281,9 +277,7 @@ |
1001 | self.report_file.flush() |
1002 | |
1003 | self.ui.load_report(self.report_file.name) |
1004 | - self.assertTrue(self.ui.report is None) |
1005 | - self.assertEqual(self.ui.msg_title, _('Invalid problem report')) |
1006 | - self.assertEqual(self.ui.msg_severity, 'error') |
1007 | + self.assertTrue('damaged' in self.ui.report['InvalidReason']) |
1008 | |
1009 | def test_restart(self): |
1010 | '''restart()''' |
1011 | @@ -323,7 +317,7 @@ |
1012 | |
1013 | # report without any information (distro bug) |
1014 | self.ui.report = apport.Report() |
1015 | - self.ui.collect_info() |
1016 | + self.ui.collect_info(self.ui.report) |
1017 | self.assertTrue(set(['Date', 'Uname', 'DistroRelease', 'ProblemType']).issubset( |
1018 | set(self.ui.report.keys()))) |
1019 | self.assertEqual(self.ui.ic_progress_pulses, 0, |
1020 | @@ -341,7 +335,7 @@ |
1021 | # apport hooks) |
1022 | self.ui.report['Fstab'] = ('/etc/fstab', True) |
1023 | self.ui.report['CompressedValue'] = problem_report.CompressedValue(b'Test') |
1024 | - self.ui.collect_info() |
1025 | + self.ui.collect_info(self.ui.report) |
1026 | self.assertTrue(set(['SourcePackage', 'Package', 'ProblemType', |
1027 | 'Uname', 'Dependencies', 'DistroRelease', 'Date', |
1028 | 'ExecutablePath']).issubset(set(self.ui.report.keys()))) |
1029 | @@ -356,7 +350,7 @@ |
1030 | # report with only package information |
1031 | self.ui.report = apport.Report('Bug') |
1032 | self.ui.cur_package = 'bash' |
1033 | - self.ui.collect_info() |
1034 | + self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package) |
1035 | self.assertTrue(set(['SourcePackage', 'Package', 'ProblemType', |
1036 | 'Uname', 'Dependencies', 'DistroRelease', |
1037 | 'Date']).issubset(set(self.ui.report.keys()))) |
1038 | @@ -371,7 +365,7 @@ |
1039 | self.ui.report = apport.Report('Bug') |
1040 | self.ui.cur_package = 'bash' |
1041 | self.ui.report_file = self.report_file.name |
1042 | - self.ui.collect_info() |
1043 | + self.ui.collect_info(self.ui.report, self.ui.report_file, cur_package=self.ui.cur_package) |
1044 | self.assertTrue(os.stat(self.report_file.name).st_mode & stat.S_IRGRP) |
1045 | |
1046 | def test_collect_info_crashdb_spec(self): |
1047 | @@ -386,7 +380,7 @@ |
1048 | |
1049 | self.ui.report = apport.Report('Bug') |
1050 | self.ui.cur_package = 'bash' |
1051 | - self.ui.collect_info() |
1052 | + self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package) |
1053 | self.assertTrue('CrashDB' in self.ui.report) |
1054 | self.assertFalse('UnreportableReason' in self.ui.report, |
1055 | self.ui.report.get('UnreportableReason')) |
1056 | @@ -405,7 +399,7 @@ |
1057 | |
1058 | self.ui.report = apport.Report('Bug') |
1059 | self.ui.cur_package = 'bash' |
1060 | - self.ui.collect_info() |
1061 | + self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package) |
1062 | self.assertFalse('UnreportableReason' in self.ui.report, |
1063 | self.ui.report.get('UnreportableReason')) |
1064 | self.assertEqual(self.ui.report['BashHook'], 'Moo') |
1065 | @@ -422,7 +416,7 @@ |
1066 | |
1067 | self.ui.report = apport.Report('Bug') |
1068 | self.ui.cur_package = 'bash' |
1069 | - self.ui.collect_info() |
1070 | + self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package) |
1071 | self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'], |
1072 | self.ui.report.get('UnreportableReason', '<not set>')) |
1073 | |
1074 | @@ -434,7 +428,7 @@ |
1075 | |
1076 | self.ui.report = apport.Report('Bug') |
1077 | self.ui.cur_package = 'bash' |
1078 | - self.ui.collect_info() |
1079 | + self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package) |
1080 | self.assertTrue('package hook' in self.ui.report['UnreportableReason'], |
1081 | self.ui.report.get('UnreportableReason', '<not set>')) |
1082 | |
1083 | @@ -446,7 +440,7 @@ |
1084 | |
1085 | self.ui.report = apport.Report('Bug') |
1086 | self.ui.cur_package = 'bash' |
1087 | - self.ui.collect_info() |
1088 | + self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package) |
1089 | self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'], |
1090 | self.ui.report.get('UnreportableReason', '<not set>')) |
1091 | |
1092 | @@ -454,7 +448,7 @@ |
1093 | '''handle_duplicate()''' |
1094 | |
1095 | self.ui.load_report(self.report_file.name) |
1096 | - self.assertEqual(self.ui.handle_duplicate(), False) |
1097 | + self.assertEqual(self.ui.handle_duplicate(self.ui.report), False) |
1098 | self.assertEqual(self.ui.msg_title, None) |
1099 | self.assertEqual(self.ui.opened_url, None) |
1100 | |
1101 | @@ -462,7 +456,7 @@ |
1102 | self.report['KnownReport'] = demo_url |
1103 | self.update_report_file() |
1104 | self.ui.load_report(self.report_file.name) |
1105 | - self.assertEqual(self.ui.handle_duplicate(), True) |
1106 | + self.assertEqual(self.ui.handle_duplicate(self.ui.report), True) |
1107 | self.assertEqual(self.ui.msg_severity, 'info') |
1108 | self.assertEqual(self.ui.opened_url, demo_url) |
1109 | |
1110 | @@ -471,7 +465,7 @@ |
1111 | self.report['KnownReport'] = '1' |
1112 | self.update_report_file() |
1113 | self.ui.load_report(self.report_file.name) |
1114 | - self.assertEqual(self.ui.handle_duplicate(), True) |
1115 | + self.assertEqual(self.ui.handle_duplicate(self.ui.report), True) |
1116 | self.assertEqual(self.ui.msg_severity, 'info') |
1117 | self.assertEqual(self.ui.opened_url, None) |
1118 | |
1119 | @@ -742,6 +736,76 @@ |
1120 | |
1121 | return r |
1122 | |
1123 | + @patch.object(apport.fileutils, 'get_new_reports') |
1124 | + def test_run_crashes(self, patched_fileutils): |
1125 | + '''run_crashes()''' |
1126 | + r = self._gen_test_crash() |
1127 | + self.ui = TestSuiteUserInterface() |
1128 | + |
1129 | + files = [os.path.join(apport.fileutils.report_dir, '%s%i.crash' % t) |
1130 | + for t in zip(['test'] * 3, range(3))] |
1131 | + patched_fileutils.return_value = files |
1132 | + |
1133 | + mtimes = [] |
1134 | + for report_file in files: |
1135 | + with open(report_file, 'wb') as f: |
1136 | + r.write(f) |
1137 | + mtimes.append(os.stat(report_file).st_mtime) |
1138 | + |
1139 | + # Only system internal reports. None should be processed at this time. |
1140 | + with patch.object(self.ui, 'run_crash') as patched_run_crash: |
1141 | + self.ui.run_crashes() |
1142 | + self.assertEqual(patched_run_crash.call_count, 0) |
1143 | + |
1144 | + # They shouldn't be marked as seen. |
1145 | + new_mtimes = [] |
1146 | + for report_file in files: |
1147 | + new_mtimes.append(os.stat(report_file).st_mtime) |
1148 | + self.assertEqual(mtimes, new_mtimes) |
1149 | + |
1150 | + # Three system internal reports and one application report. |
1151 | + path = os.path.join(apport.fileutils.report_dir, 'test3.crash') |
1152 | + r['DesktopFile'] = glob.glob('/usr/share/applications/*.desktop')[0] |
1153 | + with open(path, 'wb') as f: |
1154 | + r.write(f) |
1155 | + files.append(path) |
1156 | + patched_fileutils.return_value = files |
1157 | + |
1158 | + with patch.object(apport.fileutils, 'mark_report_upload') as p: |
1159 | + self.ui.present_details_response = {'report': True, |
1160 | + 'blacklist': False, |
1161 | + 'examine': False, |
1162 | + 'restart': False} |
1163 | + self.ui.run_crashes() |
1164 | + |
1165 | + # All reports should be uploaded. |
1166 | + self.assertEqual(p.call_count, 4) |
1167 | + |
1168 | + # They should be marked as seen. |
1169 | + for i in range(len(files) - 1): |
1170 | + self.assertNotEqual(os.stat(files[i]).st_mtime, mtimes[i]) |
1171 | + |
1172 | + r = apport.Report() |
1173 | + with open(files[0], 'rb') as f: |
1174 | + r.load(f) |
1175 | + with open(files[0], 'wb') as f: |
1176 | + r['ExecutablePath'] = '/nonexistant' |
1177 | + r.write(f) |
1178 | + |
1179 | + with patch.object(apport.fileutils, 'seen_report') as seen: |
1180 | + # Look at all the reports again. |
1181 | + seen.return_value = False |
1182 | + with patch.object(apport.fileutils, 'mark_report_upload') as p: |
1183 | + self.ui.present_details_response = {'report': True, |
1184 | + 'blacklist': False, |
1185 | + 'examine': False, |
1186 | + 'restart': False} |
1187 | + self.ui.run_crashes() |
1188 | + with open(files[0], 'rb') as f: |
1189 | + r.load(f) |
1190 | + self.assertEqual(r['InvalidReason'], |
1191 | + 'This problem report applies to a program which is not installed any more.') |
1192 | + |
1193 | def test_run_crash(self): |
1194 | '''run_crash()''' |
1195 | |
1196 | @@ -859,8 +923,7 @@ |
1197 | 'examine': False, |
1198 | 'restart': False} |
1199 | self.ui.run_crash(report_file) |
1200 | - self.assertEqual(self.ui.msg_severity, 'error', self.ui.msg_text) |
1201 | - self.assertTrue('decompress' in self.ui.msg_text) |
1202 | + self.assertTrue('decompress' in self.ui.report['InvalidMachineReason']) |
1203 | self.assertTrue(self.ui.present_details_shown) |
1204 | |
1205 | def test_run_crash_argv_file(self): |
1206 | @@ -967,10 +1030,12 @@ |
1207 | |
1208 | # run |
1209 | self.ui = TestSuiteUserInterface() |
1210 | + self.ui.present_details_response = {'report': False, |
1211 | + 'blacklist': False, |
1212 | + 'examine': False, |
1213 | + 'restart': False} |
1214 | self.ui.run_crash(report_file) |
1215 | - self.assertEqual(self.ui.msg_severity, 'error') |
1216 | - self.assertTrue('memory' in self.ui.msg_text, '%s: %s' % |
1217 | - (self.ui.msg_title, self.ui.msg_text)) |
1218 | + self.assertEqual(self.ui.report['InvalidMachineReason'], 'No core dump') |
1219 | |
1220 | def test_run_crash_preretraced(self): |
1221 | '''run_crash() pre-retraced reports. |
1222 | @@ -1008,7 +1073,7 @@ |
1223 | copying a .crash file. |
1224 | ''' |
1225 | self.ui.report = self._gen_test_crash() |
1226 | - self.ui.collect_info() |
1227 | + self.ui.collect_info(self.ui.report) |
1228 | |
1229 | # now pretend to move it to a machine where the package is not |
1230 | # installed |
1231 | @@ -1048,9 +1113,7 @@ |
1232 | 'examine': False, |
1233 | 'restart': False} |
1234 | self.ui.run_crash(report_file) |
1235 | - |
1236 | - self.assertEqual(self.ui.msg_title, _('Invalid problem report')) |
1237 | - self.assertEqual(self.ui.msg_severity, 'error') |
1238 | + self.assertTrue('does not exist' in self.ui.report['InvalidMachineReason']) |
1239 | |
1240 | def test_run_crash_uninstalled(self): |
1241 | '''run_crash() on reports with subsequently uninstalled packages''' |
1242 | @@ -1069,8 +1132,10 @@ |
1243 | 'restart': False} |
1244 | self.ui.run_crash(report_file) |
1245 | |
1246 | - self.assertEqual(self.ui.msg_title, _('Invalid problem report')) |
1247 | - self.assertEqual(self.ui.msg_severity, 'info') |
1248 | + self.assertTrue('not installed' in self.ui.report['InvalidReason']) |
1249 | + self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason']) |
1250 | + |
1251 | + self.ui.report = None |
1252 | |
1253 | # interpreted program got uninstalled between crash and report |
1254 | r = apport.Report() |
1255 | @@ -1080,8 +1145,10 @@ |
1256 | |
1257 | self.ui.run_crash(report_file) |
1258 | |
1259 | - self.assertEqual(self.ui.msg_title, _('Invalid problem report')) |
1260 | - self.assertEqual(self.ui.msg_severity, 'info') |
1261 | + self.assertTrue('not installed' in self.ui.report['InvalidReason']) |
1262 | + self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason']) |
1263 | + |
1264 | + self.ui.report = None |
1265 | |
1266 | # interpreter got uninstalled between crash and report |
1267 | r = apport.Report() |
1268 | @@ -1091,8 +1158,8 @@ |
1269 | |
1270 | self.ui.run_crash(report_file) |
1271 | |
1272 | - self.assertEqual(self.ui.msg_title, _('Invalid problem report')) |
1273 | - self.assertEqual(self.ui.msg_severity, 'info') |
1274 | + self.assertTrue('not installed' in self.ui.report['InvalidReason']) |
1275 | + self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason']) |
1276 | |
1277 | def test_run_crash_updated_binary(self): |
1278 | '''run_crash() on binary that got updated in the meantime''' |
1279 | |
1280 | === modified file 'test/test_ui_gtk.py' |
1281 | --- test/test_ui_gtk.py 2012-11-23 13:43:15 +0000 |
1282 | +++ test/test_ui_gtk.py 2013-04-27 08:52:25 +0000 |
1283 | @@ -18,6 +18,7 @@ |
1284 | import apport |
1285 | import shutil |
1286 | import subprocess |
1287 | +import glob |
1288 | from gi.repository import GLib, Gtk |
1289 | from apport import unicode_gettext as _ |
1290 | from mock import patch |
1291 | @@ -606,8 +607,10 @@ |
1292 | self.app.w('continue_button').clicked() |
1293 | return False |
1294 | |
1295 | - # remove the crash from setUp() and create a kernel oops |
1296 | - os.remove(self.app.report_file) |
1297 | + self.app.report['DesktopFile'] = glob.glob('/usr/share/applications/*.desktop')[0] |
1298 | + self.app.report['ProcCmdline'] = 'apport-bug apport' |
1299 | + with open(self.app.report_file, 'wb') as f: |
1300 | + self.app.report.write(f) |
1301 | kernel_oops = subprocess.Popen([kernel_oops_path], stdin=subprocess.PIPE) |
1302 | kernel_oops.communicate(b'Plasma conduit phase misalignment') |
1303 | self.assertEqual(kernel_oops.returncode, 0) |
1304 | @@ -616,8 +619,8 @@ |
1305 | self.app.run_crashes() |
1306 | |
1307 | # we should have reported one crash |
1308 | - self.assertEqual(self.app.crashdb.latest_id(), 0) |
1309 | - r = self.app.crashdb.download(0) |
1310 | + self.assertEqual(self.app.crashdb.latest_id(), 1) |
1311 | + r = self.app.crashdb.download(1) |
1312 | self.assertEqual(r['ProblemType'], 'KernelOops') |
1313 | self.assertEqual(r['OopsText'], 'Plasma conduit phase misalignment') |
1314 | |
1315 | @@ -627,7 +630,7 @@ |
1316 | self.assertTrue('Plasma conduit' in r['Title']) |
1317 | |
1318 | # URL was opened |
1319 | - self.assertEqual(self.app.open_url.call_count, 1) |
1320 | + self.assertEqual(self.app.open_url.call_count, 2) |
1321 | |
1322 | def test_bug_report_installed_package(self): |
1323 | '''Bug report for installed package''' |
Hello Evan,
sorry for the late answer! Nexus sprint, FOSDEM and all that..
> + grouped_reports = {} desktop_ entry() :
> + for f in reports:
> + self.load_report(f)
> + if not self.load_report(f) or not self.get_
It seems we load the application reports twice here, and further down.
I guess it does it that way instead of calling run_crash(f)
immediately to avoid spreading the internal ones over several
application report popups. This should eventually be made more clever
by either saving the application reports in a separate map, or using
load() with binary=False, or just accepting that in the rare case of
multiple apps crashing at the same time the internal reports might get
spread out over two runs (which, from the POV of avoiding too many
reports in memory at the same time might not actually a bad thing?)
> + # We may not be able to load the report, but we should still
> + # show that it occurred and send the reason for statistical
> + # purposes.
> + grouped_reports[f] = self.report
> + self.packages[f] = self.cur_package
So grouped_reports() has both the "failed to load" as well as the
"system" ones. If load_report() fails we need to be *much* more
careful, though. The file might not be readable, have disappeared from
disk, or be totally corrupted, so you cannot assume anything about
self.report or self.cur_package. self.report will be None for
many failure conditions, while self.cur_package could just be the
previous successful one.
I don't think we should do anything if the first set of checks in
load_reports() fails. For the "ensure that the crashed program is
still installed" case, if that fails we should either do nothing, or
continue to show the error message, but what's the point of telling
errors.u.c. about that?
In any case, this means that grouped_ reports/ self.package has some
reliable and some bogus data, and after this code we don't kno any
more which is which. Can we perhaps continue to ignore the broken ones
for now, and perhaps handle these in a separate MP?
> + # Make sure we don't report these again. Note that these reports. keys(): fileutils. mark_report_ seen(f)
> + # grouped reports will not be marked as seen if we do not
> + # have a regular application error report to present.
> + for f in grouped_
> + apport.
So we will always mark them as seen even if the user declines to send
them? I don't have a better idea about these either, but it kind of
seems to be contradictory to your usual approach of "I want to get
all reports, no matter how broken they are" :-) But this seems ok to
me.
> def run_crash(self, report_file, confirm=True): get('ProblemTyp e') == 'Crash' and 'Signal' in report and 'CoreDump' not in report and 'Stacktrace' not in report:
> [...]
> + for path, report in self.get_reports():
> + # check for absent CoreDumps (removed if they exceed size limit)
> + if report.
> + reason = _('This problem report is damaged and cannot be processe...