← Back to team overview

apport-hackers team mailing list archive

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

 

Evan Dandrea has proposed merging lp:~ev/apport/grouped_reports into lp:apport.

Requested reviews:
  Apport upstream developers (apport-hackers)

For more details, see:
https://code.launchpad.net/~ev/apport/grouped_reports/+merge/144591

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
-- 
https://code.launchpad.net/~ev/apport/grouped_reports/+merge/144591
Your team Apport upstream developers is requested to review the proposed merge of lp:~ev/apport/grouped_reports into lp:apport.
=== modified file 'apport/ui.py'
--- apport/ui.py	2013-01-08 07:08:16 +0000
+++ apport/ui.py	2013-01-23 21:33:24 +0000
@@ -15,8 +15,8 @@
 
 __version__ = '2.8'
 
-import glob, sys, os.path, optparse, traceback, locale, gettext, re
-import errno, zlib
+import glob, sys, os.path, optparse, traceback, locale, gettext, re, itertools
+import errno, zlib, struct
 import subprocess, threading, webbrowser
 import signal
 import time
@@ -148,6 +148,11 @@
     #if report.get('Signal') == '6' and 'AssertionMessage' not in report:
     #    report['UnreportableReason'] = _('The program crashed on an assertion failure, but the message could not be retrieved. Apport does not support reporting these crashes.')
 
+    update_report(report, reportfile)
+
+
+def update_report(report, reportfile):
+    '''Write the report to disk.'''
     if reportfile:
         try:
             with open(reportfile, 'ab') as f:
@@ -164,6 +169,16 @@
         os.chmod(reportfile, 0o640)
 
 
+def mark_invalid(report, reportfile, reason, machine_reason):
+    '''Mark the report as invalid while continuing to process further reports.
+    The invalid report will appear in the UI with the 'reason' argument as its
+    text and will be uploaded to daisy.ubuntu.com for statistical purposes.
+    '''
+    report['InvalidReason'] = reason
+    report['InvalidMachineReason'] = machine_reason
+    update_report(report, reportfile)
+
+
 class UserInterface:
     '''Apport user interface API.
 
@@ -178,7 +193,9 @@
         self.gettext_domain = 'apport'
         self.report = None
         self.report_file = None
+        self.grouped_reports = {}
         self.cur_package = None
+        self.packages = {}
 
         try:
             self.crashdb = apport.crashdb.get_crashdb(None)
@@ -209,17 +226,58 @@
             reports = apport.fileutils.get_new_system_reports()
         else:
             reports = apport.fileutils.get_new_reports()
-        for f in reports:
-            if not self.load_report(f):
+
+        # Determine which reports should be grouped with the first regular
+        # crash rather than shown in a dialog on their own. These are referred
+        # to as 'silent' errors.
+        grouped_reports = {}
+        for f in reports:
+            self.load_report(f)
+            if not self.load_report(f) or not self.get_desktop_entry():
+                # 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
+
+        grouped_reports_reported = False
+        for f in reports:
+            if f in grouped_reports or not self.load_report(f):
                 continue
+
             if self.report['ProblemType'] == 'Hang':
                 self.finish_hang(f)
             else:
-                self.run_crash(f)
+                if not grouped_reports_reported:
+                    grouped_reports_reported = True
+                    self.grouped_reports = grouped_reports
+                    self.run_crash(f)
+
+                    # 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)
+
+                    self.grouped_reports = None
+                else:
+                    self.run_crash(f)
             result = True
 
         return result
 
+    def get_reports(self):
+        '''Return an iterator over all the reports and their respective
+        files.
+        '''
+        r = [(self.report_file, self.report)]
+
+        if self.grouped_reports:
+            grouped = self.grouped_reports.items()
+            return itertools.chain(r, grouped)
+        else:
+            return r
+
     def run_crash(self, report_file, confirm=True):
         '''Present and report a particular crash.
 
@@ -244,44 +302,17 @@
             if 'Ignore' in self.report:
                 return
 
-            # check for absent CoreDumps (removed if they exceed size limit)
-            if self.report.get('ProblemType') == 'Crash' and 'Signal' in self.report and 'CoreDump' not in self.report and 'Stacktrace' not in self.report:
-                subject = os.path.basename(self.report.get('ExecutablePath', _('unknown program')))
-                heading = _('Sorry, the program "%s" closed unexpectedly') % subject
-                self.ui_error_message(
-                    _('Problem in %s') % subject,
-                    '%s\n\n%s' % (heading, _('Your computer does not have '
-                    'enough free memory to automatically analyze the problem '
-                    'and send a report to the developers.')))
-                return
+            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 processed.')
+                    machine_reason = 'No core dump'
+                    mark_invalid(report, path, reason, machine_reason)
 
             allowed_to_report = apport.fileutils.allowed_to_report()
             response = self.ui_present_report_details(allowed_to_report)
             if response['report'] or response['examine']:
-                try:
-                    if 'Dependencies' not in self.report:
-                        self.collect_info()
-                except (IOError, zlib.error) as e:
-                    # can happen with broken core dumps
-                    self.report = None
-                    self.ui_error_message(
-                        _('Invalid problem report'), '%s\n\n%s' % (
-                            _('This problem report is damaged and cannot be processed.'),
-                            repr(e)))
-                    self.ui_shutdown()
-                    return
-                except ValueError:  # package does not exist
-                    self.ui_error_message(_('Invalid problem report'),
-                                          _('The report belongs to a package that is not installed.'))
-                    self.ui_shutdown()
-                    return
-                except Exception as e:
-                    apport.error(repr(e))
-                    self.ui_error_message(_('Invalid problem report'),
-                                          _('An error occurred while attempting to process this'
-                                            ' problem report:') + '\n\n' + str(e))
-                    self.ui_shutdown()
-                    return
+                self.collect_all_info()
 
             if self.report is None:
                 # collect() does that on invalid reports
@@ -297,21 +328,22 @@
             if not response['report']:
                 return
 
