Merge lp:~ev/apport/grouped_reports into lp:~apport-hackers/apport/trunk

Proposed by Evan
Status: Needs review
Proposed branch: lp:~ev/apport/grouped_reports
Merge into: lp:~apport-hackers/apport/trunk
Diff against target: 1323 lines (+404/-228)
7 files modified
apport/ui.py (+195/-124)
gtk/apport-gtk (+79/-38)
gtk/apport-gtk.ui (+16/-22)
kde/apport-kde (+1/-1)
test/test_backend_apt_dpkg.py (+1/-1)
test/test_ui.py (+104/-37)
test/test_ui_gtk.py (+8/-5)
To merge this branch: bzr merge lp:~ev/apport/grouped_reports
Reviewer Review Type Date Requested Status
Apport upstream developers 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.
Revision history for this message
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
2596. By Evan

Merge with trunk.

Unmerged revisions

2596. By Evan

Merge with trunk.

2595. By Evan

Split does not take keyword arguments.

2594. By Evan

Fix failing test from new collect_info interface

2593. By Evan

Fix the kernel oops test GTK test.

2592. By Evan

Fix UI tests.

2591. By Evan

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

2590. By Evan

Pass in the current package.

2589. By Evan

Update the tests to handle the new handle_duplicate function signature.

2588. By Evan

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

2587. By Evan

