Merge lp:~ev/apport/grouped_reports into lp:apport

Proposed by Evan on 2013-01-23
Status: Needs review
Proposed branch: lp:~ev/apport/grouped_reports
Merge into: lp:apport
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
Reviewer Review Type Date Requested Status
Apport upstream developers 2013-01-23 Pending
Review via email: mp+144591@code.launchpad.net

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://wiki.ubuntu.com/ErrorTracker#error

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 InvalidMachineReason, that will be sent to errors.ubuntu.com. If either of these fields are present, the report will not be sent to Launchpad. We will count the occurrences of the permutations of these fields to get an understanding of what kinds of errors users are encountering when apport appears (and hopefully remedy the non-hardware ones). We will also use it to better inform the "average errors per calendar day" graph, filling in this otherwise missing error counts. Finally, it presents this information to the user just in the "Show Details" window in a less technical string, as mentioned in the specification:

"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

To post a comment you must log in.
Martin Pitt (pitti) wrote :
Download full text (5.8 KiB)

Hello Evan,

sorry for the late answer! Nexus sprint, FOSDEM and all that..

> + grouped_reports = {}
> + for f in reports:
> + self.load_report(f)
> + if not self.load_report(f) or not self.get_desktop_entry():

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
> + # grouped reports will not be marked as seen if we do not
> + # have a regular application error report to present.
> + for f in grouped_reports.keys():
> + apport.fileutils.mark_report_seen(f)

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):
> [...]
> + for path, report in self.get_reports():
> + # check for absent CoreDumps (removed if they exceed size limit)
> + if report.get('ProblemType') == 'Crash' and 'Signal' in report and 'CoreDump' not in report and 'Stacktrace' not in report:
> + reason = _('This problem report is damaged and cannot be processe...

Read more...

lp:~ev/apport/grouped_reports updated on 2013-04-27
2596. By Evan on 2013-04-27

Merge with trunk.

Unmerged revisions

2596. By Evan on 2013-04-27

Merge with trunk.

2595. By Evan on 2013-01-23

Split does not take keyword arguments.

2594. By Evan on 2013-01-23

Fix failing test from new collect_info interface

2593. By Evan on 2013-01-23

Fix the kernel oops test GTK test.

2592. By Evan on 2013-01-23

Fix UI tests.

2591. By Evan on 2013-01-22

Collect packages for grouped reports. Correctly pass in the current package to collect_info.

2590. By Evan on 2013-01-22

Pass in the current package.

2589. By Evan on 2013-01-21

Update the tests to handle the new handle_duplicate function signature.

2588. By Evan on 2013-01-21

Fix the UI tests to match the new collect_info function signature. Correctly initialise the response from ui_present_report_details.

2587. By Evan on 2013-01-21

Properly initialise the grouped_reports dictionary.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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'''

Subscribers

People subscribed via source and target branches