-            apport.fileutils.mark_report_upload(report_file)
-            # We check for duplicates and unreportable crashes here, rather
-            # than before we show the dialog, as we want to submit these to the
-            # crash database, but not Launchpad.
-            if self.crashdb.accepts(self.report):
-                # FIXME: This behaviour is not really correct, but necessary as
-                # long as we only support a single crashdb and have whoopsie
-                # hardcoded. Once we have multiple crash dbs, we need to check
-                # accepts() earlier, and not even present the data if none of
-                # the DBs wants the report. See LP#957177 for details.
-                if self.handle_duplicate():
-                    return
-                if self.check_unreportable():
-                    return
-                self.file_report()
+            for path, report in self.get_reports():
+                apport.fileutils.mark_report_upload(path)
+                # We check for duplicates and unreportable crashes here, rather
+                # than before we show the dialog, as we want to submit these to the
+                # crash database, but not Launchpad.
+                if self.crashdb.accepts(report):
+                    # FIXME: This behaviour is not really correct, but necessary as
+                    # long as we only support a single crashdb and have whoopsie
+                    # hardcoded. Once we have multiple crash dbs, we need to check
+                    # accepts() earlier, and not even present the data if none of
+                    # the DBs wants the report. See LP#957177 for details.
+                    if self.handle_duplicate(report):
+                        return
+                    if self.check_unreportable(report):
+                        return
+                    self.file_report(report)
         except IOError as e:
             # fail gracefully if file is not readable for us
             if e.errno in (errno.EPERM, errno.EACCES):
@@ -448,7 +480,8 @@
             self.cur_package = self.options.package
 
         try:
-            self.collect_info(symptom_script)
+            self.collect_info(self.report, self.report_file, self.cur_package,
+                              symptom_script)
         except ValueError as e:
             if 'package' in str(e) and 'does not exist' in str(e):
                 if not self.cur_package:
@@ -461,12 +494,12 @@
             else:
                 raise
 
-        if self.check_unreportable():
+        if self.check_unreportable(self.report):
             return
 
         self.add_extra_tags()
 
-        if self.handle_duplicate():
+        if self.handle_duplicate(self.report):
             return True
 
         # not useful for bug reports, and has potentially sensitive information
@@ -486,7 +519,7 @@
             allowed_to_report = apport.fileutils.allowed_to_report()
             response = self.ui_present_report_details(allowed_to_report)
             if response['report']:
-                self.file_report()
+                self.file_report(self.report)
 
         return True
 
@@ -537,7 +570,8 @@
                 if not os.path.exists(os.path.join(apport.report._hook_dir, 'source_%s.py' % p)):
                     print('Package %s not installed and no hook available, ignoring' % p)
                     continue
-            self.collect_info(ignore_uninstalled=True)
+            self.collect_info(self.report, self.report_file, self.cur_package,
+                              ignore_uninstalled=True)
             info_collected = True
 
         if not info_collected:
@@ -933,7 +967,45 @@
 
         return True
 
-    def collect_info(self, symptom_script=None, ignore_uninstalled=False,
+    def exception_wrapped_collect_info(self, report, report_file=None,
+                                       cur_package=None, symptom_script=None,
+                                       ignore_uninstalled=False,
+                                       on_finished=None):
+        try:
+            self.collect_info(report, report_file, cur_package, symptom_script,
+                              ignore_uninstalled, on_finished)
+        except (IOError, zlib.error, struct.error) as e:
+            # can happen with broken core dumps
+            reason = _('This problem report is damaged and cannot be processed.')
+            machine_reason = excstr(e)
+            mark_invalid(report, report_file, reason, machine_reason)
+        except ValueError as e:  # package does not exist
+            reason = _('The report belongs to a package that is not installed.')
+            machine_reason = excstr(e)
+            mark_invalid(report, report_file, reason, machine_reason)
+        except Exception as e:
+            reason = _('This problem report is damaged and cannot be processed.')
+            machine_reason = excstr(e)
+            mark_invalid(report, report_file, reason, machine_reason)
+
+    def collect_all_info(self, on_finished=None):
+        '''Collect additional information from an application report and from
+        all grouped reports.'''
+        if 'Dependencies' not in self.report:
+            self.exception_wrapped_collect_info(self.report,
+                                                self.report_file,
+                                                self.cur_package)
+
+        for path in self.grouped_reports:
+            report = self.grouped_reports[path]
+            if 'Dependencies' not in report:
+                self.exception_wrapped_collect_info(report, path, self.packages[path])
+
+        if on_finished:
+            on_finished()
+
+    def collect_info(self, report, report_file=None, cur_package=None,
+                     symptom_script=None, ignore_uninstalled=False,
                      on_finished=None):
         '''Collect additional information.
 
@@ -947,25 +1019,25 @@
         run_symptom()).
         '''
         # check if binary changed since the crash happened
-        if 'ExecutablePath' in self.report and 'ExecutableTimestamp' in self.report:
-            orig_time = int(self.report['ExecutableTimestamp'])
-            del self.report['ExecutableTimestamp']
-            cur_time = int(os.stat(self.report['ExecutablePath']).st_mtime)
+        if 'ExecutablePath' in report and 'ExecutableTimestamp' in report:
+            orig_time = int(report['ExecutableTimestamp'])
+            del report['ExecutableTimestamp']
+            cur_time = int(os.stat(report['ExecutablePath']).st_mtime)
 
             if orig_time != cur_time:
-                self.report['UnreportableReason'] = (
+                report['UnreportableReason'] = (
                     _('The problem happened with the program %s which changed '
-                      'since the crash occurred.') % self.report['ExecutablePath'])
+                      'since the crash occurred.') % report['ExecutablePath'])
                 return
 
-        if not self.cur_package and 'ExecutablePath' not in self.report \
+        if not cur_package and 'ExecutablePath' not in report \
                 and not symptom_script:
             # this happens if we file a bug without specifying a PID or a
             # package
-            self.report.add_os_info()
+            report.add_os_info()
         else:
             # check if we already ran, skip if so
-            if (self.report.get('ProblemType') == 'Crash' and 'Stacktrace' in self.report) or (self.report.get('ProblemType') != 'Crash' and 'Dependencies' in self.report):
+            if (report.get('ProblemType') == 'Crash' and 'Stacktrace' in report) or (report.get('ProblemType') != 'Crash' and 'Dependencies' in report):
                 if on_finished:
                     on_finished()
                 return
