3
3
This encapsulates the workflow and common code for any user interface
4
4
implementation (like GTK, Qt, or CLI).
6
Copyright (C) 2007 Canonical Ltd.
7
Author: Martin Pitt <martin.pitt@ubuntu.com>
9
This program is free software; you can redistribute it and/or modify it
10
under the terms of the GNU General Public License as published by the
11
Free Software Foundation; either version 2 of the License, or (at your
12
option) any later version. See http://www.gnu.org/copyleft/gpl.html for
13
the full text of the license.
7
# Copyright (C) 2007 - 2009 Canonical Ltd.
8
# Author: Martin Pitt <martin.pitt@ubuntu.com>
10
# This program is free software; you can redistribute it and/or modify it
11
# under the terms of the GNU General Public License as published by the
12
# Free Software Foundation; either version 2 of the License, or (at your
13
# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
14
# the full text of the license.
18
18
import glob, sys, os.path, optparse, time, traceback, locale, gettext, re
19
19
import pwd, errno, urllib, zlib
255
263
if self.handle_duplicate():
258
if self.report.get('ProblemType') in ['Crash', 'KernelCrash',
260
response = self.ui_present_report_details()
261
if response == 'cancel':
263
if response == 'reduced':
265
del self.report['CoreDump']
267
pass # Huh? Should not happen, but did in https://launchpad.net/bugs/86007
269
assert response == 'full'
266
# confirm what will be sent
267
response = self.ui_present_report_details(False)
268
if response == 'cancel':
270
if response == 'reduced':
272
del self.report['CoreDump']
274
pass # Huh? Should not happen, but did in https://launchpad.net/bugs/86007
276
assert response == 'full'
271
278
self.file_report()
272
279
except IOError, e:
358
365
self.report['UnreportableReason'])
361
if not self.handle_duplicate():
362
# we do not confirm contents of bug reports, this might have
368
if self.handle_duplicate():
371
# not useful for bug reports, and has potentially sensitive information
373
del self.report['ProcCmdline']
377
if self.options.save:
365
del self.report['ProcCmdline']
379
f = open(self.options.save, 'w')
382
except (IOError, OSError), e:
383
self.ui_error_message(_('Cannot create report'), str(e))
369
385
# show what's being sent
370
response = self.ui_present_report_details()
386
response = self.ui_present_report_details(False)
371
387
if response != 'cancel':
372
388
self.file_report()
392
def run_update_report(self):
393
'''Update an existing bug with locally collected information.'''
395
# avoid irrelevant noise
396
if not self.crashdb.can_update(self.options.update_report):
397
self.ui_error_message(_('Updating problem report'),
398
_('You are not the reporter or subscriber of this '
399
'problem report, or the report is a duplicate or already '
400
'closed.\n\nPlease create a new report using "apport-bug".'))
403
is_reporter = self.crashdb.is_reporter(self.options.update_report)
406
r = self.ui_question_yesno(
407
_('You are not the reporter of this problem report. It '
408
'is much easier to mark a bug as a duplicate of another '
409
'than to move your comments and attachments to a new bug.\n\n'
410
'Subsequently, we recommend that you file a new bug report '
411
'using "apport-bug" and make a comment in this bug about '
412
'the one you file.\n\n'
413
'Do you really want to proceed?'))
417
# list of affected source packages
418
self.report = apport.Report('Bug')
419
if self.options.package:
420
pkgs = [self.options.package.strip()]
422
pkgs = self.crashdb.get_affected_packages(self.options.update_report)
424
info_collected = False
426
#print 'Collecting apport information for source package %s...' % p
428
self.report['SourcePackage'] = p
429
self.report['Package'] = p # no way to find this out
431
# we either must have the package installed or a source package hook
432
# available to collect sensible information
434
apport.packaging.get_version(p)
436
if not os.path.exists(os.path.join(apport.report._hook_dir, 'source_%s.py' % p)):
437
print 'Package %s not installed and no hook available, ignoring' % p
439
self.collect_info(ignore_uninstalled=True)
440
info_collected = True
442
if not info_collected:
443
self.ui_info_message(_('Updating problem report'),
444
_('No additional information collected.'))
447
self.report.add_user_info()
448
self.report.add_proc_environ()
450
# delete the uninteresting keys
451
del self.report['ProblemType']
452
del self.report['Date']
454
del self.report['SourcePackage']
458
if len(self.report) == 0:
459
self.ui_info_message(_('Updating problem report'),
460
_('No additional information collected.'))
463
# show what's being sent
464
response = self.ui_present_report_details(True)
465
if response != 'cancel':
466
self.crashdb.update(self.options.update_report, self.report,
467
'apport information', change_description=is_reporter,
468
attachment_comment='apport information')
376
473
def run_symptoms(self):
377
474
'''Report a bug from a list of available symptoms.
461
560
optparser.add_option('-f', '--file-bug',
462
561
help=_('Start in bug filing mode. Requires --package and an optional --pid, or just a --pid. If neither is given, display a list of known symptoms. (Implied if a single argument is given.)'),
463
562
action='store_true', dest='filebug', default=False)
563
optparser.add_option('-u', '--update-bug', type='int',
564
help=_('Start in bug updating mode. Can take an optional --package.'),
565
dest='update_report')
464
566
optparser.add_option('-s', '--symptom', metavar='SYMPTOM',
465
567
help=_('File a bug report about a symptom. (Implied if symptom name is given as only argument.)'),
473
575
optparser.add_option('-c', '--crash-file',
474
576
help=_('Report the crash from given .apport or .crash file instead of the pending ones in %s. (Implied if file is given as only argument.)') % apport.fileutils.report_dir,
475
577
action='store', type='string', dest='crash_file', default=None, metavar='PATH')
578
optparser.add_option('--save',
579
help=_('In --file-bug mode, save the collected information into a file instead of reporting it. This file can then be reported with --crash-file later on.'),
580
type='string', dest='save', default=None, metavar='PATH')
476
581
optparser.add_option('-v', '--version',
477
582
help=_('Print the Apport version number.'),
478
583
action='store_true', dest='version', default=None)
480
585
(self.options, self.args) = optparser.parse_args()
482
# "do what I mean" for one single argument
483
if len(sys.argv) != 2 or sys.argv[1].startswith('-'):
587
# "do what I mean" for zero or one arguments
588
if len(sys.argv) == 0:
591
cmd = os.environ.get('APPORT_INVOKED_AS', sys.argv[0])
592
if cmd.endswith('-update-bug') or cmd.endswith('-collect'):
593
if len(self.args) == 1:
594
self.options.update_report = self.args[0]
598
optparser.error('You need to specify a report number to update')
601
# no argument: default to "show pending crashes" except when called in
603
if len(self.args) == 0 and cmd.endswith('-bug'):
604
self.options.filebug = True
607
# one argument: guess "file bug" mode by argument type
608
if len(self.args) != 1:
487
if os.path.exists(os.path.join(symptom_script_dir, sys.argv[1] + '.py')):
612
if os.path.exists(os.path.join(symptom_script_dir, self.args[0] + '.py')):
613
self.options.filebug = True
614
self.options.symptom = self.args[0]
489
self.options.filebug = True
490
self.options.symptom = sys.argv[1]
492
617
# .crash/.apport file?
493
elif sys.argv[1].endswith('.crash') or sys.argv[1].endswith('.apport'):
618
elif self.args[0].endswith('.crash') or self.args[0].endswith('.apport'):
619
self.options.crash_file = self.args[0]
495
self.options.crash_file = sys.argv[1]
498
elif sys.argv[1].isdigit():
623
elif self.args[0].isdigit():
624
self.options.filebug = True
625
self.options.pid = self.args[0]
500
self.options.filebug = True
501
self.options.pid = sys.argv[1]
504
elif '/' in sys.argv[1]:
505
pkg = apport.packaging.get_file_package(sys.argv[1])
629
elif '/' in self.args[0]:
630
pkg = apport.packaging.get_file_package(self.args[0])
507
optparser.error('%s does not belong to a package.' % sys.argv[1])
632
optparser.error('%s does not belong to a package.' % self.args[0])
510
635
self.options.filebug = True
804
929
# ensure that the crashed program is still installed:
805
930
if self.report['ProblemType'] == 'Crash':
806
exe_path = self.report.get('InterpreterPath', self.report.get('ExecutablePath'))
807
if not exe_path or not os.path.exists(exe_path):
931
exe_path = self.report.get('ExecutablePath', '')
932
if not os.path.exists(exe_path):
808
933
msg = _('This problem report applies to a program which is not installed any more.')
809
if self.report.has_key('ExecutablePath'):
810
935
msg = '%s (%s)' % (msg, self.report['ExecutablePath'])
811
936
self.report = None
812
937
self.ui_info_message(_('Invalid problem report'), msg)
940
if 'InterpreterPath' in self.report:
941
if not os.path.exists(self.report['InterpreterPath']):
942
msg = _('This problem report applies to a program which is not installed any more.')
943
self.ui_info_message(_('Invalid problem report'), '%s (%s)'
944
% (msg, self.report['InterpreterPath']))
817
949
def get_desktop_entry(self):
1601
1741
self.assertEqual(self.ui.msg_severity, 'error')
1743
def test_run_report_bug_file(self):
1744
'''run_report_bug() with saving report into a file.'''
1746
d = os.path.join(apport.fileutils.report_dir, 'home')
1748
reportfile = os.path.join(d, 'bashisbad.apport')
1750
sys.argv = ['ui-test', '-f', '-p', 'bash', '--save', reportfile]
1751
self.ui = _TestSuiteUserInterface()
1752
self.assertEqual(self.ui.run_argv(), True)
1754
self.assertEqual(self.ui.msg_severity, None)
1755
self.assertEqual(self.ui.msg_title, None)
1756
self.assertEqual(self.ui.opened_url, None)
1757
self.failIf(self.ui.present_details_shown)
1759
self.assert_(self.ui.ic_progress_pulses > 0)
1762
r.load(open(reportfile))
1764
self.assertEqual(r['SourcePackage'], 'bash')
1765
self.assert_('Dependencies' in r.keys())
1766
self.assert_('ProcEnviron' in r.keys())
1767
self.assertEqual(r['ProblemType'], 'Bug')
1770
sys.argv = ['ui-test', '-c', reportfile]
1771
self.ui = _TestSuiteUserInterface()
1773
self.ui.present_details_response = 'full'
1774
self.assertEqual(self.ui.run_argv(), True)
1776
self.assertEqual(self.ui.msg_text, None)
1777
self.assertEqual(self.ui.msg_severity, None)
1778
self.assert_(self.ui.present_details_shown)
1603
1780
def _gen_test_crash(self):
1604
1781
'''Generate a Report with real crash data.'''
1729
1910
self.assert_('assert' in self.ui.msg_text, '%s: %s' %
1730
1911
(self.ui.msg_title, self.ui.msg_text))
1731
1912
self.assertEqual(self.ui.msg_severity, 'info')
1913
self.failIf(self.ui.present_details_shown)
1733
1915
def test_run_crash_argv_file(self):
1734
1916
'''run_crash() through a file specified on the command line.'''
1919
self.report['Package'] = 'bash'
1920
self.update_report_file()
1922
sys.argv = ['ui-test', '-c', self.report_file.name]
1923
self.ui = _TestSuiteUserInterface()
1925
self.ui.present_details_response = 'full'
1926
self.assertEqual(self.ui.run_argv(), True)
1928
self.assertEqual(self.ui.msg_text, None)
1929
self.assertEqual(self.ui.msg_severity, None)
1930
self.assert_(self.ui.present_details_shown)
1736
1933
self.report['Package'] = 'bash'
1737
1934
self.report['UnreportableReason'] = 'It stinks.'
1738
1935
self.update_report_file()
1919
2118
self.assertEqual(self.ui.msg_title, None)
1920
2119
self.assertEqual(self.ui.opened_url, None)
1921
2120
self.assertEqual(self.ui.ic_progress_pulses, 0)
2121
self.failIf(self.ui.present_details_shown)
1923
2123
# report in crash notification dialog, send report
1924
2124
r.write(open(report_file, 'w'))
1925
2125
self.ui = _TestSuiteUserInterface()
1926
2126
self.ui.present_package_error_response = 'report'
2127
self.ui.present_details_response = 'full'
1927
2128
self.ui.run_crash(report_file)
1928
2129
self.assertEqual(self.ui.msg_severity, None)
1929
2130
self.assertEqual(self.ui.msg_title, None)
1930
2131
self.assertEqual(self.ui.opened_url, 'http://bash.bugs.example.com/%i' % self.ui.crashdb.latest_id())
2132
self.assert_(self.ui.present_details_shown)
1932
2134
self.assert_('SourcePackage' in self.ui.report.keys())
1933
2135
self.assert_('Package' in self.ui.report.keys())
2005
2209
for s in bad_strings:
2006
2210
self.failIf(s in dump.getvalue(), 'dump contains sensitive string: %s' % s)
2212
def test_run_update_report_nonexisting_package_from_bug(self):
2213
'''run_update_report() on a nonexisting package (from bug).'''
2215
sys.argv = ['ui-test', '-u', '1']
2216
self.ui = _TestSuiteUserInterface()
2217
self.ui.crashdb = apport.crashdb_impl.memory.CrashDatabase(None,
2218
'', {'dummy_data': 1})
2220
self.assertEqual(self.ui.run_argv(), False)
2221
self.assert_('No additional information collected.' in
2223
self.failIf(self.ui.present_details_shown)
2225
def test_run_update_report_nonexisting_package_cli(self):
2226
'''run_update_report() on a nonexisting package (CLI argument).'''
2228
sys.argv = ['ui-test', '-u', '1', '-p', 'bar']
2229
self.ui = _TestSuiteUserInterface()
2230
self.ui.crashdb = apport.crashdb_impl.memory.CrashDatabase(None,
2231
'', {'dummy_data': 1})
2233
self.assertEqual(self.ui.run_argv(), False)
2234
self.assert_('No additional information collected.' in
2236
self.failIf(self.ui.present_details_shown)
2238
def test_run_update_report_existing_package_from_bug(self):
2239
'''run_update_report() on an existing package (from bug).'''
2241
sys.argv = ['ui-test', '-u', '1']
2242
self.ui = _TestSuiteUserInterface()
2243
self.ui.crashdb = apport.crashdb_impl.memory.CrashDatabase(None,
2244
'', {'dummy_data': 1})
2246
self.ui.crashdb.download(1)['SourcePackage'] = 'bash'
2247
self.ui.crashdb.download(1)['Package'] = 'bash'
2248
self.assertEqual(self.ui.run_argv(), True)
2249
self.assertEqual(self.ui.msg_severity, None, self.ui.msg_text)
2250
self.assertEqual(self.ui.msg_title, None)
2251
self.assertEqual(self.ui.opened_url, None)
2252
self.assert_(self.ui.present_details_shown)
2254
self.assert_(self.ui.ic_progress_pulses > 0)
2255
self.assert_(self.ui.report['Package'].startswith('bash '))
2256
self.assert_('Dependencies' in self.ui.report.keys())
2257
self.assert_('ProcEnviron' in self.ui.report.keys())
2259
def test_run_update_report_existing_package_cli(self):
2260
'''run_update_report() on an existing package (CLI argument).'''
2262
sys.argv = ['ui-test', '-u', '1', '-p', 'bash']
2263
self.ui = _TestSuiteUserInterface()
2264
self.ui.crashdb = apport.crashdb_impl.memory.CrashDatabase(None,
2265
'', {'dummy_data': 1})
2267
self.assertEqual(self.ui.run_argv(), True)
2268
self.assertEqual(self.ui.msg_severity, None, self.ui.msg_text)
2269
self.assertEqual(self.ui.msg_title, None)
2270
self.assertEqual(self.ui.opened_url, None)
2271
self.assert_(self.ui.present_details_shown)
2273
self.assert_(self.ui.ic_progress_pulses > 0)
2274
self.assert_(self.ui.report['Package'].startswith('bash '))
2275
self.assert_('Dependencies' in self.ui.report.keys())
2276
self.assert_('ProcEnviron' in self.ui.report.keys())
2278
def test_run_update_report_noninstalled_but_hook(self):
2279
'''run_update_report() on an uninstalled package with a source hook.'''
2281
sys.argv = ['ui-test', '-u', '1']
2282
self.ui = _TestSuiteUserInterface()
2283
self.ui.crashdb = apport.crashdb_impl.memory.CrashDatabase(None,
2284
'', {'dummy_data': 1})
2286
f = open(os.path.join(self.hookdir, 'source_foo.py'), 'w')
2287
f.write('def add_info(r, ui):\n r["MachineType"]="Laptop"\n')
2290
self.assertEqual(self.ui.run_argv(), True, self.ui.report)
2291
self.assertEqual(self.ui.msg_severity, None, self.ui.msg_text)
2292
self.assertEqual(self.ui.msg_title, None)
2293
self.assertEqual(self.ui.opened_url, None)
2294
self.assert_(self.ui.present_details_shown)
2296
self.assert_(self.ui.ic_progress_pulses > 0)
2297
self.assertEqual(self.ui.report['Package'], 'foo (not installed)')
2298
self.assertEqual(self.ui.report['MachineType'], 'Laptop')
2299
self.assert_('ProcEnviron' in self.ui.report.keys())
2008
2301
def _run_hook(self, code):
2009
2302
f = open(os.path.join(self.hookdir, 'coreutils.py'), 'w')
2010
2303
f.write('def add_info(report, ui):\n%s\n' %
2197
2492
self.assertEqual(self.ui.run_argv(), True)
2198
2493
self.assertEqual(self.ui.msg_severity, None)
2199
2494
self.assert_('kind of problem' in self.ui.msg_text)
2200
self.assertEqual(self.ui.msg_choices,
2201
['bar', 'foo does not work', 'Other problem'])
2495
self.assertEqual(set(self.ui.msg_choices),
2496
set(['bar', 'foo does not work', 'Other problem']))
2204
2499
self.assertEqual(self.ui.ic_progress_pulses, 0)
2205
2500
self.assertEqual(self.ui.report, None)
2501
self.failIf(self.ui.present_details_shown)
2207
2503
# now, choose foo -> bash report
2208
self.ui.question_choice_response = [1]
2504
self.ui.question_choice_response = [self.ui.msg_choices.index('foo does not work')]
2209
2505
self.assertEqual(self.ui.run_argv(), True)
2210
2506
self.assertEqual(self.ui.msg_severity, None)
2211
2507
self.assert_(self.ui.ic_progress_pulses > 0)
2508
self.assert_(self.ui.present_details_shown)
2212
2509
self.assert_(self.ui.report['Package'].startswith('bash'))
2214
2511
def test_parse_argv(self):
2215
2512
'''parse_args() option inference for a single argument'''
2217
def _chk(arg, expected_opts):
2218
sys.argv = ['ui-test']
2514
def _chk(program_name, arg, expected_opts, argv_options=None):
2515
sys.argv = [program_name]
2517
sys.argv += argv_options
2220
2519
sys.argv.append(arg)
2221
ui = UserInterface()
2520
orig_stderr = sys.stderr
2521
sys.stderr = open('/dev/null', 'w')
2523
ui = UserInterface()
2526
sys.stderr = orig_stderr
2223
2527
expected_opts['version'] = None
2224
2528
self.assertEqual(ui.args, [])
2225
2529
self.assertEqual(ui.options, expected_opts)
2227
2531
# no arguments -> show pending crashes
2228
_chk(None, {'filebug': False, 'package': None,
2229
'pid': None, 'crash_file': None, 'symptom': None})
2532
_chk('apport-gtk', None, {'filebug': False, 'package': None,
2533
'pid': None, 'crash_file': None, 'symptom': None,
2534
'update_report': None, 'save': None})
2535
# ... except when being called as '*-bug', then default to bug mode
2536
_chk('apport-bug', None, {'filebug': True, 'package': None,
2537
'pid': None, 'crash_file': None, 'symptom': None,
2538
'update_report': None, 'save': None})
2539
# updating report not allowed without args
2540
self.assertRaises(SystemExit, _chk, 'apport-collect', None, {})
2232
_chk('coreutils', {'filebug': True, 'package': 'coreutils',
2233
'pid': None, 'crash_file': None, 'symptom': None})
2543
_chk('apport-kde', 'coreutils', {'filebug': True, 'package':
2544
'coreutils', 'pid': None, 'crash_file': None, 'symptom': None,
2545
'update_report': None, 'save': None})
2546
_chk('apport-bug', 'coreutils', {'filebug': True, 'package':
2547
'coreutils', 'pid': None, 'crash_file': None, 'symptom': None,
2548
'update_report': None, 'save': None})
2549
_chk('apport-bug', 'coreutils', {'filebug': True, 'package':
2550
'coreutils', 'pid': None, 'crash_file': None, 'symptom': None,
2551
'update_report': None, 'save': 'foo.apport'},
2552
['--save', 'foo.apport'])
2235
2554
# symptom is preferred over package
2236
2555
f = open(os.path.join(symptom_script_dir, 'coreutils.py'), 'w')
2242
_chk('coreutils', {'filebug': True, 'package': None,
2243
'pid': None, 'crash_file': None, 'symptom': 'coreutils'})
2561
_chk('apport-cli', 'coreutils', {'filebug': True, 'package': None,
2562
'pid': None, 'crash_file': None, 'symptom': 'coreutils',
2563
'update_report': None, 'save': None})
2564
_chk('apport-bug', 'coreutils', {'filebug': True, 'package': None,
2565
'pid': None, 'crash_file': None, 'symptom': 'coreutils',
2566
'update_report': None, 'save': None})
2246
_chk('1234', {'filebug': True, 'package': None,
2247
'pid': '1234', 'crash_file': None, 'symptom': None})
2569
_chk('apport-cli', '1234', {'filebug': True, 'package': None,
2570
'pid': '1234', 'crash_file': None, 'symptom': None,
2571
'update_report': None, 'save': None})
2572
_chk('apport-bug', '1234', {'filebug': True, 'package': None,
2573
'pid': '1234', 'crash_file': None, 'symptom': None,
2574
'update_report': None, 'save': None})
2249
2576
# .crash/.apport files; check correct handling of spaces
2250
2577
for suffix in ('.crash', '.apport'):
2251
_chk('/tmp/f oo' + suffix, {'filebug': False, 'package': None,
2252
'pid': None, 'crash_file': '/tmp/f oo' + suffix, 'symptom': None})
2578
for prog in ('apport-cli', 'apport-bug'):
2579
_chk(prog, '/tmp/f oo' + suffix, {'filebug': False,
2580
'package': None, 'pid': None,
2581
'crash_file': '/tmp/f oo' + suffix, 'symptom': None,
2582
'update_report': None, 'save': None})
2255
_chk('/usr/bin/tail', {'filebug': True, 'package': 'coreutils',
2256
'pid': None, 'crash_file': None, 'symptom': None})
2585
_chk('apport-cli', '/usr/bin/tail', {'filebug': True,
2586
'package': 'coreutils',
2587
'pid': None, 'crash_file': None, 'symptom': None,
2588
'update_report': None, 'save': None})
2589
_chk('apport-bug', '/usr/bin/tail', {'filebug': True,
2590
'package': 'coreutils',
2591
'pid': None, 'crash_file': None, 'symptom': None,
2592
'update_report': None, 'save': None})
2594
# update existing report
2595
_chk('apport-collect', '1234', {'filebug': False, 'package': None,
2596
'pid': None, 'crash_file': None, 'symptom': None,
2597
'update_report': '1234', 'save': None})
2598
_chk('apport-update-bug', '1234', {'filebug': False,
2599
'package': None, 'pid': None, 'crash_file': None,
2600
'symptom': None, 'update_report': '1234', 'save': None})
2258
2602
unittest.main()