Properly initialise the grouped_reports dictionary.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'apport/ui.py'
--- apport/ui.py 2013-03-19 10:38:05 +0000
+++ apport/ui.py 2013-04-27 08:52:25 +0000
@@ -15,8 +15,8 @@
1515
16__version__ = '2.9.2'16__version__ = '2.9.2'
1717
18import glob, sys, os.path, optparse, traceback, locale, gettext, re18import glob, sys, os.path, optparse, traceback, locale, gettext, re, itertools
19import errno, zlib19import errno, zlib, struct
20import subprocess, threading, webbrowser20import subprocess, threading, webbrowser
21import signal21import signal
22import time22import time
@@ -150,6 +150,11 @@
150 #if report.get('Signal') == '6' and 'AssertionMessage' not in report:150 #if report.get('Signal') == '6' and 'AssertionMessage' not in report:
151 # report['UnreportableReason'] = _('The program crashed on an assertion failure, but the message could not be retrieved. Apport does not support reporting these crashes.')151 # report['UnreportableReason'] = _('The program crashed on an assertion failure, but the message could not be retrieved. Apport does not support reporting these crashes.')
152152
153 update_report(report, reportfile)
154
155
156def update_report(report, reportfile):
157 '''Write the report to disk.'''
153 if reportfile:158 if reportfile:
154 try:159 try:
155 with open(reportfile, 'ab') as f:160 with open(reportfile, 'ab') as f:
@@ -166,6 +171,16 @@
166 os.chmod(reportfile, 0o640)171 os.chmod(reportfile, 0o640)
167172
168173
174def mark_invalid(report, reportfile, reason, machine_reason):
175 '''Mark the report as invalid while continuing to process further reports.
176 The invalid report will appear in the UI with the 'reason' argument as its
177 text and will be uploaded to daisy.ubuntu.com for statistical purposes.
178 '''
179 report['InvalidReason'] = reason
180 report['InvalidMachineReason'] = machine_reason
181 update_report(report, reportfile)
182
183
169class UserInterface:184class UserInterface:
170 '''Apport user interface API.185 '''Apport user interface API.
171186
@@ -180,7 +195,9 @@
180 self.gettext_domain = 'apport'195 self.gettext_domain = 'apport'
181 self.report = None196 self.report = None
182 self.report_file = None197 self.report_file = None
198 self.grouped_reports = {}
183 self.cur_package = None199 self.cur_package = None
200 self.packages = {}
184201
185 try:202 try:
186 self.crashdb = apport.crashdb.get_crashdb(None)203 self.crashdb = apport.crashdb.get_crashdb(None)
@@ -211,17 +228,58 @@
211 reports = apport.fileutils.get_new_system_reports()228 reports = apport.fileutils.get_new_system_reports()
212 else:229 else:
213 reports = apport.fileutils.get_new_reports()230 reports = apport.fileutils.get_new_reports()
214 for f in reports:231
215 if not self.load_report(f):232 # Determine which reports should be grouped with the first regular
233 # crash rather than shown in a dialog on their own. These are referred
234 # to as 'silent' errors.
235 grouped_reports = {}
236 for f in reports:
237 self.load_report(f)
238 if not self.load_report(f) or not self.get_desktop_entry():
239 # We may not be able to load the report, but we should still
240 # show that it occurred and send the reason for statistical
241 # purposes.
242 grouped_reports[f] = self.report
243 self.packages[f] = self.cur_package
244
245 grouped_reports_reported = False
246 for f in reports:
247 if f in grouped_reports or not self.load_report(f):
216 continue248 continue
249
217 if self.report['ProblemType'] == 'Hang':250 if self.report['ProblemType'] == 'Hang':
218 self.finish_hang(f)251 self.finish_hang(f)
219 else:252 else:
220 self.run_crash(f)253 if not grouped_reports_reported:
254 grouped_reports_reported = True
255 self.grouped_reports = grouped_reports
256 self.run_crash(f)
257
258 # Make sure we don't report these again. Note that these
259 # grouped reports will not be marked as seen if we do not
260 # have a regular application error report to present.
261 for f in grouped_reports.keys():
262 apport.fileutils.mark_report_seen(f)
263
264 self.grouped_reports = None
265 else:
266 self.run_crash(f)
221 result = True267 result = True
222268
223 return result269 return result
224270
271 def get_reports(self):
272 '''Return an iterator over all the reports and their respective
273 files.
274 '''
275 r = [(self.report_file, self.report)]
276
277 if self.grouped_reports:
278 grouped = self.grouped_reports.items()
279 return itertools.chain(r, grouped)
280 else:
281 return r
282
225 def run_crash(self, report_file, confirm=True):283 def run_crash(self, report_file, confirm=True):
226 '''Present and report a particular crash.284 '''Present and report a particular crash.
227285
@@ -246,44 +304,17 @@
246 if 'Ignore' in self.report:304 if 'Ignore' in self.report:
247 return305 return
248306
249 # check for absent CoreDumps (removed if they exceed size limit)307 for path, report in self.get_reports():
250 if self.report.get('ProblemType') == 'Crash' and 'Signal' in self.report and 'CoreDump' not in self.report and 'Stacktrace' not in self.report:308 # check for absent CoreDumps (removed if they exceed size limit)
251 subject = os.path.basename(self.report.get('ExecutablePath', _('unknown program')))309 if report.get('ProblemType') == 'Crash' and 'Signal' in report and 'CoreDump' not in report and 'Stacktrace' not in report:
252 heading = _('Sorry, the program "%s" closed unexpectedly') % subject310 reason = _('This problem report is damaged and cannot be processed.')
253 self.ui_error_message(311 machine_reason = 'No core dump'
254 _('Problem in %s') % subject,312 mark_invalid(report, path, reason, machine_reason)
255 '%s\n\n%s' % (heading, _('Your computer does not have '
256 'enough free memory to automatically analyze the problem '
257 'and send a report to the developers.')))
258 return
259313
260 allowed_to_report = apport.fileutils.allowed_to_report()314 allowed_to_report = apport.fileutils.allowed_to_report()
261 response = self.ui_present_report_details(allowed_to_report)315 response = self.ui_present_report_details(allowed_to_report)
262 if response['report'] or response['examine']:316 if response['report'] or response['examine']:
263 try:317 self.collect_all_info()
264 if 'Dependencies' not in self.report:
265 self.collect_info()
266 except (IOError, zlib.error) as e:
267 # can happen with broken core dumps
268 self.report = None
269 self.ui_error_message(
270 _('Invalid problem report'), '%s\n\n%s' % (
271 _('This problem report is damaged and cannot be processed.'),
272 repr(e)))
273 self.ui_shutdown()
274 return
275 except ValueError: # package does not exist
276 self.ui_error_message(_('Invalid problem report'),
277 _('The report belongs to a package that is not installed.'))
278 self.ui_shutdown()
279 return
280 except Exception as e:
281 apport.error(repr(e))
282 self.ui_error_message(_('Invalid problem report'),
283 _('An error occurred while attempting to process this'
284 ' problem report:') + '\n\n' + str(e))
285 self.ui_shutdown()
286 return
287318
288 if self.report is None:319 if self.report is None:
289 # collect() does that on invalid reports320 # collect() does that on invalid reports
@@ -299,21 +330,22 @@
299 if not response['report']:330 if not response['report']:
300 return331 return
301332
302 apport.fileutils.mark_report_upload(report_file)333 for path, report in self.get_reports():
303 # We check for duplicates and unreportable crashes here, rather334 apport.fileutils.mark_report_upload(path)
304 # than before we show the dialog, as we want to submit these to the335 # We check for duplicates and unreportable crashes here, rather
305 # crash database, but not Launchpad.336 # than before we show the dialog, as we want to submit these to the
306 if self.crashdb.accepts(self.report):337 # crash database, but not Launchpad.
307 # FIXME: This behaviour is not really correct, but necessary as338 if self.crashdb.accepts(report):
308 # long as we only support a single crashdb and have whoopsie339 # FIXME: This behaviour is not really correct, but necessary as
309 # hardcoded. Once we have multiple crash dbs, we need to check340 # long as we only support a single crashdb and have whoopsie
310 # accepts() earlier, and not even present the data if none of341 # hardcoded. Once we have multiple crash dbs, we need to check
311 # the DBs wants the report. See LP#957177 for details.342 # accepts() earlier, and not even present the data if none of
312 if self.handle_duplicate():343 # the DBs wants the report. See LP#957177 for details.
313 return344 if self.handle_duplicate(report):
314 if self.check_unreportable():345 return
315 return346 if self.check_unreportable(report):
316 self.file_report()347 return
348 self.file_report(report)
317 except IOError as e:349 except IOError as e:
318 # fail gracefully if file is not readable for us350 # fail gracefully if file is not readable for us
319 if e.errno in (errno.EPERM, errno.EACCES):351 if e.errno in (errno.EPERM, errno.EACCES):
@@ -450,7 +482,8 @@
450 self.cur_package = self.options.package482 self.cur_package = self.options.package
451483
452 try:484 try:
453 self.collect_info(symptom_script)485 self.collect_info(self.report, self.report_file, self.cur_package,
486 symptom_script)
454 except ValueError as e:487 except ValueError as e:
455 if 'package' in str(e) and 'does not exist' in str(e):488 if 'package' in str(e) and 'does not exist' in str(e):
456 if not self.cur_package:489 if not self.cur_package:
@@ -463,12 +496,12 @@
463 else:496 else:
464 raise497 raise
465498
466 if self.check_unreportable():499 if self.check_unreportable(self.report):
467 return500 return
468501
469 self.add_extra_tags()502 self.add_extra_tags()
470503
471 if self.handle_duplicate():504 if self.handle_duplicate(self.report):
472 return True505 return True
473506
474 # not useful for bug reports, and has potentially sensitive information507 # not useful for bug reports, and has potentially sensitive information
@@ -488,7 +521,7 @@
488 allowed_to_report = apport.fileutils.allowed_to_report()521 allowed_to_report = apport.fileutils.allowed_to_report()
489 response = self.ui_present_report_details(allowed_to_report)522 response = self.ui_present_report_details(allowed_to_report)
490 if response['report']:523 if response['report']:
491 self.file_report()524 self.file_report(self.report)
492525
493 return True526 return True
494527
@@ -539,7 +572,8 @@
539 if not os.path.exists(os.path.join(apport.report._hook_dir, 'source_%s.py' % p)):572 if not os.path.exists(os.path.join(apport.report._hook_dir, 'source_%s.py' % p)):
540 print('Package %s not installed and no hook available, ignoring' % p)573 print('Package %s not installed and no hook available, ignoring' % p)
541 continue574 continue
542 self.collect_info(ignore_uninstalled=True)575 self.collect_info(self.report, self.report_file, self.cur_package,
576 ignore_uninstalled=True)
543 info_collected = True577 info_collected = True
544578
545 if not info_collected:579 if not info_collected:
@@ -935,7 +969,45 @@
935969
936 return True970 return True
937971
938 def collect_info(self, symptom_script=None, ignore_uninstalled=False,972 def exception_wrapped_collect_info(self, report, report_file=None,
973 cur_package=None, symptom_script=None,
974 ignore_uninstalled=False,
975 on_finished=None):
976 try:
977 self.collect_info(report, report_file, cur_package, symptom_script,
978 ignore_uninstalled, on_finished)
979 except (IOError, zlib.error, struct.error) as e:
980 # can happen with broken core dumps
981 reason = _('This problem report is damaged and cannot be processed.')
982 machine_reason = excstr(e)
983 mark_invalid(report, report_file, reason, machine_reason)
984 except ValueError as e: # package does not exist
985 reason = _('The report belongs to a package that is not installed.')
986 machine_reason = excstr(e)
987 mark_invalid(report, report_file, reason, machine_reason)
988 except Exception as e:
989 reason = _('This problem report is damaged and cannot be processed.')
990 machine_reason = excstr(e)
991 mark_invalid(report, report_file, reason, machine_reason)
992
993 def collect_all_info(self, on_finished=None):
994 '''Collect additional information from an application report and from
995 all grouped reports.'''
996 if 'Dependencies' not in self.report:
997 self.exception_wrapped_collect_info(self.report,
998 self.report_file,
999 self.cur_package)
1000
1001 for path in self.grouped_reports:
1002 report = self.grouped_reports[path]
1003 if 'Dependencies' not in report:
1004 self.exception_wrapped_collect_info(report, path, self.packages[path])
1005
1006 if on_finished:
1007 on_finished()
1008
1009 def collect_info(self, report, report_file=None, cur_package=None,
1010 symptom_script=None, ignore_uninstalled=False,
939 on_finished=None):1011 on_finished=None):
940 '''Collect additional information.1012 '''Collect additional information.
9411013
@@ -949,25 +1021,25 @@
949 run_symptom()).1021 run_symptom()).
950 '''1022 '''
951 # check if binary changed since the crash happened1023 # check if binary changed since the crash happened
952 if 'ExecutablePath' in self.report and 'ExecutableTimestamp' in self.report:1024 if 'ExecutablePath' in report and 'ExecutableTimestamp' in report:
953 orig_time = int(self.report['ExecutableTimestamp'])1025 orig_time = int(report['ExecutableTimestamp'])
954 del self.report['ExecutableTimestamp']1026 del report['ExecutableTimestamp']
955 cur_time = int(os.stat(self.report['ExecutablePath']).st_mtime)1027 cur_time = int(os.stat(report['ExecutablePath']).st_mtime)
9561028
957 if orig_time != cur_time:1029 if orig_time != cur_time:
958 self.report['UnreportableReason'] = (1030 report['UnreportableReason'] = (
959 _('The problem happened with the program %s which changed '1031 _('The problem happened with the program %s which changed '
960 'since the crash occurred.') % self.report['ExecutablePath'])1032 'since the crash occurred.') % report['ExecutablePath'])
961 return1033 return
9621034
963 if not self.cur_package and 'ExecutablePath' not in self.report \1035 if not cur_package and 'ExecutablePath' not in report \
964 and not symptom_script:1036 and not symptom_script:
965 # this happens if we file a bug without specifying a PID or a1037 # this happens if we file a bug without specifying a PID or a
966 # package1038 # package
967 self.report.add_os_info()1039 report.add_os_info()
968 else:1040 else:
969 # check if we already ran, skip if so1041 # check if we already ran, skip if so
970 if (self.report.get('ProblemType') == 'Crash' and 'Stacktrace' in self.report) or (self.report.get('ProblemType') != 'Crash' and 'Dependencies' in self.report):1042 if (report.get('ProblemType') == 'Crash' and 'Stacktrace' in report) or (report.get('ProblemType') != 'Crash' and 'Dependencies' in report):
971 if on_finished:1043 if on_finished:
972 on_finished()1044 on_finished()
973 return1045 return
@@ -978,12 +1050,12 @@
9781050
979 hookui = HookUI(self)1051 hookui = HookUI(self)
9801052
981 if 'Stacktrace' not in self.report:1053 if 'Stacktrace' not in report:
982 # save original environment, in case hooks change it1054 # save original environment, in case hooks change it
983 orig_env = os.environ.copy()1055 orig_env = os.environ.copy()
984 icthread = apport.REThread.REThread(target=thread_collect_info,1056 icthread = apport.REThread.REThread(target=thread_collect_info,
985 name='thread_collect_info',1057 name='thread_collect_info',
986 args=(self.report, self.report_file, self.cur_package,1058 args=(report, report_file, cur_package,
987 hookui, symptom_script, ignore_uninstalled))1059 hookui, symptom_script, ignore_uninstalled))
988 icthread.start()1060 icthread.start()
989 while icthread.isAlive():1061 while icthread.isAlive():
@@ -1007,8 +1079,8 @@
1007 return1079 return
10081080
1009 # check bug patterns1081 # check bug patterns
1010 if self.report['ProblemType'] == 'KernelCrash' or self.report['ProblemType'] == 'KernelOops' or 'Package' in self.report:1082 if report['ProblemType'] == 'KernelCrash' or report['ProblemType'] == 'KernelOops' or 'Package' in report:
1011 bpthread = apport.REThread.REThread(target=self.report.search_bug_patterns,1083 bpthread = apport.REThread.REThread(target=report.search_bug_patterns,
1012 args=(self.crashdb.get_bugpattern_baseurl(),))1084 args=(self.crashdb.get_bugpattern_baseurl(),))
1013 bpthread.start()1085 bpthread.start()
1014 while bpthread.isAlive():1086 while bpthread.isAlive():
@@ -1019,12 +1091,12 @@
1019 sys.exit(1)1091 sys.exit(1)
1020 bpthread.exc_raise()1092 bpthread.exc_raise()
1021 if bpthread.return_value():1093 if bpthread.return_value():
1022 self.report['KnownReport'] = bpthread.return_value()1094 report['KnownReport'] = bpthread.return_value()
10231095
1024 # check crash database if problem is known1096 # check crash database if problem is known
1025 if self.report['ProblemType'] != 'Bug':1097 if report['ProblemType'] != 'Bug':
1026 known_thread = apport.REThread.REThread(target=self.crashdb.known,1098 known_thread = apport.REThread.REThread(target=self.crashdb.known,
1027 args=(self.report,))1099 args=(report,))
1028 known_thread.start()1100 known_thread.start()
1029 while known_thread.isAlive():1101 while known_thread.isAlive():
1030 self.ui_pulse_info_collection_progress()1102 self.ui_pulse_info_collection_progress()
@@ -1036,13 +1108,13 @@
1036 val = known_thread.return_value()1108 val = known_thread.return_value()
1037 if val is not None:1109 if val is not None:
1038 if val is True:1110 if val is True:
1039 self.report['KnownReport'] = '1'1111 report['KnownReport'] = '1'
1040 else:1112 else:
1041 self.report['KnownReport'] = val1113 report['KnownReport'] = val
10421114
1043 # anonymize; needs to happen after duplicate checking, otherwise we1115 # anonymize; needs to happen after duplicate checking, otherwise we
1044 # might damage the stack trace1116 # might damage the stack trace
1045 anonymize_thread = apport.REThread.REThread(target=self.report.anonymize)1117 anonymize_thread = apport.REThread.REThread(target=report.anonymize)
1046 anonymize_thread.start()1118 anonymize_thread.start()
1047 while anonymize_thread.isAlive():1119 while anonymize_thread.isAlive():
1048 self.ui_pulse_info_collection_progress()1120 self.ui_pulse_info_collection_progress()
@@ -1055,10 +1127,10 @@
1055 self.ui_stop_info_collection_progress()1127 self.ui_stop_info_collection_progress()
10561128
1057 # check that we were able to determine package names1129 # check that we were able to determine package names
1058 if 'UnreportableReason' not in self.report:1130 if 'UnreportableReason' not in report:
1059 if ('SourcePackage' not in self.report or1131 if ('SourcePackage' not in report or
1060 (not self.report['ProblemType'].startswith('Kernel')1132 (not report['ProblemType'].startswith('Kernel')
1061 and 'Package' not in self.report)):1133 and 'Package' not in report)):
1062 self.ui_error_message(_('Invalid problem report'),1134 self.ui_error_message(_('Invalid problem report'),
1063 _('Could not determine the package or source package name.'))1135 _('Could not determine the package or source package name.'))
1064 # TODO This is not called consistently, is it really needed?1136 # TODO This is not called consistently, is it really needed?
@@ -1112,26 +1184,26 @@
1112 os.write(w, str(e))1184 os.write(w, str(e))
1113 sys.exit(1)1185 sys.exit(1)
11141186
1115 def file_report(self):1187 def file_report(self, report):
1116 '''Upload the current report and guide the user to the reporting web page.'''1188 '''Upload the current report and guide the user to the reporting web page.'''
1117 # FIXME: This behaviour is not really correct, but necessary as1189 # FIXME: This behaviour is not really correct, but necessary as
1118 # long as we only support a single crashdb and have whoopsie1190 # long as we only support a single crashdb and have whoopsie
1119 # hardcoded. Once we have multiple crash dbs, we need to check1191 # hardcoded. Once we have multiple crash dbs, we need to check
1120 # accepts() earlier, and not even present the data if none of1192 # accepts() earlier, and not even present the data if none of
1121 # the DBs wants the report. See LP#957177 for details.1193 # the DBs wants the report. See LP#957177 for details.
1122 if not self.crashdb.accepts(self.report):1194 if not self.crashdb.accepts(report):
1123 return1195 return
1124 # drop PackageArchitecture if equal to Architecture1196 # drop PackageArchitecture if equal to Architecture
1125 if self.report.get('PackageArchitecture') == self.report.get('Architecture'):1197 if report.get('PackageArchitecture') == report.get('Architecture'):
1126 try:1198 try:
1127 del self.report['PackageArchitecture']1199 del report['PackageArchitecture']
1128 except KeyError:1200 except KeyError:
1129 pass1201 pass
11301202
1131 # StacktraceAddressSignature is redundant and does not need to clutter1203 # StacktraceAddressSignature is redundant and does not need to clutter
1132 # the database1204 # the database
1133 try:1205 try:
1134 del self.report['StacktraceAddressSignature']1206 del report['StacktraceAddressSignature']
1135 except KeyError:1207 except KeyError:
1136 pass1208 pass
11371209
@@ -1144,7 +1216,7 @@
11441216
1145 self.ui_start_upload_progress()1217 self.ui_start_upload_progress()
1146 upthread = apport.REThread.REThread(target=self.crashdb.upload,1218 upthread = apport.REThread.REThread(target=self.crashdb.upload,
1147 args=(self.report, progress_callback))1219 args=(report, progress_callback))
1148 upthread.start()1220 upthread.start()
1149 while upthread.isAlive():1221 while upthread.isAlive():
1150 self.ui_set_upload_progress(__upload_progress)1222 self.ui_set_upload_progress(__upload_progress)
@@ -1161,7 +1233,7 @@
1161 user, password = data1233 user, password = data
1162 self.crashdb.set_credentials(user, password)1234 self.crashdb.set_credentials(user, password)
1163 upthread = apport.REThread.REThread(target=self.crashdb.upload,1235 upthread = apport.REThread.REThread(target=self.crashdb.upload,
1164 args=(self.report, progress_callback))1236 args=(report, progress_callback))
1165 upthread.start()1237 upthread.start()
1166 except (TypeError, SyntaxError, ValueError):1238 except (TypeError, SyntaxError, ValueError):
1167 raise1239 raise
@@ -1176,7 +1248,7 @@
1176 ticket = upthread.return_value()1248 ticket = upthread.return_value()
1177 self.ui_stop_upload_progress()1249 self.ui_stop_upload_progress()
11781250
1179 url = self.crashdb.get_comment_url(self.report, ticket)1251 url = self.crashdb.get_comment_url(report, ticket)
1180 if url:1252 if url:
1181 self.open_url(url)1253 self.open_url(url)
11821254
@@ -1187,27 +1259,26 @@
1187 be processed, otherwise self.report is initialized and True is1259 be processed, otherwise self.report is initialized and True is
1188 returned.1260 returned.
1189 '''1261 '''
1262 self.report = apport.Report()
1190 try:1263 try:
1191 self.report = apport.Report()
1192 with open(path, 'rb') as f:1264 with open(path, 'rb') as f:
1193 self.report.load(f, binary='compressed')1265 self.report.load(f, binary='compressed')
1194 if 'ProblemType' not in self.report:1266 if 'ProblemType' not in self.report:
1195 raise ValueError('Report does not contain "ProblemType" field')1267 raise ValueError('Report does not contain "ProblemType" field')
1196 except MemoryError:1268 except MemoryError as e:
1197 self.report = None1269 reason = _('Your system does not have enough memory to process this crash report.')
1198 self.ui_error_message(_('Memory exhaustion'),1270 machine_reason = excstr(e)
1199 _('Your system does not have enough memory to process this crash report.'))1271 mark_invalid(self.report, path, reason, machine_reason)
1200 return False1272 return False
1201 except IOError as e:1273 except IOError as e:
1202 self.report = None1274 reason = _('Invalid problem report')
1203 self.ui_error_message(_('Invalid problem report'), e.strerror)1275 machine_reason = excstr(e)
1276 mark_invalid(self.report, path, reason, machine_reason)
1204 return False1277 return False
1205 except (TypeError, ValueError, AssertionError, zlib.error) as e:1278 except (TypeError, ValueError, AssertionError, zlib.error) as e:
1206 self.report = None1279 reason = _('This problem report is damaged and cannot be processed.')
1207 self.ui_error_message(_('Invalid problem report'),1280 machine_reason = excstr(e)
1208 '%s\n\n%s' % (1281 mark_invalid(self.report, path, reason, machine_reason)
1209 _('This problem report is damaged and cannot be processed.'),
1210 repr(e)))
1211 return False1282 return False
12121283
1213 if 'Package' in self.report:1284 if 'Package' in self.report:
@@ -1219,38 +1290,38 @@
1219 if self.report['ProblemType'] == 'Crash':1290 if self.report['ProblemType'] == 'Crash':
1220 exe_path = self.report.get('ExecutablePath', '')1291 exe_path = self.report.get('ExecutablePath', '')
1221 if not os.path.exists(exe_path):1292 if not os.path.exists(exe_path):
1222 msg = _('This problem report applies to a program which is not installed any more.')1293 reason = _('This problem report applies to a program which is not installed any more.')
1223 if exe_path:1294 machine_reason = "ENOENT: '%s'" % exe_path
1224 msg = '%s (%s)' % (msg, self.report['ExecutablePath'])1295 mark_invalid(self.report, path, reason, machine_reason)
1225 self.report = None
1226 self.ui_info_message(_('Invalid problem report'), msg)
1227 return False1296 return False
12281297
1229 if 'InterpreterPath' in self.report:1298 if 'InterpreterPath' in self.report:
1230 if not os.path.exists(self.report['InterpreterPath']):1299 if not os.path.exists(self.report['InterpreterPath']):
1231 msg = _('This problem report applies to a program which is not installed any more.')1300 reason = _('This problem report applies to a program which is not installed any more.')
1232 self.ui_info_message(_('Invalid problem report'), '%s (%s)'1301 machine_reason = "ENOENT: '%s'" % self.report['InterpreterPath']
1233 % (msg, self.report['InterpreterPath']))1302 mark_invalid(self.report, path, reason, machine_reason)
1234 return False1303 return False
12351304
1236 return True1305 return True
12371306
1238 def check_unreportable(self):1307 def check_unreportable(self, report):
1239 '''Check if the current report is unreportable.1308 '''Check if the current report is unreportable.
12401309
1241 If so, display an info message and return True.1310 If so, display an info message and return True.
1242 '''1311 '''
1243 if not self.crashdb.accepts(self.report):1312 if not self.crashdb.accepts(report):
1244 return False1313 return False
1245 if 'UnreportableReason' in self.report:1314 if 'InvalidReason' in report:
1246 if type(self.report['UnreportableReason']) == bytes:1315 return True
1247 self.report['UnreportableReason'] = self.report['UnreportableReason'].decode('UTF-8')1316 if 'UnreportableReason' in report:
1248 if 'Package' in self.report:1317 if type(report['UnreportableReason']) == bytes:
1249 title = _('Problem in %s') % self.report['Package'].split()[0]1318 report['UnreportableReason'] = report['UnreportableReason'].decode('UTF-8')
1319 if 'Package' in report:
1320 title = _('Problem in %s') % report['Package'].split()[0]
1250 else:1321 else:
1251 title = ''1322 title = ''
1252 self.ui_info_message(title, _('The problem cannot be reported:\n\n%s') %1323 self.ui_info_message(title, _('The problem cannot be reported:\n\n%s') %
1253 self.report['UnreportableReason'])1324 report['UnreportableReason'])
1254 return True1325 return True
1255 return False1326 return False
12561327
@@ -1291,26 +1362,26 @@
1291 return None1362 return None
1292 return result1363 return result
12931364
1294 def handle_duplicate(self):1365 def handle_duplicate(self, report):
1295 '''Check if current report matches a bug pattern.1366 '''Check if current report matches a bug pattern.
12961367
1297 If so, tell the user about it, open the existing bug in a browser, and1368 If so, tell the user about it, open the existing bug in a browser, and
1298 return True.1369 return True.
1299 '''1370 '''
1300 if not self.crashdb.accepts(self.report):1371 if not self.crashdb.accepts(report):
1301 return False1372 return False
1302 if 'KnownReport' not in self.report:1373 if 'KnownReport' not in report:
1303 return False1374 return False
13041375
1305 # if we have an URL, open it; otherwise this is just a marker that we1376 # if we have an URL, open it; otherwise this is just a marker that we
1306 # know about it1377 # know about it
1307 if self.report['KnownReport'].startswith('http'):1378 if report['KnownReport'].startswith('http'):
1308 self.ui_info_message(_('Problem already known'),1379 self.ui_info_message(_('Problem already known'),
1309 _('This problem was already reported in the bug report displayed \1380 _('This problem was already reported in the bug report displayed \
1310in the web browser. Please check if you can add any further information that \1381in the web browser. Please check if you can add any further information that \
1311might be helpful for the developers.'))1382might be helpful for the developers.'))
13121383
1313 self.open_url(self.report['KnownReport'])1384 self.open_url(report['KnownReport'])
1314 else:1385 else:
1315 self.ui_info_message(_('Problem already known'),1386 self.ui_info_message(_('Problem already known'),
1316 _('This problem was already reported to developers. Thank you!'))1387 _('This problem was already reported to developers. Thank you!'))
13171388
=== modified file 'gtk/apport-gtk'
--- gtk/apport-gtk 2012-11-23 14:23:46 +0000
+++ gtk/apport-gtk 2013-04-27 08:52:25 +0000
@@ -11,7 +11,7 @@
11# option) any later version. See http://www.gnu.org/copyleft/gpl.html for11# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
12# the full text of the license.12# the full text of the license.
1313
14import os.path, sys, subprocess, os, re, errno14import os.path, sys, subprocess, os, re, errno, itertools
1515
16from gi.repository import GObject, GLib, Wnck, GdkX11, Gdk16from gi.repository import GObject, GLib, Wnck, GdkX11, Gdk
17try:17try:
@@ -63,11 +63,13 @@
63 column = Gtk.TreeViewColumn('Report', Gtk.CellRendererText(), text=0)63 column = Gtk.TreeViewColumn('Report', Gtk.CellRendererText(), text=0)
64 self.w('details_treeview').append_column(column)64 self.w('details_treeview').append_column(column)
65 self.spinner = self.add_spinner_over_treeview(self.w('details_treeview'))65 self.spinner = self.add_spinner_over_treeview(self.w('details_treeview'))
66 self.w('details_treeview').set_row_separator_func(self.on_row_separator, None)
6667
67 self.md = None68 self.md = None
6869
69 self.desktop_info = None70 self.desktop_info = None
7071
72
71 #73 #
72 # ui_* implementation of abstract UserInterface classes74 # ui_* implementation of abstract UserInterface classes
73 #75 #
@@ -100,42 +102,56 @@
100 if not self.w('details_treeview').get_property('visible'):102 if not self.w('details_treeview').get_property('visible'):
101 return103 return
102104
103 if shown_keys:
104 keys = set(self.report.keys()) & set(shown_keys)
105 else:
106 keys = self.report.keys()
107 # show the most interesting items on top
108 keys = sorted(keys)
109 for k in ('Traceback', 'StackTrace', 'Title', 'ProblemType', 'Package', 'ExecutablePath'):
110 if k in keys:
111 keys.remove(k)
112 keys.insert(0, k)
113
114 self.tree_model.clear()105 self.tree_model.clear()
115 for key in keys:106
116 keyiter = self.tree_model.insert_before(None, None)107 for report in itertools.chain([self.report], self.grouped_reports.values()):
117 self.tree_model.set_value(keyiter, 0, key)108 if shown_keys:
118109 keys = set(report.keys()) & set(shown_keys)
119 valiter = self.tree_model.insert_before(keyiter, None)110 else:
120 if not hasattr(self.report[key], 'gzipvalue') and \111 keys = report.keys()
121 hasattr(self.report[key], 'isspace') and \112 # show the most interesting items on top
122 not self.report._is_binary(self.report[key]):113 keys = sorted(keys)
123 v = self.report[key]114 for k in ('Traceback', 'StackTrace', 'Title', 'ProblemType', 'Package', 'ExecutablePath'):
124 if len(v) > 4000:115 if k in keys:
125 v = v[:4000]116 keys.remove(k)
117 keys.insert(0, k)
118
119 if report != self.report:
120 # insert separator
121 keyiter = self.tree_model.insert_before(None, None)
122 self.tree_model.set_value(keyiter, 0, '')
123
124 if 'InvalidReason' in keys:
125 # This report is invalid, but we need to tell the user that
126 # we're going to send it for statistical purposes.
127 keyiter = self.tree_model.insert_before(None, None)
128 self.tree_model.set_value(keyiter, 0, report['InvalidReason'])
129 continue
130
131 for key in keys:
132 keyiter = self.tree_model.insert_before(None, None)
133 self.tree_model.set_value(keyiter, 0, key)
134
135 valiter = self.tree_model.insert_before(keyiter, None)
136 if not hasattr(report[key], 'gzipvalue') and \
137 hasattr(report[key], 'isspace') and \
138 not report._is_binary(report[key]):
139 v = report[key]
140 if len(v) > 4000:
141 v = v[:4000]
142 if type(v) == bytes:
143 v += b'\n[...]'
144 else:
145 v += '\n[...]'
126 if type(v) == bytes:146 if type(v) == bytes:
127 v += b'\n[...]'147 v = v.decode('UTF-8', errors='replace')
128 else:148 self.tree_model.set_value(valiter, 0, v)
129 v += '\n[...]'149 # expand the row if the value has less than 5 lines
130 if type(v) == bytes:150 if len(list(filter(lambda c: c == '\n', report[key]))) < 4:
131 v = v.decode('UTF-8', errors='replace')151 self.w('details_treeview').expand_row(
132 self.tree_model.set_value(valiter, 0, v)152 self.tree_model.get_path(keyiter), False)
133 # expand the row if the value has less than 5 lines153 else:
134 if len(list(filter(lambda c: c == '\n', self.report[key]))) < 4:154 self.tree_model.set_value(valiter, 0, _('(binary data)'))
135 self.w('details_treeview').expand_row(
136 self.tree_model.get_path(keyiter), False)
137 else:
138 self.tree_model.set_value(valiter, 0, _('(binary data)'))
139155
140 def get_system_application_title(self):156 def get_system_application_title(self):
141 '''Get dialog title for a non-.desktop application.157 '''Get dialog title for a non-.desktop application.
@@ -207,6 +223,14 @@
207 self.w('send_error_report').set_active(False)223 self.w('send_error_report').set_active(False)
208 self.w('send_error_report').hide()224 self.w('send_error_report').hide()
209225
226 if self.grouped_reports and allowed_to_report:
227 self.w('previous_internal_errors').set_active(True)
228 self.w('previous_internal_errors').show()
229 self.w('ignore_future_problems').hide()
230 else:
231 self.w('previous_internal_errors').set_active(False)
232 self.w('previous_internal_errors').hide()
233
210 self.w('examine').set_visible(self.can_examine_locally())234 self.w('examine').set_visible(self.can_examine_locally())
211235
212 self.w('continue_button').set_label(_('Continue'))236 self.w('continue_button').set_label(_('Continue'))
@@ -346,8 +370,18 @@
346 if len(sys.argv) == 1:370 if len(sys.argv) == 1:
347 d.set_focus_on_map(False)371 d.set_focus_on_map(False)
348372
349 return_value = { 'report' : False, 'blacklist' : False,373 return_value = {
350 'restart' : False, 'examine' : False }374 # Should the problem be reported?
375 'report' : False,
376 # Should future instances of this type be ignored?
377 'blacklist' : False,
378 # Should the application be restarted?
379 'restart' : False,
380 # Has an interactive debugging session been requested?
381 'examine' : False,
382 # Should grouped reports also be reported?
383 'grouped': False
384 }
351 def dialog_crash_dismissed(widget):385 def dialog_crash_dismissed(widget):
352 self.w('dialog_crash_new').hide()386 self.w('dialog_crash_new').hide()
353 if widget is self.w('dialog_crash_new'):387 if widget is self.w('dialog_crash_new'):
@@ -364,6 +398,8 @@
364 return_value['blacklist'] = True398 return_value['blacklist'] = True
365 if widget == self.w('continue_button') and self.desktop_info:399 if widget == self.w('continue_button') and self.desktop_info:
366 return_value['restart'] = True400 return_value['restart'] = True
401 if self.w('previous_internal_errors').get_active():
402 return_value['grouped'] = True
367 Gtk.main_quit()403 Gtk.main_quit()
368404
369 self.w('dialog_crash_new').connect('destroy', dialog_crash_dismissed)405 self.w('dialog_crash_new').connect('destroy', dialog_crash_dismissed)
@@ -561,6 +597,11 @@
561 # Event handlers597 # Event handlers
562 #598 #
563599
600 def on_row_separator(self, model, iterator, data):
601 if model[iterator][0] == '':
602 return True
603 return False
604
564 def on_show_details_clicked(self, widget):605 def on_show_details_clicked(self, widget):
565 sw = self.w('details_scrolledwindow')606 sw = self.w('details_scrolledwindow')
566 if sw.get_property('visible'):607 if sw.get_property('visible'):
@@ -574,7 +615,7 @@
574 if not self.collect_called:615 if not self.collect_called:
575 self.collect_called = True616 self.collect_called = True
576 self.ui_update_view(['ExecutablePath'])617 self.ui_update_view(['ExecutablePath'])
577 GLib.idle_add(lambda: self.collect_info(on_finished=self.ui_update_view))618 GLib.idle_add(lambda: self.collect_all_info(on_finished=self.ui_update_view))
578 return True619 return True
579620
580 def on_progress_window_close_event(self, widget, event=None):621 def on_progress_window_close_event(self, widget, event=None):
581622
=== modified file 'gtk/apport-gtk.ui'
--- gtk/apport-gtk.ui 2012-03-02 12:03:31 +0000
+++ gtk/apport-gtk.ui 2013-04-27 08:52:25 +0000
@@ -27,11 +27,9 @@
27 <child>27 <child>
28 <object class="GtkButton" id="button7">28 <object class="GtkButton" id="button7">
29 <property name="label">gtk-cancel</property>29 <property name="label">gtk-cancel</property>
30 <property name="use_action_appearance">False</property>
31 <property name="visible">True</property>30 <property name="visible">True</property>
32 <property name="can_focus">True</property>31 <property name="can_focus">True</property>
33 <property name="receives_default">True</property>32 <property name="receives_default">True</property>
34 <property name="use_action_appearance">False</property>
35 <property name="use_stock">True</property>33 <property name="use_stock">True</property>
36 </object>34 </object>
37 <packing>35 <packing>
@@ -43,11 +41,9 @@
43 <child>41 <child>
44 <object class="GtkButton" id="button13">42 <object class="GtkButton" id="button13">
45 <property name="label">gtk-ok</property>43 <property name="label">gtk-ok</property>
46 <property name="use_action_appearance">False</property>
47 <property name="visible">True</property>44 <property name="visible">True</property>
48 <property name="can_focus">True</property>45 <property name="can_focus">True</property>
49 <property name="receives_default">True</property>46 <property name="receives_default">True</property>
50 <property name="use_action_appearance">False</property>
51 <property name="use_stock">True</property>47 <property name="use_stock">True</property>
52 </object>48 </object>
53 <packing>49 <packing>
@@ -213,11 +209,9 @@
213 <child>209 <child>
214 <object class="GtkCheckButton" id="send_error_report">210 <object class="GtkCheckButton" id="send_error_report">
215 <property name="label" translatable="yes">Send an error report to help fix this problem</property>211 <property name="label" translatable="yes">Send an error report to help fix this problem</property>
216 <property name="use_action_appearance">False</property>
217 <property name="visible">True</property>212 <property name="visible">True</property>
218 <property name="can_focus">True</property>213 <property name="can_focus">True</property>
219 <property name="receives_default">False</property>214 <property name="receives_default">False</property>
220 <property name="use_action_appearance">False</property>
221 <property name="xalign">0</property>215 <property name="xalign">0</property>
222 <property name="active">True</property>216 <property name="active">True</property>
223 <property name="draw_indicator">True</property>217 <property name="draw_indicator">True</property>
@@ -231,11 +225,9 @@
231 <child>225 <child>
232 <object class="GtkCheckButton" id="ignore_future_problems">226 <object class="GtkCheckButton" id="ignore_future_problems">
233 <property name="label" translatable="yes">Ignore future problems of this program version</property>227 <property name="label" translatable="yes">Ignore future problems of this program version</property>
234 <property name="use_action_appearance">False</property>
235 <property name="visible">True</property>228 <property name="visible">True</property>
236 <property name="can_focus">True</property>229 <property name="can_focus">True</property>
237 <property name="receives_default">False</property>230 <property name="receives_default">False</property>
238 <property name="use_action_appearance">False</property>
239 <property name="xalign">0</property>231 <property name="xalign">0</property>
240 <property name="draw_indicator">True</property>232 <property name="draw_indicator">True</property>
241 </object>233 </object>
@@ -245,6 +237,22 @@
245 <property name="position">1</property>237 <property name="position">1</property>
246 </packing>238 </packing>
247 </child>239 </child>
240 <child>
241 <object class="GtkCheckButton" id="previous_internal_errors">
242 <property name="label" translatable="yes">Report previous internal errors too</property>
243 <property name="visible">True</property>
244 <property name="can_focus">True</property>
245 <property name="receives_default">False</property>
246 <property name="xalign">0.0099999997764825821</property>
247 <property name="yalign">0.51999998092651367</property>
248 <property name="draw_indicator">True</property>
249 </object>
250 <packing>
251 <property name="expand">False</property>
252 <property name="fill">True</property>
253 <property name="position">2</property>
254 </packing>
255 </child>
248 </object>256 </object>
249 <packing>257 <packing>
250 <property name="expand">False</property>258 <property name="expand">False</property>
@@ -285,11 +293,9 @@
285 <child>293 <child>
286 <object class="GtkButton" id="show_details">294 <object class="GtkButton" id="show_details">
287 <property name="label" translatable="yes">Show Details</property>295 <property name="label" translatable="yes">Show Details</property>
288 <property name="use_action_appearance">False</property>
289 <property name="visible">True</property>296 <property name="visible">True</property>
290 <property name="can_focus">True</property>297 <property name="can_focus">True</property>
291 <property name="receives_default">True</property>298 <property name="receives_default">True</property>
292 <property name="use_action_appearance">False</property>
293 <signal name="clicked" handler="on_show_details_clicked" swapped="no"/>299 <signal name="clicked" handler="on_show_details_clicked" swapped="no"/>
294 </object>300 </object>
295 <packing>301 <packing>
@@ -301,11 +307,9 @@
301 <child>307 <child>
302 <object class="GtkButton" id="examine">308 <object class="GtkButton" id="examine">
303 <property name="label" translatable="yes">_Examine locally</property>309 <property name="label" translatable="yes">_Examine locally</property>
304 <property name="use_action_appearance">False</property>
305 <property name="visible">True</property>310 <property name="visible">True</property>
306 <property name="can_focus">True</property>311 <property name="can_focus">True</property>
307 <property name="receives_default">True</property>312 <property name="receives_default">True</property>
308 <property name="use_action_appearance">False</property>
309 <property name="use_underline">True</property>313 <property name="use_underline">True</property>
310 </object>314 </object>
311 <packing>315 <packing>
@@ -317,10 +321,8 @@
317 <child>321 <child>
318 <object class="GtkButton" id="cancel_button">322 <object class="GtkButton" id="cancel_button">
319 <property name="label">gtk-cancel</property>323 <property name="label">gtk-cancel</property>
320 <property name="use_action_appearance">False</property>
321 <property name="can_focus">True</property>324 <property name="can_focus">True</property>
322 <property name="receives_default">True</property>325 <property name="receives_default">True</property>
323 <property name="use_action_appearance">False</property>
324 <property name="use_stock">True</property>326 <property name="use_stock">True</property>
325 </object>327 </object>
326 <packing>328 <packing>
@@ -346,11 +348,9 @@
346 <child>348 <child>
347 <object class="GtkButton" id="closed_button">349 <object class="GtkButton" id="closed_button">
348 <property name="label" translatable="yes">Leave Closed</property>350 <property name="label" translatable="yes">Leave Closed</property>
349 <property name="use_action_appearance">False</property>
350 <property name="visible">True</property>351 <property name="visible">True</property>
351 <property name="can_focus">True</property>352 <property name="can_focus">True</property>
352 <property name="receives_default">True</property>353 <property name="receives_default">True</property>
353 <property name="use_action_appearance">False</property>
354 </object>354 </object>
355 <packing>355 <packing>
356 <property name="expand">False</property>356 <property name="expand">False</property>
@@ -361,11 +361,9 @@
361 <child>361 <child>
362 <object class="GtkButton" id="continue_button">362 <object class="GtkButton" id="continue_button">
363 <property name="label" translatable="yes">Continue</property>363 <property name="label" translatable="yes">Continue</property>
364 <property name="use_action_appearance">False</property>
365 <property name="visible">True</property>364 <property name="visible">True</property>
366 <property name="can_focus">True</property>365 <property name="can_focus">True</property>
367 <property name="receives_default">True</property>366 <property name="receives_default">True</property>
368 <property name="use_action_appearance">False</property>
369 </object>367 </object>
370 <packing>368 <packing>
371 <property name="expand">False</property>369 <property name="expand">False</property>
@@ -466,12 +464,10 @@
466 <child>464 <child>
467 <object class="GtkButton" id="button_cancel_collecting">465 <object class="GtkButton" id="button_cancel_collecting">
468 <property name="label">gtk-cancel</property>466 <property name="label">gtk-cancel</property>
469 <property name="use_action_appearance">False</property>
470 <property name="visible">True</property>467 <property name="visible">True</property>
471 <property name="can_focus">True</property>468 <property name="can_focus">True</property>
472 <property name="can_default">True</property>469 <property name="can_default">True</property>
473 <property name="receives_default">False</property>470 <property name="receives_default">False</property>
474 <property name="use_action_appearance">False</property>
475 <property name="use_stock">True</property>471 <property name="use_stock">True</property>
476 <signal name="clicked" handler="on_progress_window_close_event" swapped="no"/>472 <signal name="clicked" handler="on_progress_window_close_event" swapped="no"/>
477 </object>473 </object>
@@ -569,12 +565,10 @@
569 <child>565 <child>
570 <object class="GtkButton" id="button_cancel_upload">566 <object class="GtkButton" id="button_cancel_upload">
571 <property name="label">gtk-cancel</property>567 <property name="label">gtk-cancel</property>
572 <property name="use_action_appearance">False</property>
573 <property name="visible">True</property>568 <property name="visible">True</property>
574 <property name="can_focus">True</property>569 <property name="can_focus">True</property>
575 <property name="can_default">True</property>570 <property name="can_default">True</property>
576 <property name="receives_default">False</property>571 <property name="receives_default">False</property>
577 <property name="use_action_appearance">False</property>
578 <property name="use_stock">True</property>572 <property name="use_stock">True</property>
579 <signal name="clicked" handler="on_progress_window_close_event" swapped="no"/>573 <signal name="clicked" handler="on_progress_window_close_event" swapped="no"/>
580 </object>574 </object>
581575
=== modified file 'kde/apport-kde'
--- kde/apport-kde 2012-11-23 14:23:46 +0000
+++ kde/apport-kde 2013-04-27 08:52:25 +0000
@@ -278,7 +278,7 @@
278 self.treeview.setVisible(visible)278 self.treeview.setVisible(visible)
279 if visible and not self.collect_called:279 if visible and not self.collect_called:
280 self.ui.ui_update_view(self, ['ExecutablePath'])280 self.ui.ui_update_view(self, ['ExecutablePath'])
281 QTimer.singleShot(0, lambda: self.ui.collect_info(on_finished=self.collect_done))281 QTimer.singleShot(0, lambda: self.ui.collect_info(self.ui.report, on_finished=self.collect_done))
282 self.collect_called = True282 self.collect_called = True
283 if visible:283 if visible:
284 self.setMaximumSize(16777215, 16777215)284 self.setMaximumSize(16777215, 16777215)
285285
=== 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-04-27 08:52:25 +0000
@@ -762,7 +762,7 @@
762 assert readelf.returncode == 0762 assert readelf.returncode == 0
763 for line in out.splitlines():763 for line in out.splitlines():
764 if line.startswith(' Machine:'):764 if line.startswith(' Machine:'):
765 machine = line.split(maxsplit=1)[1]765 machine = line.split(' ', 1)[1]
766 break766 break
767 else:767 else:
768 self.fail('could not fine Machine: in readelf output')768 self.fail('could not fine Machine: in readelf output')
769769
=== modified file 'test/test_ui.py'
--- test/test_ui.py 2013-04-03 09:05:41 +0000
+++ test/test_ui.py 2013-04-27 08:52:25 +0000
@@ -10,11 +10,11 @@
10from io import BytesIO10from io import BytesIO
1111
12import apport.ui12import apport.ui
13from apport.ui import _
14import apport.report13import apport.report
15import problem_report14import problem_report
16import apport.crashdb_impl.memory15import apport.crashdb_impl.memory
17import stat16import stat
17from mock import patch
1818
1919
20class TestSuiteUserInterface(apport.ui.UserInterface):20class TestSuiteUserInterface(apport.ui.UserInterface):
@@ -53,7 +53,7 @@
53 # these store the choices the ui_present_* calls do53 # these store the choices the ui_present_* calls do
54 self.present_package_error_response = None54 self.present_package_error_response = None
55 self.present_kernel_error_response = None55 self.present_kernel_error_response = None
56 self.present_details_response = None56 self.present_details_response = {}
57 self.question_yesno_response = None57 self.question_yesno_response = None
58 self.question_choice_response = None58 self.question_choice_response = None
59 self.question_file_response = None59 self.question_file_response = None
@@ -264,11 +264,7 @@
264 self.update_report_file()264 self.update_report_file()
265 self.ui.load_report(self.report_file.name)265 self.ui.load_report(self.report_file.name)
266266
267 self.assertTrue(self.ui.report is None)267 self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason'])
268 self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
269 self.assertEqual(self.ui.msg_severity, 'info')
270
271 self.ui.clear_msg()
272268
273 # invalid base64 encoding269 # invalid base64 encoding
274 self.report_file.seek(0)270 self.report_file.seek(0)
@@ -281,9 +277,7 @@
281 self.report_file.flush()277 self.report_file.flush()
282278
283 self.ui.load_report(self.report_file.name)279 self.ui.load_report(self.report_file.name)
284 self.assertTrue(self.ui.report is None)280 self.assertTrue('damaged' in self.ui.report['InvalidReason'])
285 self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
286 self.assertEqual(self.ui.msg_severity, 'error')
287281
288 def test_restart(self):282 def test_restart(self):
289 '''restart()'''283 '''restart()'''
@@ -323,7 +317,7 @@
323317
324 # report without any information (distro bug)318 # report without any information (distro bug)
325 self.ui.report = apport.Report()319 self.ui.report = apport.Report()
326 self.ui.collect_info()320 self.ui.collect_info(self.ui.report)
327 self.assertTrue(set(['Date', 'Uname', 'DistroRelease', 'ProblemType']).issubset(321 self.assertTrue(set(['Date', 'Uname', 'DistroRelease', 'ProblemType']).issubset(
328 set(self.ui.report.keys())))322 set(self.ui.report.keys())))
329 self.assertEqual(self.ui.ic_progress_pulses, 0,323 self.assertEqual(self.ui.ic_progress_pulses, 0,
@@ -341,7 +335,7 @@
341 # apport hooks)335 # apport hooks)
342 self.ui.report['Fstab'] = ('/etc/fstab', True)336 self.ui.report['Fstab'] = ('/etc/fstab', True)
343 self.ui.report['CompressedValue'] = problem_report.CompressedValue(b'Test')337 self.ui.report['CompressedValue'] = problem_report.CompressedValue(b'Test')
344 self.ui.collect_info()338 self.ui.collect_info(self.ui.report)
345 self.assertTrue(set(['SourcePackage', 'Package', 'ProblemType',339 self.assertTrue(set(['SourcePackage', 'Package', 'ProblemType',
346 'Uname', 'Dependencies', 'DistroRelease', 'Date',340 'Uname', 'Dependencies', 'DistroRelease', 'Date',
347 'ExecutablePath']).issubset(set(self.ui.report.keys())))341 'ExecutablePath']).issubset(set(self.ui.report.keys())))
@@ -356,7 +350,7 @@
356 # report with only package information350 # report with only package information
357 self.ui.report = apport.Report('Bug')351 self.ui.report = apport.Report('Bug')
358 self.ui.cur_package = 'bash'352 self.ui.cur_package = 'bash'
359 self.ui.collect_info()353 self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
360 self.assertTrue(set(['SourcePackage', 'Package', 'ProblemType',354 self.assertTrue(set(['SourcePackage', 'Package', 'ProblemType',
361 'Uname', 'Dependencies', 'DistroRelease',355 'Uname', 'Dependencies', 'DistroRelease',
362 'Date']).issubset(set(self.ui.report.keys())))356 'Date']).issubset(set(self.ui.report.keys())))
@@ -371,7 +365,7 @@
371 self.ui.report = apport.Report('Bug')365 self.ui.report = apport.Report('Bug')
372 self.ui.cur_package = 'bash'366 self.ui.cur_package = 'bash'
373 self.ui.report_file = self.report_file.name367 self.ui.report_file = self.report_file.name
374 self.ui.collect_info()368 self.ui.collect_info(self.ui.report, self.ui.report_file, cur_package=self.ui.cur_package)
375 self.assertTrue(os.stat(self.report_file.name).st_mode & stat.S_IRGRP)369 self.assertTrue(os.stat(self.report_file.name).st_mode & stat.S_IRGRP)
376370
377 def test_collect_info_crashdb_spec(self):371 def test_collect_info_crashdb_spec(self):
@@ -386,7 +380,7 @@
386380
387 self.ui.report = apport.Report('Bug')381 self.ui.report = apport.Report('Bug')
388 self.ui.cur_package = 'bash'382 self.ui.cur_package = 'bash'
389 self.ui.collect_info()383 self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
390 self.assertTrue('CrashDB' in self.ui.report)384 self.assertTrue('CrashDB' in self.ui.report)
391 self.assertFalse('UnreportableReason' in self.ui.report,385 self.assertFalse('UnreportableReason' in self.ui.report,
392 self.ui.report.get('UnreportableReason'))386 self.ui.report.get('UnreportableReason'))
@@ -405,7 +399,7 @@
405399
406 self.ui.report = apport.Report('Bug')400 self.ui.report = apport.Report('Bug')
407 self.ui.cur_package = 'bash'401 self.ui.cur_package = 'bash'
408 self.ui.collect_info()402 self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
409 self.assertFalse('UnreportableReason' in self.ui.report,403 self.assertFalse('UnreportableReason' in self.ui.report,
410 self.ui.report.get('UnreportableReason'))404 self.ui.report.get('UnreportableReason'))
411 self.assertEqual(self.ui.report['BashHook'], 'Moo')405 self.assertEqual(self.ui.report['BashHook'], 'Moo')
@@ -422,7 +416,7 @@
422416
423 self.ui.report = apport.Report('Bug')417 self.ui.report = apport.Report('Bug')
424 self.ui.cur_package = 'bash'418 self.ui.cur_package = 'bash'
425 self.ui.collect_info()419 self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
426 self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'],420 self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'],
427 self.ui.report.get('UnreportableReason', '<not set>'))421 self.ui.report.get('UnreportableReason', '<not set>'))
428422
@@ -434,7 +428,7 @@
434428
435 self.ui.report = apport.Report('Bug')429 self.ui.report = apport.Report('Bug')
436 self.ui.cur_package = 'bash'430 self.ui.cur_package = 'bash'
437 self.ui.collect_info()431 self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
438 self.assertTrue('package hook' in self.ui.report['UnreportableReason'],432 self.assertTrue('package hook' in self.ui.report['UnreportableReason'],
439 self.ui.report.get('UnreportableReason', '<not set>'))433 self.ui.report.get('UnreportableReason', '<not set>'))
440434
@@ -446,7 +440,7 @@
446440
447 self.ui.report = apport.Report('Bug')441 self.ui.report = apport.Report('Bug')
448 self.ui.cur_package = 'bash'442 self.ui.cur_package = 'bash'
449 self.ui.collect_info()443 self.ui.collect_info(self.ui.report, cur_package=self.ui.cur_package)
450 self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'],444 self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'],
451 self.ui.report.get('UnreportableReason', '<not set>'))445 self.ui.report.get('UnreportableReason', '<not set>'))
452446
@@ -454,7 +448,7 @@
454 '''handle_duplicate()'''448 '''handle_duplicate()'''
455449
456 self.ui.load_report(self.report_file.name)450 self.ui.load_report(self.report_file.name)
457 self.assertEqual(self.ui.handle_duplicate(), False)451 self.assertEqual(self.ui.handle_duplicate(self.ui.report), False)
458 self.assertEqual(self.ui.msg_title, None)452 self.assertEqual(self.ui.msg_title, None)
459 self.assertEqual(self.ui.opened_url, None)453 self.assertEqual(self.ui.opened_url, None)
460454
@@ -462,7 +456,7 @@
462 self.report['KnownReport'] = demo_url456 self.report['KnownReport'] = demo_url
463 self.update_report_file()457 self.update_report_file()
464 self.ui.load_report(self.report_file.name)458 self.ui.load_report(self.report_file.name)
465 self.assertEqual(self.ui.handle_duplicate(), True)459 self.assertEqual(self.ui.handle_duplicate(self.ui.report), True)
466 self.assertEqual(self.ui.msg_severity, 'info')460 self.assertEqual(self.ui.msg_severity, 'info')
467 self.assertEqual(self.ui.opened_url, demo_url)461 self.assertEqual(self.ui.opened_url, demo_url)
468462
@@ -471,7 +465,7 @@
471 self.report['KnownReport'] = '1'465 self.report['KnownReport'] = '1'
472 self.update_report_file()466 self.update_report_file()
473 self.ui.load_report(self.report_file.name)467 self.ui.load_report(self.report_file.name)
474 self.assertEqual(self.ui.handle_duplicate(), True)468 self.assertEqual(self.ui.handle_duplicate(self.ui.report), True)
475 self.assertEqual(self.ui.msg_severity, 'info')469 self.assertEqual(self.ui.msg_severity, 'info')
476 self.assertEqual(self.ui.opened_url, None)470 self.assertEqual(self.ui.opened_url, None)
477471
@@ -742,6 +736,76 @@
742736
743 return r737 return r
744738
739 @patch.object(apport.fileutils, 'get_new_reports')
740 def test_run_crashes(self, patched_fileutils):
741 '''run_crashes()'''
742 r = self._gen_test_crash()
743 self.ui = TestSuiteUserInterface()
744
745 files = [os.path.join(apport.fileutils.report_dir, '%s%i.crash' % t)
746 for t in zip(['test'] * 3, range(3))]
747 patched_fileutils.return_value = files
748
749 mtimes = []
750 for report_file in files:
751 with open(report_file, 'wb') as f:
752 r.write(f)
753 mtimes.append(os.stat(report_file).st_mtime)
754
755 # Only system internal reports. None should be processed at this time.
756 with patch.object(self.ui, 'run_crash') as patched_run_crash:
757 self.ui.run_crashes()
758 self.assertEqual(patched_run_crash.call_count, 0)
759
760 # They shouldn't be marked as seen.
761 new_mtimes = []
762 for report_file in files:
763 new_mtimes.append(os.stat(report_file).st_mtime)
764 self.assertEqual(mtimes, new_mtimes)
765
766 # Three system internal reports and one application report.
767 path = os.path.join(apport.fileutils.report_dir, 'test3.crash')
768 r['DesktopFile'] = glob.glob('/usr/share/applications/*.desktop')[0]
769 with open(path, 'wb') as f:
770 r.write(f)
771 files.append(path)
772 patched_fileutils.return_value = files
773
774 with patch.object(apport.fileutils, 'mark_report_upload') as p:
775 self.ui.present_details_response = {'report': True,
776 'blacklist': False,
777 'examine': False,
778 'restart': False}
779 self.ui.run_crashes()
780
781 # All reports should be uploaded.
782 self.assertEqual(p.call_count, 4)
783
784 # They should be marked as seen.
785 for i in range(len(files) - 1):
786 self.assertNotEqual(os.stat(files[i]).st_mtime, mtimes[i])
787
788 r = apport.Report()
789 with open(files[0], 'rb') as f:
790 r.load(f)
791 with open(files[0], 'wb') as f:
792 r['ExecutablePath'] = '/nonexistant'
793 r.write(f)
794
795 with patch.object(apport.fileutils, 'seen_report') as seen:
796 # Look at all the reports again.
797 seen.return_value = False
798 with patch.object(apport.fileutils, 'mark_report_upload') as p:
799 self.ui.present_details_response = {'report': True,
800 'blacklist': False,
801 'examine': False,
802 'restart': False}
803 self.ui.run_crashes()
804 with open(files[0], 'rb') as f:
805 r.load(f)
806 self.assertEqual(r['InvalidReason'],
807 'This problem report applies to a program which is not installed any more.')
808
745 def test_run_crash(self):809 def test_run_crash(self):
746 '''run_crash()'''810 '''run_crash()'''
747811
@@ -859,8 +923,7 @@
859 'examine': False,923 'examine': False,
860 'restart': False}924 'restart': False}
861 self.ui.run_crash(report_file)925 self.ui.run_crash(report_file)
862 self.assertEqual(self.ui.msg_severity, 'error', self.ui.msg_text)926 self.assertTrue('decompress' in self.ui.report['InvalidMachineReason'])
863 self.assertTrue('decompress' in self.ui.msg_text)
864 self.assertTrue(self.ui.present_details_shown)927 self.assertTrue(self.ui.present_details_shown)
865928
866 def test_run_crash_argv_file(self):929 def test_run_crash_argv_file(self):
@@ -967,10 +1030,12 @@
9671030
968 # run1031 # run
969 self.ui = TestSuiteUserInterface()1032 self.ui = TestSuiteUserInterface()
1033 self.ui.present_details_response = {'report': False,
1034 'blacklist': False,
1035 'examine': False,
1036 'restart': False}
970 self.ui.run_crash(report_file)1037 self.ui.run_crash(report_file)
971 self.assertEqual(self.ui.msg_severity, 'error')1038 self.assertEqual(self.ui.report['InvalidMachineReason'], 'No core dump')
972 self.assertTrue('memory' in self.ui.msg_text, '%s: %s' %
973 (self.ui.msg_title, self.ui.msg_text))
9741039
975 def test_run_crash_preretraced(self):1040 def test_run_crash_preretraced(self):
976 '''run_crash() pre-retraced reports.1041 '''run_crash() pre-retraced reports.
@@ -1008,7 +1073,7 @@
1008 copying a .crash file.1073 copying a .crash file.
1009 '''1074 '''
1010 self.ui.report = self._gen_test_crash()1075 self.ui.report = self._gen_test_crash()
1011 self.ui.collect_info()1076 self.ui.collect_info(self.ui.report)
10121077
1013 # now pretend to move it to a machine where the package is not1078 # now pretend to move it to a machine where the package is not
1014 # installed1079 # installed
@@ -1048,9 +1113,7 @@
1048 'examine': False,1113 'examine': False,
1049 'restart': False}1114 'restart': False}
1050 self.ui.run_crash(report_file)1115 self.ui.run_crash(report_file)
10511116 self.assertTrue('does not exist' in self.ui.report['InvalidMachineReason'])
1052 self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
1053 self.assertEqual(self.ui.msg_severity, 'error')
10541117
1055 def test_run_crash_uninstalled(self):1118 def test_run_crash_uninstalled(self):
1056 '''run_crash() on reports with subsequently uninstalled packages'''1119 '''run_crash() on reports with subsequently uninstalled packages'''
@@ -1069,8 +1132,10 @@
1069 'restart': False}1132 'restart': False}
1070 self.ui.run_crash(report_file)1133 self.ui.run_crash(report_file)
10711134
1072 self.assertEqual(self.ui.msg_title, _('Invalid problem report'))1135 self.assertTrue('not installed' in self.ui.report['InvalidReason'])
1073 self.assertEqual(self.ui.msg_severity, 'info')1136 self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason'])
1137
1138 self.ui.report = None
10741139
1075 # interpreted program got uninstalled between crash and report1140 # interpreted program got uninstalled between crash and report
1076 r = apport.Report()1141 r = apport.Report()
@@ -1080,8 +1145,10 @@
10801145
1081 self.ui.run_crash(report_file)1146 self.ui.run_crash(report_file)
10821147
1083 self.assertEqual(self.ui.msg_title, _('Invalid problem report'))1148 self.assertTrue('not installed' in self.ui.report['InvalidReason'])
1084 self.assertEqual(self.ui.msg_severity, 'info')1149 self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason'])
1150
1151 self.ui.report = None
10851152
1086 # interpreter got uninstalled between crash and report1153 # interpreter got uninstalled between crash and report
1087 r = apport.Report()1154 r = apport.Report()
@@ -1091,8 +1158,8 @@
10911158
1092 self.ui.run_crash(report_file)1159 self.ui.run_crash(report_file)
10931160
1094 self.assertEqual(self.ui.msg_title, _('Invalid problem report'))1161 self.assertTrue('not installed' in self.ui.report['InvalidReason'])
1095 self.assertEqual(self.ui.msg_severity, 'info')1162 self.assertTrue('ENOENT' in self.ui.report['InvalidMachineReason'])
10961163
1097 def test_run_crash_updated_binary(self):1164 def test_run_crash_updated_binary(self):
1098 '''run_crash() on binary that got updated in the meantime'''1165 '''run_crash() on binary that got updated in the meantime'''
10991166
=== 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-04-27 08:52:25 +0000
@@ -18,6 +18,7 @@
18import apport18import apport
19import shutil19import shutil
20import subprocess20import subprocess
21import glob
21from gi.repository import GLib, Gtk22from gi.repository import GLib, Gtk
22from apport import unicode_gettext as _23from apport import unicode_gettext as _
23from mock import patch24from mock import patch
@@ -606,8 +607,10 @@
606 self.app.w('continue_button').clicked()607 self.app.w('continue_button').clicked()
607 return False608 return False
608609
609 # remove the crash from setUp() and create a kernel oops610 self.app.report['DesktopFile'] = glob.glob('/usr/share/applications/*.desktop')[0]
610 os.remove(self.app.report_file)611 self.app.report['ProcCmdline'] = 'apport-bug apport'
612 with open(self.app.report_file, 'wb') as f:
613 self.app.report.write(f)
611 kernel_oops = subprocess.Popen([kernel_oops_path], stdin=subprocess.PIPE)614 kernel_oops = subprocess.Popen([kernel_oops_path], stdin=subprocess.PIPE)
612 kernel_oops.communicate(b'Plasma conduit phase misalignment')615 kernel_oops.communicate(b'Plasma conduit phase misalignment')
613 self.assertEqual(kernel_oops.returncode, 0)616 self.assertEqual(kernel_oops.returncode, 0)
@@ -616,8 +619,8 @@
616 self.app.run_crashes()619 self.app.run_crashes()
617620
618 # we should have reported one crash621 # we should have reported one crash
619 self.assertEqual(self.app.crashdb.latest_id(), 0)622 self.assertEqual(self.app.crashdb.latest_id(), 1)
620 r = self.app.crashdb.download(0)623 r = self.app.crashdb.download(1)
621 self.assertEqual(r['ProblemType'], 'KernelOops')624 self.assertEqual(r['ProblemType'], 'KernelOops')
622 self.assertEqual(r['OopsText'], 'Plasma conduit phase misalignment')625 self.assertEqual(r['OopsText'], 'Plasma conduit phase misalignment')
623626
@@ -627,7 +630,7 @@
627 self.assertTrue('Plasma conduit' in r['Title'])630 self.assertTrue('Plasma conduit' in r['Title'])
628631
629 # URL was opened632 # URL was opened
630 self.assertEqual(self.app.open_url.call_count, 1)633 self.assertEqual(self.app.open_url.call_count, 2)
631634
632 def test_bug_report_installed_package(self):635 def test_bug_report_installed_package(self):
633 '''Bug report for installed package'''636 '''Bug report for installed package'''

Subscribers

People subscribed via source and target branches