@@ -976,12 +1048,12 @@
 
             hookui = HookUI(self)
 
-            if 'Stacktrace' not in self.report:
+            if 'Stacktrace' not in report:
                 # save original environment, in case hooks change it
                 orig_env = os.environ.copy()
                 icthread = apport.REThread.REThread(target=thread_collect_info,
                                                     name='thread_collect_info',
-                                                    args=(self.report, self.report_file, self.cur_package,
+                                                    args=(report, report_file, cur_package,
                                                           hookui, symptom_script, ignore_uninstalled))
                 icthread.start()
                 while icthread.isAlive():
@@ -1005,8 +1077,8 @@
                 return
 
             # check bug patterns
-            if self.report['ProblemType'] == 'KernelCrash' or self.report['ProblemType'] == 'KernelOops' or 'Package' in self.report:
-                bpthread = apport.REThread.REThread(target=self.report.search_bug_patterns,
+            if report['ProblemType'] == 'KernelCrash' or report['ProblemType'] == 'KernelOops' or 'Package' in report:
+                bpthread = apport.REThread.REThread(target=report.search_bug_patterns,
                                                     args=(self.crashdb.get_bugpattern_baseurl(),))
                 bpthread.start()
                 while bpthread.isAlive():
@@ -1017,12 +1089,12 @@
                         sys.exit(1)
                 bpthread.exc_raise()
                 if bpthread.return_value():
-                    self.report['KnownReport'] = bpthread.return_value()
+                    report['KnownReport'] = bpthread.return_value()
 
             # check crash database if problem is known
-            if self.report['ProblemType'] != 'Bug':
+            if report['ProblemType'] != 'Bug':
                 known_thread = apport.REThread.REThread(target=self.crashdb.known,
-                                                        args=(self.report,))
+                                                        args=(report,))
                 known_thread.start()
                 while known_thread.isAlive():
                     self.ui_pulse_info_collection_progress()
@@ -1034,13 +1106,13 @@
                 val = known_thread.return_value()
                 if val is not None:
                     if val is True:
-                        self.report['KnownReport'] = '1'
+                        report['KnownReport'] = '1'
                     else:
-                        self.report['KnownReport'] = val
+                        report['KnownReport'] = val
 
             # anonymize; needs to happen after duplicate checking, otherwise we
             # might damage the stack trace
-            anonymize_thread = apport.REThread.REThread(target=self.report.anonymize)
+            anonymize_thread = apport.REThread.REThread(target=report.anonymize)
             anonymize_thread.start()
             while anonymize_thread.isAlive():
                 self.ui_pulse_info_collection_progress()
@@ -1053,10 +1125,10 @@
             self.ui_stop_info_collection_progress()
 
             # check that we were able to determine package names
-            if 'UnreportableReason' not in self.report:
-                if ('SourcePackage' not in self.report or
-                    (not self.report['ProblemType'].startswith('Kernel')
-                     and 'Package' not in self.report)):
+            if 'UnreportableReason' not in report:
+                if ('SourcePackage' not in report or
+                    (not report['ProblemType'].startswith('Kernel')
+                     and 'Package' not in report)):
                     self.ui_error_message(_('Invalid problem report'),
                                           _('Could not determine the package or source package name.'))
                     # TODO This is not called consistently, is it really needed?
@@ -1110,26 +1182,26 @@
             os.write(w, str(e))
             sys.exit(1)
 
-    def file_report(self):
+    def file_report(self, report):
         '''Upload the current report and guide the user to the reporting web page.'''
         # FIXME: This behaviour is not really correct, but necessary as
         # long as we only support a single crashdb and have whoopsie
         # hardcoded. Once we have multiple crash dbs, we need to check
         # accepts() earlier, and not even present the data if none of
         # the DBs wants the report. See LP#957177 for details.
-        if not self.crashdb.accepts(self.report):
+        if not self.crashdb.accepts(report):
             return
         # drop PackageArchitecture if equal to Architecture
-        if self.report.get('PackageArchitecture') == self.report.get('Architecture'):
+        if report.get('PackageArchitecture') == report.get('Architecture'):
             try:
-                del self.report['PackageArchitecture']
+                del report['PackageArchitecture']
             except KeyError:
                 pass
 
         # StacktraceAddressSignature is redundant and does not need to clutter
         # the database
         try:
-            del self.report['StacktraceAddressSignature']
+            del report['StacktraceAddressSignature']
         except KeyError:
             pass
 
@@ -1142,7 +1214,7 @@
 
         self.ui_start_upload_progress()
         upthread = apport.REThread.REThread(target=self.crashdb.upload,
-                                            args=(self.report, progress_callback))
+                                            args=(report, progress_callback))
         upthread.start()
         while upthread.isAlive():
             self.ui_set_upload_progress(__upload_progress)
@@ -1159,7 +1231,7 @@
                     user, password = data
                     self.crashdb.set_credentials(user, password)
                     upthread = apport.REThread.REThread(target=self.crashdb.upload,
-                                                        args=(self.report, progress_callback))
+                                                        args=(report, progress_callback))
                     upthread.start()
             except (TypeError, SyntaxError, ValueError):
                 raise
@@ -1174,7 +1246,7 @@
         ticket = upthread.return_value()
         self.ui_stop_upload_progress()
 
-        url = self.crashdb.get_comment_url(self.report, ticket)
+        url = self.crashdb.get_comment_url(report, ticket)
         if url:
             self.open_url(url)
 
@@ -1185,27 +1257,26 @@
         be processed, otherwise self.report is initialized and True is
         returned.
         '''
+        self.report = apport.Report()
         try:
-            self.report = apport.Report()
             with open(path, 'rb') as f:
                 self.report.load(f, binary='compressed')
             if 'ProblemType' not in self.report:
                 raise ValueError('Report does not contain "ProblemType" field')
-        except MemoryError:
-            self.report = None
-            self.ui_error_message(_('Memory exhaustion'),
-                                  _('Your system does not have enough memory to process this crash report.'))
+        except MemoryError as e:
+            reason = _('Your system does not have enough memory to process this crash report.')
+            machine_reason = excstr(e)
+            mark_invalid(self.report, path, reason, machine_reason)
             return False
         except IOError as e:
-            self.report = None
-            self.ui_error_message(_('Invalid problem report'), e.strerror)
+            reason = _('Invalid problem report')
+            machine_reason = excstr(e)
+            mark_invalid(self.report, path, reason, machine_reason)
             return False
         except (TypeError, ValueError, AssertionError, zlib.error) as e:
-            self.report = None
-            self.ui_error_message(_('Invalid problem report'),
-                                  '%s\n\n%s' % (
-                                      _('This problem report is damaged and cannot be processed.'),
-                                      repr(e)))
+            reason = _('This problem report is damaged and cannot be processed.')
+            machine_reason = excstr(e)
+            mark_invalid(self.report, path, reason, machine_reason)
             return False
 
         if 'Package' in self.report:
@@ -1217,38 +1288,38 @@
         if self.report['ProblemType'] == 'Crash':
             exe_path = self.report.get('ExecutablePath', '')
             if not os.path.exists(exe_path):
-                msg = _('This problem report applies to a program which is not installed any more.')
-                if exe_path:
-                    msg = '%s (%s)' % (msg, self.report['ExecutablePath'])
-                self.report = None
-                self.ui_info_message(_('Invalid problem report'), msg)
+                reason = _('This problem report applies to a program which is not installed any more.')
+                machine_reason = "ENOENT: '%s'" % exe_path
+                mark_invalid(self.report, path, reason, machine_reason)
                 return False
 
             if 'InterpreterPath' in self.report:
                 if not os.path.exists(self.report['InterpreterPath']):
-                    msg = _('This problem report applies to a program which is not installed any more.')
-                    self.ui_info_message(_('Invalid problem report'), '%s (%s)'
-                                         % (msg, self.report['InterpreterPath']))
+                    reason = _('This problem report applies to a program which is not installed any more.')
+                    machine_reason = "ENOENT: '%s'" % self.report['InterpreterPath']
+                    mark_invalid(self.report, path, reason, machine_reason)
                     return False
 
         return True
 
-    def check_unreportable(self):
+    def check_unreportable(self, report):
         '''Check if the current report is unreportable.
 
         If so, display an info message and return True.
         '''
-        if not self.crashdb.accepts(self.report):
+        if not self.crashdb.accepts(report):
             return False
-        if 'UnreportableReason' in self.report:
-            if type(self.report['UnreportableReason']) == bytes:
-                self.report['UnreportableReason'] = self.report['UnreportableReason'].decode('UTF-8')
-            if 'Package' in self.report:
-                title = _('Problem in %s') % self.report['Package'].split()[0]
+        if 'InvalidReason' in report:
+            return True
+        if 'UnreportableReason' in report:
+            if type(report['UnreportableReason']) == bytes:
+                report['UnreportableReason'] = report['UnreportableReason'].decode('UTF-8')
+            if 'Package' in report:
+                title = _('Problem in %s') % report['Package'].split()[0]
             else:
                 title = ''
             self.ui_info_message(title, _('The problem cannot be reported:\n\n%s') %
-                                 self.report['UnreportableReason'])
+                                 report['UnreportableReason'])
             return True
         return False
 
@@ -1284,26 +1355,26 @@
             return None
         return result
 
-    def handle_duplicate(self):
+    def handle_duplicate(self, report):
         '''Check if current report matches a bug pattern.
 
         If so, tell the user about it, open the existing bug in a browser, and
         return True.
         '''
-        if not self.crashdb.accepts(self.report):
+        if not self.crashdb.accepts(report):
             return False
-        if 'KnownReport' not in self.report:
+        if 'KnownReport' not in report:
             return False
 
         # if we have an URL, open it; otherwise this is just a marker that we
         # know about it
-        if self.report['KnownReport'].startswith('http'):
+        if report['KnownReport'].startswith('http'):
             self.ui_info_message(_('Problem already known'),
                                  _('This problem was already reported in the bug report displayed \
 in the web browser. Please check if you can add any further information that \
 might be helpful for the developers.'))
 
-            self.open_url(self.report['KnownReport'])
+            self.open_url(report['KnownReport'])
         else:
             self.ui_info_message(_('Problem already known'),
                                  _('This problem was already reported to developers. Thank you!'))

=== modified file 'gtk/apport-gtk'
--- gtk/apport-gtk	2012-11-23 14:23:46 +0000
+++ gtk/apport-gtk	2013-01-23 21:33:24 +0000
@@ -11,7 +11,7 @@
 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
 # the full text of the license.
 
-import os.path, sys, subprocess, os, re, errno
+import os.path, sys, subprocess, os, re, errno, itertools
 
 from gi.repository import GObject, GLib, Wnck, GdkX11, Gdk
 try:
@@ -63,11 +63,13 @@
         column = Gtk.TreeViewColumn('Report', Gtk.CellRendererText(), text=0)
         self.w('details_treeview').append_column(column)
         self.spinner = self.add_spinner_over_treeview(self.w('details_treeview'))
+        self.w('details_treeview').set_row_separator_func(self.on_row_separator, None)
 
         self.md = None
 
         self.desktop_info = None
 
+
     #
     # ui_* implementation of abstract UserInterface classes
     #
@@ -100,42 +102,56 @@
         if not self.w('details_treeview').get_property('visible'):
             return
 
-        if shown_keys:
-            keys = set(self.report.keys()) & set(shown_keys)
-        else:
-            keys = self.report.keys()
-        # show the most interesting items on top
-        keys = sorted(keys)
-        for k in ('Traceback', 'StackTrace', 'Title', 'ProblemType', 'Package', 'ExecutablePath'):
-            if k in keys:
-                keys.remove(k)
-                keys.insert(0, k)
-
         self.tree_model.clear()
-        for key in keys:
-            keyiter = self.tree_model.insert_before(None, None)
-            self.tree_model.set_value(keyiter, 0, key)
-
-            valiter = self.tree_model.insert_before(keyiter, None)
-            if not hasattr(self.report[key], 'gzipvalue') and \
-                hasattr(self.report[key], 'isspace') and \
-                not self.report._is_binary(self.report[key]):
-                v = self.report[key]
-                if len(v) > 4000:
-                    v = v[:4000]
+
+        for report in itertools.chain([self.report], self.grouped_reports.values()):
+            if shown_keys:
+                keys = set(report.keys()) & set(shown_keys)
+            else:
+                keys = report.keys()
+            # show the most interesting items on top
+            keys = sorted(keys)
+            for k in ('Traceback', 'StackTrace', 'Title', 'ProblemType', 'Package', 'ExecutablePath'):
+                if k in keys:
+                    keys.remove(k)
+                    keys.insert(0, k)
+
+            if report != self.report:
+                # insert separator
+                keyiter = self.tree_model.insert_before(None, None)
+                self.tree_model.set_value(keyiter, 0, '')
+
+            if 'InvalidReason' in keys:
+                # This report is invalid, but we need to tell the user that
+                # we're going to send it for statistical purposes.
+                keyiter = self.tree_model.insert_before(None, None)
+                self.tree_model.set_value(keyiter, 0, report['InvalidReason'])
+                continue
+
+            for key in keys:
+                keyiter = self.tree_model.insert_before(None, None)
+                self.tree_model.set_value(keyiter, 0, key)
+
+                valiter = self.tree_model.insert_before(keyiter, None)
+                if not hasattr(report[key], 'gzipvalue') and \
+                    hasattr(report[key], 'isspace') and \
+                    not report._is_binary(report[key]):
+                    v = report[key]
+                    if len(v) > 4000:
+                        v = v[:4000]
+                        if type(v) == bytes:
+                            v += b'\n[...]'
+                        else:
+                            v += '\n[...]'
                     if type(v) == bytes:
-                        v += b'\n[...]'
-                    else:
-                        v += '\n[...]'
-                if type(v) == bytes:
-                    v = v.decode('UTF-8', errors='replace')
-                self.tree_model.set_value(valiter, 0, v)
-                # expand the row if the value has less than 5 lines
-                if len(list(filter(lambda c: c == '\n', self.report[key]))) < 4:
-                    self.w('details_treeview').expand_row(
-                        self.tree_model.get_path(keyiter), False)
-            else:
-                self.tree_model.set_value(valiter, 0, _('(binary data)'))
+                        v = v.decode('UTF-8', errors='replace')
+                    self.tree_model.set_value(valiter, 0, v)
+                    # expand the row if the value has less than 5 lines
+                    if len(list(filter(lambda c: c == '\n', report[key]))) < 4:
+                        self.w('details_treeview').expand_row(
+                            self.tree_model.get_path(keyiter), False)
+                else:
+                    self.tree_model.set_value(valiter, 0, _('(binary data)'))
 
     def get_system_application_title(self):
         '''Get dialog title for a non-.desktop application.
@@ -207,6 +223,14 @@
             self.w('send_error_report').set_active(False)
             self.w('send_error_report').hide()
 
+        if self.grouped_reports and allowed_to_report:
+            self.w('previous_internal_errors').set_active(True)
+            self.w('previous_internal_errors').show()
+            self.w('ignore_future_problems').hide()
+        else:
+            self.w('previous_internal_errors').set_active(False)
+            self.w('previous_internal_errors').hide()
+
         self.w('examine').set_visible(self.can_examine_locally())
 
         self.w('continue_button').set_label(_('Continue'))
@@ -346,8 +370,18 @@
         if len(sys.argv) == 1:
             d.set_focus_on_map(False)
 
-        return_value = { 'report' : False, 'blacklist' : False,
-                         'restart' : False, 'examine' : False }
+        return_value = { 
+            # Should the problem be reported?
+            'report' : False,
+            # Should future instances of this type be ignored?
+            'blacklist' : False,
+            # Should the application be restarted?
+            'restart' : False,
+            # Has an interactive debugging session been requested?
+            'examine' : False,
+            # Should grouped reports also be reported?
+            'grouped': False
+        }
         def dialog_crash_dismissed(widget):
             self.w('dialog_crash_new').hide()
             if widget is self.w('dialog_crash_new'):
@@ -364,6 +398,8 @@
                 return_value['blacklist'] = True
             if widget == self.w('continue_button') and self.desktop_info:
                 return_value['restart'] = True
+            if self.w('previous_internal_errors').get_active():
+                return_value['grouped'] = True
             Gtk.main_quit()
 
         self.w('dialog_crash_new').connect('destroy', dialog_crash_dismissed)
@@ -561,6 +597,11 @@
     # Event handlers
     #
 
+    def on_row_separator(self, model, iterator, data):
+        if model[iterator][0] == '':
+            return True
+        return False
+
     def on_show_details_clicked(self, widget):
         sw = self.w('details_scrolledwindow')
         if sw.get_property('visible'):
@@ -574,7 +615,7 @@
             if not self.collect_called:
                 self.collect_called = True
                 self.ui_update_view(['ExecutablePath'])
-                GLib.idle_add(lambda: self.collect_info(on_finished=self.ui_update_view))
+                GLib.idle_add(lambda: self.collect_all_info(on_finished=self.ui_update_view))
         return True
 
     def on_progress_window_close_event(self, widget, event=None):

=== modified file 'gtk/apport-gtk.ui'
--- gtk/apport-gtk.ui	2012-03-02 12:03:31 +0000
+++ gtk/apport-gtk.ui	2013-01-23 21:33:24 +0000
@@ -27,11 +27,9 @@
             <child>
               <object class="GtkButton" id="button7">
                 <property name="label">gtk-cancel</property>
-                <property name="use_action_appearance">False</property>
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="receives_default">True</property>
-                <property name="use_action_appearance">False</property>
                 <property name="use_stock">True</property>
               </object>
               <packing>
@@ -43,11 +41,9 @@
             <child>
               <object class="GtkButton" id="button13">
                 <property name="label">gtk-ok</property>
-                <property name="use_action_appearance">False</property>
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="receives_default">True</property>
-                <property name="use_action_appearance">False</property>
                 <property name="use_stock">True</property>
               </object>
               <packing>
@@ -213,11 +209,9 @@
                     <child>
                       <object class="GtkCheckButton" id="send_error_report">
                         <property name="label" translatable="yes">Send an error report to help fix this problem</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="visible">True</property>
                         <property name="can_focus">True</property>
                         <property name="receives_default">False</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="xalign">0</property>
                         <property name="active">True</property>
                         <property name="draw_indicator">True</property>
@@ -231,11 +225,9 @@
                     <child>
                       <object class="GtkCheckButton" id="ignore_future_problems">
                         <property name="label" translatable="yes">Ignore future problems of this program version</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="visible">True</property>
                         <property name="can_focus">True</property>
                         <property name="receives_default">False</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="xalign">0</property>
                         <property name="draw_indicator">True</property>
                       </object>
@@ -245,6 +237,22 @@
                         <property name="position">1</property>
                       </packing>
                     </child>
+                    <child>
+                      <object class="GtkCheckButton" id="previous_internal_errors">
+                        <property name="label" translatable="yes">Report previous internal errors too</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="xalign">0.0099999997764825821</property>
+                        <property name="yalign">0.51999998092651367</property>
+                        <property name="draw_indicator">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">2</property>
+                      </packing>
+                    </child>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -285,11 +293,9 @@
                     <child>
                       <object class="GtkButton" id="show_details">
                         <property name="label" translatable="yes">Show Details</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="visible">True</property>
                         <property name="can_focus">True</property>
                         <property name="receives_default">True</property>
-                        <property name="use_action_appearance">False</property>
                         <signal name="clicked" handler="on_show_details_clicked" swapped="no"/>
                       </object>
                       <packing>
@@ -301,11 +307,9 @@
                     <child>
                       <object class="GtkButton" id="examine">
                         <property name="label" translatable="yes">_Examine locally</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="visible">True</property>
                         <property name="can_focus">True</property>
                         <property name="receives_default">True</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="use_underline">True</property>
                       </object>
                       <packing>
@@ -317,10 +321,8 @@
                     <child>
                       <object class="GtkButton" id="cancel_button">
                         <property name="label">gtk-cancel</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="can_focus">True</property>
                         <property name="receives_default">True</property>
-                        <property name="use_action_appearance">False</property>
                         <property name="use_stock">True</property>
                       </object>
                       <packing>
@@ -346,11 +348,9 @@
                 <child>
                   <object class="GtkButton" id="closed_button">
                     <property name="label" translatable="yes">Leave Closed</property>
-                    <property name="use_action_appearance">False</property>
                     <property name="visible">True</property>
                     <property name="can_focus">True</property>
                     <property name="receives_default">True</property>
-                    <property name="use_action_appearance">False</property>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -361,11 +361,9 @@
                 <child>
                   <object class="GtkButton" id="continue_button">
                     <property name="label" translatable="yes">Continue</property>
-                    <property name="use_action_appearance">False</property>
                     <property name="visible">True</property>
                     <property name="can_focus">True</property>
                     <property name="receives_default">True</property>
-                    <property name="use_action_appearance">False</property>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -466,12 +464,10 @@
             <child>
               <object class="GtkButton" id="button_cancel_collecting">
                 <property name="label">gtk-cancel</property>
-                <property name="use_action_appearance">False</property>
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="can_default">True</property>
                 <property name="receives_default">False</property>
-                <property name="use_action_appearance">False</property>
                 <property name="use_stock">True</property>
                 <signal name="clicked" handler="on_progress_window_close_event" swapped="no"/>
               </object>
@@ -569,12 +565,10 @@
             <child>
               <object class="GtkButton" id="button_cancel_upload">
                 <property name="label">gtk-cancel</property>
-                <property name="use_action_appearance">False</property>
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="can_default">True</property>
                 <property name="receives_default">False</property>
-                <property name="use_action_appearance">False</property>
                 <property name="use_stock">True</property>
                 <signal name="clicked" handler="on_progress_window_close_event" swapped="no"/>
               </object>

=== modified file 'kde/apport-kde'
--- kde/apport-kde	2012-11-23 14:23:46 +0000
+++ kde/apport-kde	2013-01-23 21:33:24 +0000
@@ -278,7 +278,7 @@
         self.treeview.setVisible(visible)
         if visible and not self.collect_called:
             self.ui.ui_update_view(self, ['ExecutablePath'])
-            QTimer.singleShot(0, lambda: self.ui.collect_info(on_finished=self.collect_done))
+            QTimer.singleShot(0, lambda: self.ui.collect_info(self.ui.report, on_finished=self.collect_done))
             self.collect_called = True
         if visible:
             self.setMaximumSize(16777215, 16777215)

=== modified file 'test/test_backend_apt_dpkg.py'
--- test/test_backend_apt_dpkg.py	2012-12-10 08:35:28 +0000
+++ test/test_backend_apt_dpkg.py	2013-01-23 21:33:24 +0000
@@ -762,7 +762,7 @@
         assert readelf.returncode == 0
         for line in out.splitlines():
             if line.startswith('  Machine:'):
-                machine = line.split(maxsplit=1)[1]
+                machine = line.split(' ', 1)[1]
                 break
         else:
             self.fail('could not fine Machine: in readelf output')

=== modified file 'test/test_ui.py'
--- test/test_ui.py	2012-08-24 10:31:13 +0000
+++ test/test_ui.py	2013-01-23 21:33:24 +0000
@@ -10,11 +10,11 @@
 from io import BytesIO
 
 import apport.ui
-from apport.ui import _
 import apport.report
 import problem_report
 import apport.crashdb_impl.memory
 import stat
+from mock import patch
 
 
 class TestSuiteUserInterface(apport.ui.UserInterface):
@@ -53,7 +53,7 @@
         # these store the choices the ui_present_* calls do
         self.present_package_error_response = None
         self.present_kernel_error_response = None
-        self.present_details_response = None
+        self.present_details_response = {}
         self.question_yesno_response = None
         self.question_choice_response = None
         self.question_file_response = None
@@ -264,11 +264,7 @@
         self.update_report_file()
         self.ui.load_report(self.report_file.name)
 
-        self.assertTrue(self.ui.report is None)
-        self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
-        self.assertEqual(self.ui.msg_severity, 'info')
-
-        self.ui.clear_msg()
+        self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason'])
 
         # invalid base64 encoding
         self.report_file.seek(0)
@@ -281,9 +277,7 @@
         self.report_file.flush()
 
         self.ui.load_report(self.report_file.name)
-        self.assertTrue(self.ui.report is None)
-        self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
-        self.assertEqual(self.ui.msg_severity, 'error')
+        self.assertTrue('damaged' in self.ui.report['InvalidReason'])
 
     def test_restart(self):
         '''restart()'''
@@ -323,7 +317,7 @@
 
         # report without any information (distro bug)
         self.ui.report = apport.Report()
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report)
         self.assertTrue(set(['Date', 'Uname', 'DistroRelease', 'ProblemType']).issubset(
             set(self.ui.report.keys())))
         self.assertEqual(self.ui.ic_progress_pulses, 0,
@@ -341,7 +335,7 @@
         # apport hooks)
         self.ui.report['Fstab'] = ('/etc/fstab', True)
         self.ui.report['CompressedValue'] = problem_report.CompressedValue(b'Test')
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report)
         self.assertTrue(set(['SourcePackage', 'Package', 'ProblemType',
                              'Uname', 'Dependencies', 'DistroRelease', 'Date',
                              'ExecutablePath']).issubset(set(self.ui.report.keys())))
@@ -356,7 +350,7 @@
         # report with only package information
         self.ui.report = apport.Report('Bug')
         self.ui.cur_package = 'bash'
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
         self.assertTrue(set(['SourcePackage', 'Package', 'ProblemType',
                              'Uname', 'Dependencies', 'DistroRelease',
                              'Date']).issubset(set(self.ui.report.keys())))
@@ -371,7 +365,7 @@
         self.ui.report = apport.Report('Bug')
         self.ui.cur_package = 'bash'
         self.ui.report_file = self.report_file.name
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report, self.ui.report_file, cur_package=self.ui.cur_package)
         self.assertTrue(os.stat(self.report_file.name).st_mode & stat.S_IRGRP)
 
     def test_collect_info_crashdb_spec(self):
@@ -386,7 +380,7 @@
 
         self.ui.report = apport.Report('Bug')
         self.ui.cur_package = 'bash'
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
         self.assertTrue('CrashDB' in self.ui.report)
         self.assertFalse('UnreportableReason' in self.ui.report,
                          self.ui.report.get('UnreportableReason'))
@@ -405,7 +399,7 @@
 
         self.ui.report = apport.Report('Bug')
         self.ui.cur_package = 'bash'
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
         self.assertFalse('UnreportableReason' in self.ui.report,
                          self.ui.report.get('UnreportableReason'))
         self.assertEqual(self.ui.report['BashHook'], 'Moo')
@@ -422,7 +416,7 @@
 
         self.ui.report = apport.Report('Bug')
         self.ui.cur_package = 'bash'
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
         self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'],
                         self.ui.report.get('UnreportableReason', '<not set>'))
 
@@ -434,7 +428,7 @@
 
         self.ui.report = apport.Report('Bug')
         self.ui.cur_package = 'bash'
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
         self.assertTrue('package hook' in self.ui.report['UnreportableReason'],
                         self.ui.report.get('UnreportableReason', '<not set>'))
 
@@ -446,7 +440,7 @@
 
         self.ui.report = apport.Report('Bug')
         self.ui.cur_package = 'bash'
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
         self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'],
                         self.ui.report.get('UnreportableReason', '<not set>'))
 
@@ -454,7 +448,7 @@
         '''handle_duplicate()'''
 
         self.ui.load_report(self.report_file.name)
-        self.assertEqual(self.ui.handle_duplicate(), False)
+        self.assertEqual(self.ui.handle_duplicate(self.ui.report), False)
         self.assertEqual(self.ui.msg_title, None)
         self.assertEqual(self.ui.opened_url, None)
 
@@ -462,7 +456,7 @@
         self.report['KnownReport'] = demo_url
         self.update_report_file()
         self.ui.load_report(self.report_file.name)
-        self.assertEqual(self.ui.handle_duplicate(), True)
+        self.assertEqual(self.ui.handle_duplicate(self.ui.report), True)
         self.assertEqual(self.ui.msg_severity, 'info')
         self.assertEqual(self.ui.opened_url, demo_url)
 
@@ -471,7 +465,7 @@
         self.report['KnownReport'] = '1'
         self.update_report_file()
         self.ui.load_report(self.report_file.name)
-        self.assertEqual(self.ui.handle_duplicate(), True)
+        self.assertEqual(self.ui.handle_duplicate(self.ui.report), True)
         self.assertEqual(self.ui.msg_severity, 'info')
         self.assertEqual(self.ui.opened_url, None)
 
@@ -742,6 +736,76 @@
 
         return r
 
+    @patch.object(apport.fileutils, 'get_new_reports')
+    def test_run_crashes(self, patched_fileutils):
+        '''run_crashes()'''
+        r = self._gen_test_crash()
+        self.ui = TestSuiteUserInterface()
+
+        files = [os.path.join(apport.fileutils.report_dir, '%s%i.crash' % t)
+                 for t in zip(['test'] * 3, range(3))]
+        patched_fileutils.return_value = files
+
+        mtimes = []
+        for report_file in files:
+            with open(report_file, 'wb') as f:
+                r.write(f)
+            mtimes.append(os.stat(report_file).st_mtime)
+
+        # Only system internal reports. None should be processed at this time.
+        with patch.object(self.ui, 'run_crash') as patched_run_crash:
+            self.ui.run_crashes()
+        self.assertEqual(patched_run_crash.call_count, 0)
+
+        # They shouldn't be marked as seen.
+        new_mtimes = []
+        for report_file in files:
+            new_mtimes.append(os.stat(report_file).st_mtime)
+        self.assertEqual(mtimes, new_mtimes)
+
+        # Three system internal reports and one application report.
+        path = os.path.join(apport.fileutils.report_dir, 'test3.crash')
+        r['DesktopFile'] = glob.glob('/usr/share/applications/*.desktop')[0]
+        with open(path, 'wb') as f:
+            r.write(f)
+        files.append(path)
+        patched_fileutils.return_value = files
+
+        with patch.object(apport.fileutils, 'mark_report_upload') as p:
+            self.ui.present_details_response = {'report': True,
+                                                'blacklist': False,
+                                                'examine': False,
+                                                'restart': False}
+            self.ui.run_crashes()
+
+            # All reports should be uploaded.
+            self.assertEqual(p.call_count, 4)
+
+        # They should be marked as seen.
+        for i in range(len(files) - 1):
+            self.assertNotEqual(os.stat(files[i]).st_mtime, mtimes[i])
+
+        r = apport.Report()
+        with open(files[0], 'rb') as f:
+            r.load(f)
+        with open(files[0], 'wb') as f:
+            r['ExecutablePath'] = '/nonexistant'
+            r.write(f)
+
+        with patch.object(apport.fileutils, 'seen_report') as seen:
+            # Look at all the reports again.
+            seen.return_value = False
+            with patch.object(apport.fileutils, 'mark_report_upload') as p:
+                self.ui.present_details_response = {'report': True,
+                                                    'blacklist': False,
+                                                    'examine': False,
+                                                    'restart': False}
+                self.ui.run_crashes()
+                with open(files[0], 'rb') as f:
+                    r.load(f)
+                self.assertEqual(r['InvalidReason'],
+                                 'This problem report applies to a program which is not installed any more.')
+
     def test_run_crash(self):
         '''run_crash()'''
 
@@ -859,8 +923,7 @@
                                             'examine': False,
                                             'restart': False}
         self.ui.run_crash(report_file)
-        self.assertEqual(self.ui.msg_severity, 'error', self.ui.msg_text)
-        self.assertTrue('decompress' in self.ui.msg_text)
+        self.assertTrue('decompress' in self.ui.report['InvalidMachineReason'])
         self.assertTrue(self.ui.present_details_shown)
 
     def test_run_crash_argv_file(self):
@@ -967,10 +1030,12 @@
 
         # run
         self.ui = TestSuiteUserInterface()
+        self.ui.present_details_response = {'report': False,
+                                            'blacklist': False,
+                                            'examine': False,
+                                            'restart': False}
         self.ui.run_crash(report_file)
-        self.assertEqual(self.ui.msg_severity, 'error')
-        self.assertTrue('memory' in self.ui.msg_text, '%s: %s' %
-                        (self.ui.msg_title, self.ui.msg_text))
+        self.assertEqual(self.ui.report['InvalidMachineReason'], 'No core dump')
 
     def test_run_crash_preretraced(self):
         '''run_crash() pre-retraced reports.
@@ -1008,7 +1073,7 @@
         copying a .crash file.
         '''
         self.ui.report = self._gen_test_crash()
-        self.ui.collect_info()
+        self.ui.collect_info(self.ui.report)
 
         # now pretend to move it to a machine where the package is not
         # installed
@@ -1048,9 +1113,7 @@
                                             'examine': False,
                                             'restart': False}
         self.ui.run_crash(report_file)
-
-        self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
-        self.assertEqual(self.ui.msg_severity, 'error')
+        self.assertTrue('does not exist' in self.ui.report['InvalidMachineReason'])
 
     def test_run_crash_uninstalled(self):
         '''run_crash() on reports with subsequently uninstalled packages'''
@@ -1069,8 +1132,10 @@
                                             'restart': False}
         self.ui.run_crash(report_file)
 
-        self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
-        self.assertEqual(self.ui.msg_severity, 'info')
+        self.assertTrue('not installed' in self.ui.report['InvalidReason'])
+        self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason'])
+
+        self.ui.report = None
 
         # interpreted program got uninstalled between crash and report
         r = apport.Report()
@@ -1080,8 +1145,10 @@
 
         self.ui.run_crash(report_file)
 
-        self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
-        self.assertEqual(self.ui.msg_severity, 'info')
+        self.assertTrue('not installed' in self.ui.report['InvalidReason'])
+        self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason'])
+
+        self.ui.report = None
 
         # interpreter got uninstalled between crash and report
         r = apport.Report()
@@ -1091,8 +1158,8 @@
 
         self.ui.run_crash(report_file)
 
-        self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
-        self.assertEqual(self.ui.msg_severity, 'info')
+        self.assertTrue('not installed' in self.ui.report['InvalidReason'])
+        self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason'])
 
     def test_run_crash_updated_binary(self):
         '''run_crash() on binary that got updated in the meantime'''

=== modified file 'test/test_ui_gtk.py'
--- test/test_ui_gtk.py	2012-11-23 13:43:15 +0000
+++ test/test_ui_gtk.py	2013-01-23 21:33:24 +0000
@@ -18,6 +18,7 @@
 import apport
 import shutil
 import subprocess
+import glob
 from gi.repository import GLib, Gtk
 from apport import unicode_gettext as _
 from mock import patch
@@ -606,8 +607,10 @@
             self.app.w('continue_button').clicked()
             return False
 
-        # remove the crash from setUp() and create a kernel oops
-        os.remove(self.app.report_file)
+        self.app.report['DesktopFile'] = glob.glob('/usr/share/applications/*.desktop')[0]
+        self.app.report['ProcCmdline'] = 'apport-bug apport'
+        with open(self.app.report_file, 'wb') as f:
+            self.app.report.write(f)
         kernel_oops = subprocess.Popen([kernel_oops_path], stdin=subprocess.PIPE)
         kernel_oops.communicate(b'Plasma conduit phase misalignment')
         self.assertEqual(kernel_oops.returncode, 0)
@@ -616,8 +619,8 @@
         self.app.run_crashes()
 
         # we should have reported one crash
-        self.assertEqual(self.app.crashdb.latest_id(), 0)
-        r = self.app.crashdb.download(0)
+        self.assertEqual(self.app.crashdb.latest_id(), 1)
+        r = self.app.crashdb.download(1)
         self.assertEqual(r['ProblemType'], 'KernelOops')
         self.assertEqual(r['OopsText'], 'Plasma conduit phase misalignment')
 
@@ -627,7 +630,7 @@
         self.assertTrue('Plasma conduit' in r['Title'])
 
         # URL was opened
-        self.assertEqual(self.app.open_url.call_count, 1)
+        self.assertEqual(self.app.open_url.call_count, 2)
 
     def test_bug_report_installed_package(self):
         '''Bug report for installed package'''


Follow ups