~ubuntu-branches/ubuntu/utopic/apport/utopic

« back to all changes in this revision

Viewing changes to apport/crashdb_impl/launchpad.py

  • Committer: Package Import Robot
  • Author(s): Martin Pitt
  • Date: 2012-07-09 08:14:30 UTC
  • mfrom: (148.1.66)
  • Revision ID: package-import@ubuntu.com-20120709081430-jim1xdcbior31pph
Tags: 2.3-0ubuntu1
* New upstream release:
  - launchpad.py: Rework test suite to not use Launchpad's +storeblob
    facility at all any more. It almost never works on staging and is
    horribly slow. Fake the bug creation from a blob by manually creating
    the comment and attachments ourselves, and just assume that storeblob
    works on production.  Also change the structure to allow running every
    test individually.
  - crash-digger: Add --crash-db option to specify a non-default crash
    database name. (LP: #1003506)
  - apport-gtk: Add --hanging option to specify the process ID of a hanging
    application. If the user chooses to report this error, apport will
    terminate the pid with SIGABRT, otherwise it will send SIGKILL. The
    normal core pipe handler will be used to process the resulting report
    file, with a .hanging file in /var/crash to separate these from regular
    crashes.
  - apport: Also treat a binary as modified if the /proc/pid/exe symlink
    does not point to an existing file any more. (LP: #984944)
  - Fix PEP-8 violations picked up by latest pep8 checker.
  - ui.py: Do not ignore certain exceptions during upload which are not
    likely to be a network error.
  - launchpad.py: Recongize Launchpad projects for bug query and marking
    operations. (LP: #1003506)
  - packaging-apt-dpkg.py: Fix get_source_tree() to work with apt sandboxes.
  - apport-retrace: Turn StacktraceSource generation back on, now that it
    works with the current sandboxing.
  - launchpad.py: Ensure that upload chunk size does not underrun.
    (LP: #1013334)
  - apport_python_hook: Fix UnicodeEncodeError crash with Python 2 for
    exceptions with non-ASCII characters. (LP: #972436)
  - test_ui_kde.py: Fix occasional test failure in test_1_crash_details if
    the application ends before the "is progress bar visible" check is done.

Show diffs side-by-side

added added

removed removed

Lines of Context:
10
10
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
11
11
# the full text of the license.
12
12
 
13
 
import urllib, tempfile, os.path, re, gzip, sys, email, time
 
13
import tempfile, os.path, re, gzip, sys, email, time
14
14
 
15
15
from io import BytesIO
16
16
 
135
135
 
136
136
        try:
137
137
            self.__launchpad = Launchpad.login_with('apport-collect',
138
 
                    launchpad_instance, launchpadlib_dir=self.__lpcache,
139
 
                    allow_access_levels=['WRITE_PRIVATE'],
140
 
                    credentials_file=self.auth,
141
 
                    version='1.0')
 
138
                                                    launchpad_instance,
 
139
                                                    launchpadlib_dir=self.__lpcache,
 
140
                                                    allow_access_levels=['WRITE_PRIVATE'],
 
141
                                                    credentials_file=self.auth,
 
142
                                                    version='1.0')
142
143
        except Exception as e:
143
144
            if hasattr(e, 'content'):
144
145
                msg = e.content
160
161
 
161
162
    @property
162
163
    def lp_distro(self):
163
 
        if not self.distro:
164
 
            return None
165
164
        if self.__lp_distro is None:
166
 
            self.__lp_distro = self.launchpad.distributions[self.distro]
 
165
            if self.distro:
 
166
                self.__lp_distro = self.launchpad.distributions[self.distro]
 
167
            elif 'project' in self.options:
 
168
                self.__lp_distro = self.launchpad.projects[self.options['project']]
 
169
            else:
 
170
                raise SystemError('distro or project needs to be specified in crashdb options')
 
171
 
167
172
        return self.__lp_distro
168
173
 
169
174
    def upload(self, report, progress_callback=None):
178
183
        '''
179
184
        assert self.accepts(report)
180
185
 
181
 
        # set reprocessing tags
182
 
        hdr = {}
183
 
        hdr['Tags'] = 'apport-%s' % report['ProblemType'].lower()
184
 
        a = report.get('PackageArchitecture')
185
 
        if not a or a == 'all':
186
 
            a = report.get('Architecture')
187
 
        if a:
188
 
            hdr['Tags'] += ' ' + a
189
 
        if 'Tags' in report:
190
 
            hdr['Tags'] += ' ' + report['Tags'].lower()
191
 
 
192
 
        # privacy/retracing for distro reports
193
 
        # FIXME: ugly hack until LP has a real crash db
194
 
        if 'DistroRelease' in report:
195
 
            if a and ('VmCore' in report or 'CoreDump' in report):
196
 
                hdr['Private'] = 'yes'
197
 
                hdr['Subscribers'] = self.options.get('initial_subscriber', 'apport')
198
 
                hdr['Tags'] += ' need-%s-retrace' % a
199
 
            elif 'Traceback' in report:
200
 
                hdr['Private'] = 'yes'
201
 
                hdr['Subscribers'] = 'apport'
202
 
                hdr['Tags'] += ' need-duplicate-check'
203
 
        if 'DuplicateSignature' in report and 'need-duplicate-check' not in hdr['Tags']:
204
 
                hdr['Tags'] += ' need-duplicate-check'
205
 
 
206
 
        # if we have checkbox submission key, link it to the bug; keep text
207
 
        # reference until the link is shown in Launchpad's UI
208
 
        if 'CheckboxSubmission' in report:
209
 
            hdr['HWDB-Submission'] = report['CheckboxSubmission']
210
 
 
211
 
        # order in which keys should appear in the temporary file
212
 
        order = ['ProblemType', 'DistroRelease', 'Package', 'Regression', 'Reproducible',
213
 
        'TestedUpstream', 'ProcVersionSignature', 'Uname', 'NonfreeKernelModules']
214
 
 
215
 
        # write MIME/Multipart version into temporary file
216
 
        mime = tempfile.TemporaryFile()
217
 
        report.write_mime(mime, extra_headers=hdr, skip_keys=['Tags'], priority_fields=order)
218
 
        mime.flush()
219
 
        mime.seek(0)
220
 
 
221
 
        ticket = upload_blob(mime, progress_callback,
222
 
                hostname=self.get_hostname())
 
186
        blob_file = self._generate_upload_blob(report)
 
187
        ticket = upload_blob(blob_file, progress_callback, hostname=self.get_hostname())
 
188
        blob_file.close()
223
189
        assert ticket
224
190
        return ticket
225
191
 
360
326
        return report
361
327
 
362
328
    def update(self, id, report, comment, change_description=False,
363
 
            attachment_comment=None, key_filter=None):
 
329
               attachment_comment=None, key_filter=None):
364
330
        '''Update the given report ID with all data from report.
365
331
 
366
332
        This creates a text comment with the "short" data (see
420
386
            bug.description = bug.description + '\n--- \n' + part.get_payload(decode=True).decode('UTF-8', 'replace')
421
387
            bug.lp_save()
422
388
        else:
423
 
            bug.newMessage(content=part.get_payload(decode=True),
424
 
                subject=comment)
 
389
            bug.newMessage(content=part.get_payload(decode=True), subject=comment)
425
390
 
426
391
        # other parts are the attachments:
427
392
        for part in msg_iter:
428
393
            # print '   attachment: %s...' % part.get_filename()
429
394
            bug.addAttachment(comment=attachment_comment or '',
430
 
                description=part.get_filename(),
431
 
                content_type=None,
432
 
                data=part.get_payload(decode=True),
433
 
                filename=part.get_filename(), is_patch=False)
 
395
                              description=part.get_filename(),
 
396
                              content_type=None,
 
397
                              data=part.get_payload(decode=True),
 
398
                              filename=part.get_filename(), is_patch=False)
434
399
 
435
400
    def update_traces(self, id, report, comment=''):
436
401
        '''Update the given report ID for retracing results.
493
458
        '''Return list of affected source packages for given ID.'''
494
459
 
495
460
        bug_target_re = re.compile(
496
 
                    r'/%s/(?:(?P<suite>[^/]+)/)?\+source/(?P<source>[^/]+)$' %
497
 
                    self.distro)
 
461
            r'/%s/(?:(?P<suite>[^/]+)/)?\+source/(?P<source>[^/]+)$' % self.distro)
498
462
 
499
463
        bug = self.launchpad.bugs[id]
500
464
        result = []
616
580
 
617
581
        if self.distro:
618
582
            distro_identifier = '(%s)' % self.distro.lower()
619
 
            fixed_tasks = filter(lambda task: task.status == 'Fix Released' and \
620
 
                    distro_identifier in task.bug_target_display_name.lower(), tasks)
 
583
            fixed_tasks = filter(lambda task: task.status == 'Fix Released' and
 
584
                                 distro_identifier in task.bug_target_display_name.lower(), tasks)
621
585
 
622
586
            if not fixed_tasks:
623
 
                fixed_distro = filter(lambda task: task.status == 'Fix Released' and \
624
 
                        task.bug_target_name.lower() == self.distro.lower(), tasks)
 
587
                fixed_distro = filter(lambda task: task.status == 'Fix Released' and
 
588
                                      task.bug_target_name.lower() == self.distro.lower(), tasks)
625
589
                if fixed_distro:
626
590
                    # fixed in distro inself (without source package)
627
591
                    return ''
639
603
                    return 'invalid'
640
604
            else:
641
605
                # check if there only invalid ones
642
 
                invalid_tasks = filter(lambda task: task.status in ('Invalid', "Won't Fix", 'Expired') and \
643
 
                        distro_identifier in task.bug_target_display_name.lower(), tasks)
 
606
                invalid_tasks = filter(lambda task: task.status in ('Invalid', "Won't Fix", 'Expired') and
 
607
                                       distro_identifier in task.bug_target_display_name.lower(), tasks)
644
608
                if invalid_tasks:
645
 
                    non_invalid_tasks = filter(lambda task: task.status not in ('Invalid', "Won't Fix", 'Expired') and \
 
609
                    non_invalid_tasks = filter(
 
610
                        lambda task: task.status not in ('Invalid', "Won't Fix", 'Expired') and
646
611
                        distro_identifier in task.bug_target_display_name.lower(), tasks)
647
612
                    if not non_invalid_tasks:
648
613
                        return 'invalid'
649
614
        else:
650
 
            fixed_tasks = filter(lambda task: task.status == 'Fix Released',
651
 
                    tasks)
 
615
            fixed_tasks = filter(lambda task: task.status == 'Fix Released', tasks)
652
616
            if fixed_tasks:
653
617
                # TODO: look for current series
654
618
                return ''
687
651
                if master.id == id:
688
652
                    # this happens if the bug was manually duped to a newer one
689
653
                    apport.warning('Bug %i was manually marked as a dupe of newer bug %i, not closing as duplicate',
690
 
                            id, master_id)
 
654
                                   id, master_id)
691
655
                    return
692
656
 
693
657
            for a in bug.attachments:
694
658
                if a.title in ('CoreDump.gz', 'Stacktrace.txt',
695
 
                    'ThreadStacktrace.txt', 'ProcMaps.txt', 'ProcStatus.txt',
696
 
                    'Registers.txt', 'Disassembly.txt'):
 
659
                               'ThreadStacktrace.txt', 'ProcMaps.txt',
 
660
                               'ProcStatus.txt', 'Registers.txt',
 
661
                               'Disassembly.txt'):
697
662
                    try:
698
663
                        a.removeFromBug()
699
664
                    except HTTPError:
707
672
provide, or to see if there is a workaround for the bug.  Additionally, any \
708
673
further discussion regarding the bug should occur in the other report.  \
709
674
Please continue to report any other bugs you may find.' % master_id,
710
 
                subject='This bug is a duplicate')
 
675
                           subject='This bug is a duplicate')
711
676
 
712
677
            bug = self.launchpad.bugs[id]  # refresh, LP#336866 workaround
713
678
            if bug.private:
722
687
            master_tags = master.tags
723
688
 
724
689
            if len(master.duplicates) == 10:
725
 
                if 'escalation_tag' in self.options and \
726
 
                    self.options['escalation_tag'] not in master_tags and \
727
 
                    self.options.get('escalated_tag', ' invalid ') not in master_tags:
728
 
                        master.tags = master_tags + [self.options['escalation_tag']]  # LP#254901 workaround
729
 
                        master.lp_save()
 
690
                if 'escalation_tag' in self.options and self.options['escalation_tag'] not in master_tags and self.options.get('escalated_tag', ' invalid ') not in master_tags:
 
691
                    master.tags = master_tags + [self.options['escalation_tag']]  # LP#254901 workaround
 
692
                    master.lp_save()
730
693
 
731
 
                if 'escalation_subscription' in self.options and \
732
 
                    self.options.get('escalated_tag', ' invalid ') not in master_tags:
 
694
                if 'escalation_subscription' in self.options and self.options.get('escalated_tag', ' invalid ') not in master_tags:
733
695
                    p = self.launchpad.people[self.options['escalation_subscription']]
734
696
                    master.subscribe(person=p)
735
697
 
736
698
            # requesting updated stack trace?
737
699
            if report.has_useful_stacktrace() and ('apport-request-retrace' in master_tags
738
 
                    or 'apport-failed-retrace' in master_tags):
 
700
                                                   or 'apport-failed-retrace' in master_tags):
739
701
                self.update(master_id, report, 'Updated stack trace from duplicate bug %i' % id,
740
 
                        key_filter=['Stacktrace', 'ThreadStacktrace',
741
 
                            'Package', 'Dependencies', 'ProcMaps', 'ProcCmdline'])
 
702
                            key_filter=['Stacktrace', 'ThreadStacktrace',
 
703
                                        'Package', 'Dependencies', 'ProcMaps', 'ProcCmdline'])
742
704
 
743
705
                master = self.launchpad.bugs[master_id]
744
706
                x = master.tags[:]  # LP#254901 workaround
760
722
            tags_to_copy = ['bugpattern-needed', 'running-unity']
761
723
            for series in self.lp_distro.series:
762
724
                if series.status not in ['Active Development',
763
 
                    'Current Stable Release', 'Supported']:
 
725
                                         'Current Stable Release', 'Supported']:
764
726
                    continue
765
727
                tags_to_copy.append(series.name)
766
728
            # copy tags over from the duplicate bug to the master bug
792
754
However, the latter was already fixed in an earlier package version than the \
793
755
one in this report. This might be a regression or because the problem is \
794
756
in a dependent package.' % master,
795
 
            subject='Possible regression detected')
 
757
                       subject='Possible regression detected')
796
758
        bug = self.launchpad.bugs[id]  # fresh bug object, LP#336866 workaround
797
759
        bug.tags = bug.tags + ['regression-retracer']  # LP#254901 workaround
798
760
        bug.lp_save()
823
785
            task.status = 'Invalid'
824
786
            task.lp_save()
825
787
            bug.newMessage(content=invalid_msg,
826
 
                    subject='Crash report cannot be processed')
 
788
                           subject='Crash report cannot be processed')
827
789
 
828
790
            for a in bug.attachments:
829
791
                if a.title == 'CoreDump.gz':
930
892
 
931
893
        #use a url hack here, it is faster
932
894
        person = '%s~%s' % (self.launchpad._root_uri,
933
 
            self.options.get('triaging_team', 'ubuntu-crashes-universe'))
 
895
                            self.options.get('triaging_team', 'ubuntu-crashes-universe'))
934
896
        bug.subscribe(person=person)
935
897
 
 
898
    def _generate_upload_blob(self, report):
 
899
        '''Generate a multipart/MIME temporary file for uploading.
 
900
 
 
901
        You have to close the returned file object after you are done with it.
 
902
        '''
 
903
        # set reprocessing tags
 
904
        hdr = {}
 
905
        hdr['Tags'] = 'apport-%s' % report['ProblemType'].lower()
 
906
        a = report.get('PackageArchitecture')
 
907
        if not a or a == 'all':
 
908
            a = report.get('Architecture')
 
909
        if a:
 
910
            hdr['Tags'] += ' ' + a
 
911
        if 'Tags' in report:
 
912
            hdr['Tags'] += ' ' + report['Tags'].lower()
 
913
 
 
914
        # privacy/retracing for distro reports
 
915
        # FIXME: ugly hack until LP has a real crash db
 
916
        if 'DistroRelease' in report:
 
917
            if a and ('VmCore' in report or 'CoreDump' in report):
 
918
                hdr['Private'] = 'yes'
 
919
                hdr['Subscribers'] = self.options.get('initial_subscriber', 'apport')
 
920
                hdr['Tags'] += ' need-%s-retrace' % a
 
921
            elif 'Traceback' in report:
 
922
                hdr['Private'] = 'yes'
 
923
                hdr['Subscribers'] = 'apport'
 
924
                hdr['Tags'] += ' need-duplicate-check'
 
925
        if 'DuplicateSignature' in report and 'need-duplicate-check' not in hdr['Tags']:
 
926
                hdr['Tags'] += ' need-duplicate-check'
 
927
 
 
928
        # if we have checkbox submission key, link it to the bug; keep text
 
929
        # reference until the link is shown in Launchpad's UI
 
930
        if 'CheckboxSubmission' in report:
 
931
            hdr['HWDB-Submission'] = report['CheckboxSubmission']
 
932
 
 
933
        # order in which keys should appear in the temporary file
 
934
        order = ['ProblemType', 'DistroRelease', 'Package', 'Regression', 'Reproducible',
 
935
                 'TestedUpstream', 'ProcVersionSignature', 'Uname', 'NonfreeKernelModules']
 
936
 
 
937
        # write MIME/Multipart version into temporary file
 
938
        mime = tempfile.TemporaryFile()
 
939
        report.write_mime(mime, extra_headers=hdr, skip_keys=['Tags'], priority_fields=order)
 
940
        mime.flush()
 
941
        mime.seek(0)
 
942
 
 
943
        return mime
 
944
 
936
945
#
937
946
# Launchpad storeblob API (should go into launchpadlib, see LP #315358)
938
947
#
968
977
 
969
978
            # adjust chunksize so that it takes between .5 and 2
970
979
            # seconds to send a chunk
971
 
            if t2 - t1 < .5:
972
 
                chunksize *= 2
973
 
            elif t2 - t1 > 2:
974
 
                chunksize /= 2
 
980
            if chunksize > 1024:
 
981
                if t2 - t1 < .5:
 
982
                    chunksize <<= 1
 
983
                elif t2 - t1 > 2:
 
984
                    chunksize >>= 1
975
985
 
976
986
 
977
987
class HTTPSProgressHandler(HTTPSHandler):
1036
1046
 
1037
1047
if __name__ == '__main__':
1038
1048
    import unittest, atexit, shutil, subprocess
 
1049
    import mock
1039
1050
 
1040
1051
    crashdb = None
1041
 
    segv_report = None
1042
 
    python_report = None
 
1052
    _segv_report = None
 
1053
    _python_report = None
 
1054
    _uncommon_description_report = None
1043
1055
 
1044
1056
    class _T(unittest.TestCase):
1045
1057
        # this assumes that a source package 'coreutils' exists and builds a
1065
1077
            self.ref_report['SourcePackage'] = 'coreutils'
1066
1078
 
1067
1079
            # Objects tests rely on.
1068
 
            self.uncommon_description_bug = self._file_uncommon_description_bug()
1069
1080
            self._create_project('langpack-o-matic')
1070
1081
 
1071
 
            # XXX Should create new bug reports, not reuse those.
1072
 
            self.known_test_id = self.uncommon_description_bug.id
1073
 
            self.known_test_id2 = self._file_uncommon_description_bug().id
1074
 
 
1075
1082
        def _create_project(self, name):
1076
1083
            '''Create a project using launchpadlib to be used by tests.'''
1077
1084
 
1084
1091
                    summary=name + 'summary',
1085
1092
                    title=name + 'title')
1086
1093
 
1087
 
        def _file_uncommon_description_bug(self):
 
1094
        @property
 
1095
        def hostname(self):
 
1096
            '''Get the Launchpad hostname for the given crashdb.'''
 
1097
 
 
1098
            return self.crashdb.get_hostname()
 
1099
 
 
1100
        def get_segv_report(self, force_fresh=False):
 
1101
            '''Generate SEGV crash report.
 
1102
 
 
1103
            This is only done once, subsequent calls will return the already
 
1104
            existing ID, unless force_fresh is True.
 
1105
 
 
1106
            Return the ID.
 
1107
            '''
 
1108
            global _segv_report
 
1109
            if not force_fresh and _segv_report is not None:
 
1110
                return _segv_report
 
1111
 
 
1112
            r = self._generate_sigsegv_report()
 
1113
            r.add_package_info(self.test_package)
 
1114
            r.add_os_info()
 
1115
            r.add_gdb_info()
 
1116
            r.add_user_info()
 
1117
            self.assertEqual(r.standard_title(), 'crash crashed with SIGSEGV in f()')
 
1118
 
 
1119
            # add some binary gibberish which isn't UTF-8
 
1120
            r['ShortGibberish'] = ' "]\xb6"\n'
 
1121
            r['LongGibberish'] = 'a\nb\nc\nd\ne\n\xff\xff\xff\n\f'
 
1122
 
 
1123
            # create a bug for the report
 
1124
            bug_target = self._get_bug_target(self.crashdb, r)
 
1125
            self.assertTrue(bug_target)
 
1126
 
 
1127
            id = self._file_bug(bug_target, r)
 
1128
            self.assertTrue(id > 0)
 
1129
 
 
1130
            sys.stderr.write('(Created SEGV report: https://%s/bugs/%i) ' % (self.hostname, id))
 
1131
            if not force_fresh:
 
1132
                _segv_report = id
 
1133
            return id
 
1134
 
 
1135
        def get_python_report(self):
 
1136
            '''Generate Python crash report.
 
1137
 
 
1138
            Return the ID.
 
1139
            '''
 
1140
            global _python_report
 
1141
            if _python_report is not None:
 
1142
                return _python_report
 
1143
 
 
1144
            r = apport.Report('Crash')
 
1145
            r['ExecutablePath'] = '/bin/foo'
 
1146
            r['Traceback'] = '''Traceback (most recent call last):
 
1147
  File "/bin/foo", line 67, in fuzz
 
1148
    print(weird)
 
1149
NameError: global name 'weird' is not defined'''
 
1150
            r['Tags'] = 'boogus pybogus'
 
1151
            r.add_package_info(self.test_package)
 
1152
            r.add_os_info()
 
1153
            r.add_user_info()
 
1154
            self.assertEqual(r.standard_title(),
 
1155
                             "foo crashed with NameError in fuzz(): global name 'weird' is not defined")
 
1156
 
 
1157
            bug_target = self._get_bug_target(self.crashdb, r)
 
1158
            self.assertTrue(bug_target)
 
1159
 
 
1160
            id = self._file_bug(bug_target, r)
 
1161
            self.assertTrue(id > 0)
 
1162
            sys.stderr.write('(Created Python report: https://%s/bugs/%i) ' % (self.hostname, id))
 
1163
            _python_report = id
 
1164
            return id
 
1165
 
 
1166
        def get_uncommon_description_report(self, force_fresh=False):
1088
1167
            '''File a bug report with an uncommon description.
1089
1168
 
 
1169
            This is only done once, subsequent calls will return the already
 
1170
            existing ID, unless force_fresh is True.
 
1171
 
1090
1172
            Example taken from real LP bug 269539. It contains only
1091
1173
            ProblemType/Architecture/DistroRelease in the description, and has
1092
1174
            free-form description text after the Apport data.
 
1175
 
 
1176
            Return the ID.
1093
1177
            '''
 
1178
            global _uncommon_description_report
 
1179
            if not force_fresh and _uncommon_description_report is not None:
 
1180
                return _uncommon_description_report
 
1181
 
1094
1182
            desc = '''problem
1095
1183
 
1096
1184
ProblemType: Package
1101
1189
 
1102
1190
and more
1103
1191
'''
1104
 
            return self.crashdb.launchpad.bugs.createBug(
 
1192
            bug = self.crashdb.launchpad.bugs.createBug(
1105
1193
                title=b'mixed description bug'.encode(),
1106
1194
                description=desc,
1107
1195
                target=self.crashdb.lp_distro)
1108
 
 
1109
 
        @property
1110
 
        def hostname(self):
1111
 
            '''Get the Launchpad hostname for the given crashdb.'''
1112
 
 
1113
 
            return self.crashdb.get_hostname()
1114
 
 
1115
 
        def _file_segv_report(self):
1116
 
            '''File a SEGV crash report.
1117
 
 
1118
 
            Return (crash ID, report).
1119
 
            '''
1120
 
            r = self._generate_sigsegv_report()
1121
 
            r.add_package_info(self.test_package)
1122
 
            r.add_os_info()
1123
 
            r.add_gdb_info()
1124
 
            r.add_user_info()
1125
 
            self.assertEqual(r.standard_title(), 'crash crashed with SIGSEGV in f()')
1126
 
 
1127
 
            # add some binary gibberish which isn't UTF-8
1128
 
            r['ShortGibberish'] = ' "]\xb6"\n'
1129
 
            r['LongGibberish'] = 'a\nb\nc\nd\ne\n\xff\xff\xff\n\f'
1130
 
 
1131
 
            handle = self.crashdb.upload(r)
1132
 
            self.assertTrue(handle)
1133
 
            bug_target = self._get_bug_target(self.crashdb, r)
1134
 
            self.assertTrue(bug_target)
1135
 
 
1136
 
            id = self._file_bug(bug_target, r, handle)
1137
 
            self.assertTrue(id > 0)
1138
 
            return (id, r)
1139
 
 
1140
 
        def test_1_report_segv(self):
1141
 
            '''upload() and get_comment_url() for SEGV crash
1142
 
 
1143
 
            This needs to run first, since it sets segv_report.
1144
 
            '''
1145
 
            global segv_report
1146
 
            (id, report) = self._file_segv_report()
1147
 
            segv_report = id
1148
 
            url = self.crashdb.get_comment_url(report, id)
1149
 
 
1150
 
            sys.stderr.write('(https://%s/bugs/%i) ' % (self.hostname, id))
1151
 
 
1152
 
            #TODO: check this programatically
1153
 
            sys.stderr.write('[%s] ' % url)
1154
 
 
1155
 
        def test_1_report_python(self):
1156
 
            '''upload() and get_comment_url() for Python crash
1157
 
 
1158
 
            This needs to run early, since it sets python_report.
1159
 
            '''
1160
 
            r = apport.Report('Crash')
1161
 
            r['ExecutablePath'] = '/bin/foo'
1162
 
            r['Traceback'] = '''Traceback (most recent call last):
1163
 
  File "/bin/foo", line 67, in fuzz
1164
 
    print(weird)
1165
 
NameError: global name 'weird' is not defined'''
1166
 
            r['Tags'] = 'boogus pybogus'
1167
 
            r.add_package_info(self.test_package)
1168
 
            r.add_os_info()
1169
 
            r.add_user_info()
1170
 
            self.assertEqual(r.standard_title(),
1171
 
                "foo crashed with NameError in fuzz(): global name 'weird' is not defined")
1172
 
 
1173
 
            handle = self.crashdb.upload(r)
1174
 
            self.assertTrue(handle)
1175
 
            bug_target = self._get_bug_target(self.crashdb, r)
1176
 
            self.assertTrue(bug_target)
1177
 
 
1178
 
            id = self._file_bug(bug_target, r, handle)
1179
 
            self.assertTrue(id > 0)
1180
 
            global python_report
1181
 
            python_report = id
1182
 
            sys.stderr.write('(https://%s/bugs/%i) ' % (self.hostname, id))
1183
 
 
1184
 
        def test_2_download(self):
 
1196
            sys.stderr.write('(Created uncommon description: https://%s/bugs/%i) ' % (self.hostname, bug.id))
 
1197
 
 
1198
            if not force_fresh:
 
1199
                _uncommon_description_report = bug.id
 
1200
            return bug.id
 
1201
 
 
1202
        def test_1_download(self):
1185
1203
            '''download()'''
1186
1204
 
1187
 
            r = self.crashdb.download(segv_report)
 
1205
            r = self.crashdb.download(self.get_segv_report())
1188
1206
            self.assertEqual(r['ProblemType'], 'Crash')
1189
1207
            self.assertEqual(r['Title'], 'crash crashed with SIGSEGV in f()')
1190
1208
            self.assertEqual(r['DistroRelease'], self.ref_report['DistroRelease'])
1191
1209
            self.assertEqual(r['Architecture'], self.ref_report['Architecture'])
1192
1210
            self.assertEqual(r['Uname'], self.ref_report['Uname'])
1193
1211
            self.assertEqual(r.get('NonfreeKernelModules'),
1194
 
                self.ref_report.get('NonfreeKernelModules'))
 
1212
                             self.ref_report.get('NonfreeKernelModules'))
1195
1213
            self.assertEqual(r.get('UserGroups'), self.ref_report.get('UserGroups'))
1196
1214
            tags = set(r['Tags'].split())
1197
1215
            self.assertEqual(tags, set([self.crashdb.arch_tag, 'apport-crash',
1198
 
                apport.packaging.get_system_architecture()]))
 
1216
                                        apport.packaging.get_system_architecture()]))
1199
1217
 
1200
1218
            self.assertEqual(r['Signal'], '11')
1201
1219
            self.assertTrue(r['ExecutablePath'].endswith('/crash'))
1210
1228
            self.assertTrue('Registers' in r)
1211
1229
 
1212
1230
            # check tags
1213
 
            r = self.crashdb.download(python_report)
 
1231
            r = self.crashdb.download(self.get_python_report())
1214
1232
            tags = set(r['Tags'].split())
1215
1233
            self.assertEqual(tags, set(['apport-crash', 'boogus', 'pybogus',
1216
 
                'need-duplicate-check', apport.packaging.get_system_architecture()]))
 
1234
                                        'need-duplicate-check', apport.packaging.get_system_architecture()]))
1217
1235
 
1218
 
        def test_3_update_traces(self):
 
1236
        def test_2_update_traces(self):
1219
1237
            '''update_traces()'''
1220
1238
 
1221
 
            r = self.crashdb.download(segv_report)
 
1239
            r = self.crashdb.download(self.get_segv_report())
1222
1240
            self.assertTrue('CoreDump' in r)
1223
1241
            self.assertTrue('Dependencies' in r)
1224
1242
            self.assertTrue('Disassembly' in r)
1232
1250
            r['Stacktrace'] = 'long\ntrace'
1233
1251
            r['ThreadStacktrace'] = 'thread\neven longer\ntrace'
1234
1252
            r['FooBar'] = 'bogus'
1235
 
            self.crashdb.update_traces(segv_report, r, 'I can has a better retrace?')
1236
 
            r = self.crashdb.download(segv_report)
 
1253
            self.crashdb.update_traces(self.get_segv_report(), r, 'I can has a better retrace?')
 
1254
            r = self.crashdb.download(self.get_segv_report())
1237
1255
            self.assertTrue('CoreDump' in r)
1238
1256
            self.assertTrue('Dependencies' in r)
1239
1257
            self.assertTrue('Disassembly' in r)
1243
1261
            self.assertFalse('FooBar' in r)
1244
1262
            self.assertEqual(r['Title'], 'crash crashed with SIGSEGV in f()')
1245
1263
 
1246
 
            tags = self.crashdb.launchpad.bugs[segv_report].tags
 
1264
            tags = self.crashdb.launchpad.bugs[self.get_segv_report()].tags
1247
1265
            self.assertTrue('apport-crash' in tags)
1248
1266
            self.assertFalse('apport-collected' in tags)
1249
1267
 
1251
1269
            r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
1252
1270
            r['Stacktrace'] = 'long\ntrace'
1253
1271
            r['ThreadStacktrace'] = 'thread\neven longer\ntrace'
1254
 
            self.crashdb.update_traces(segv_report, r, 'good retrace!')
1255
 
            r = self.crashdb.download(segv_report)
 
1272
            self.crashdb.update_traces(self.get_segv_report(), r, 'good retrace!')
 
1273
            r = self.crashdb.download(self.get_segv_report())
1256
1274
            self.assertFalse('CoreDump' in r)
1257
1275
            self.assertTrue('Dependencies' in r)
1258
1276
            self.assertTrue('Disassembly' in r)
1266
1284
            self.assertEqual(r['Title'], 'crash crashed with SIGSEGV in read()')
1267
1285
 
1268
1286
            # respects title amendments
1269
 
            bug = self.crashdb.launchpad.bugs[segv_report]
 
1287
            bug = self.crashdb.launchpad.bugs[self.get_segv_report()]
1270
1288
            bug.title = 'crash crashed with SIGSEGV in f() on exit'
1271
1289
            try:
1272
1290
                bug.lp_save()
1273
1291
            except HTTPError:
1274
1292
                pass  # LP#336866 workaround
1275
1293
            r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
1276
 
            self.crashdb.update_traces(segv_report, r, 'good retrace with title amendment')
1277
 
            r = self.crashdb.download(segv_report)
 
1294
            self.crashdb.update_traces(self.get_segv_report(), r, 'good retrace with title amendment')
 
1295
            r = self.crashdb.download(self.get_segv_report())
1278
1296
            self.assertEqual(r['Title'], 'crash crashed with SIGSEGV in read() on exit')
1279
1297
 
1280
1298
            # does not destroy custom titles
1281
 
            bug = self.crashdb.launchpad.bugs[segv_report]
 
1299
            bug = self.crashdb.launchpad.bugs[self.get_segv_report()]
1282
1300
            bug.title = 'crash is crashy'
1283
1301
            try:
1284
1302
                bug.lp_save()
1286
1304
                pass  # LP#336866 workaround
1287
1305
 
1288
1306
            r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
1289
 
            self.crashdb.update_traces(segv_report, r, 'good retrace with custom title')
1290
 
            r = self.crashdb.download(segv_report)
 
1307
            self.crashdb.update_traces(self.get_segv_report(), r, 'good retrace with custom title')
 
1308
            r = self.crashdb.download(self.get_segv_report())
1291
1309
            self.assertEqual(r['Title'], 'crash is crashy')
1292
1310
 
1293
1311
            # test various situations which caused crashes
1294
1312
            r['Stacktrace'] = ''  # empty file
1295
1313
            r['ThreadStacktrace'] = '"]\xb6"\n'  # not interpretable as UTF-8, LP #353805
1296
1314
            r['StacktraceSource'] = 'a\nb\nc\nd\ne\n\xff\xff\xff\n\f'
1297
 
            self.crashdb.update_traces(segv_report, r, 'tests')
 
1315
            self.crashdb.update_traces(self.get_segv_report(), r, 'tests')
1298
1316
 
1299
1317
        def test_get_comment_url(self):
1300
1318
            '''get_comment_url() for non-ASCII titles'''
1332
1350
 
1333
1351
            r = apport.Report('Bug')
1334
1352
 
1335
 
            r['OneLiner'] = 'bogus→'
 
1353
            r['OneLiner'] = b'bogus\xe2\x86\x92'.decode('UTF-8')
1336
1354
            r['StacktraceTop'] = 'f()\ng()\nh(1)'
1337
1355
            r['ShortGoo'] = 'lineone\nlinetwo'
1338
1356
            r['DpkgTerminalLog'] = 'one\ntwo\nthree\nfour\nfive\nsix'
1339
 
            r['VarLogDistupgradeBinGoo'] = '\x01' * 1024
 
1357
            r['VarLogDistupgradeBinGoo'] = b'\x01' * 1024
1340
1358
 
1341
1359
            self.crashdb.update(id, r, 'NotMe', change_description=True)
1342
1360
 
1343
1361
            r = self.crashdb.download(id)
1344
1362
 
1345
 
            self.assertEqual(r['OneLiner'], 'bogus→')
 
1363
            self.assertEqual(r['OneLiner'], b'bogus\xe2\x86\x92'.decode('UTF-8'))
1346
1364
            self.assertEqual(r['ShortGoo'], 'lineone\nlinetwo')
1347
1365
            self.assertEqual(r['DpkgTerminalLog'], 'one\ntwo\nthree\nfour\nfive\nsix')
1348
 
            self.assertEqual(r['VarLogDistupgradeBinGoo'], '\x01' * 1024)
 
1366
            self.assertEqual(r['VarLogDistupgradeBinGoo'], b'\x01' * 1024)
1349
1367
 
1350
1368
            self.assertEqual(self.crashdb.launchpad.bugs[id].tags,
1351
 
                ['apport-collected'])
 
1369
                             ['apport-collected'])
1352
1370
 
1353
1371
        def test_update_comment(self):
1354
1372
            '''update() with appending comment'''
1383
1401
            self.assertEqual(r['VarLogDistupgradeBinGoo'], '\x01' * 1024)
1384
1402
 
1385
1403
            self.assertEqual(self.crashdb.launchpad.bugs[id].tags,
1386
 
                ['apport-collected'])
 
1404
                             ['apport-collected'])
1387
1405
 
1388
1406
        def test_update_filter(self):
1389
1407
            '''update() with a key filter'''
1406
1424
            r['VarLogDistupgradeBinGoo'] = '\x01' * 1024
1407
1425
 
1408
1426
            self.crashdb.update(id, r, 'NotMe', change_description=True,
1409
 
                    key_filter=['ProblemType', 'ShortGoo', 'DpkgTerminalLog'])
 
1427
                                key_filter=['ProblemType', 'ShortGoo', 'DpkgTerminalLog'])
1410
1428
 
1411
1429
            r = self.crashdb.download(id)
1412
1430
 
1421
1439
        def test_get_distro_release(self):
1422
1440
            '''get_distro_release()'''
1423
1441
 
1424
 
            self.assertEqual(self.crashdb.get_distro_release(segv_report),
1425
 
                    self.ref_report['DistroRelease'])
 
1442
            self.assertEqual(self.crashdb.get_distro_release(self.get_segv_report()),
 
1443
                             self.ref_report['DistroRelease'])
1426
1444
 
1427
1445
        def test_get_affected_packages(self):
1428
1446
            '''get_affected_packages()'''
1429
1447
 
1430
 
            self.assertEqual(self.crashdb.get_affected_packages(segv_report),
1431
 
                    [self.ref_report['SourcePackage']])
 
1448
            self.assertEqual(self.crashdb.get_affected_packages(self.get_segv_report()),
 
1449
                             [self.ref_report['SourcePackage']])
1432
1450
 
1433
1451
        def test_is_reporter(self):
1434
1452
            '''is_reporter()'''
1435
1453
 
1436
 
            self.assertTrue(self.crashdb.is_reporter(segv_report))
 
1454
            self.assertTrue(self.crashdb.is_reporter(self.get_segv_report()))
1437
1455
            self.assertFalse(self.crashdb.is_reporter(1))
1438
1456
 
1439
1457
        def test_can_update(self):
1440
1458
            '''can_update()'''
1441
1459
 
1442
 
            self.assertTrue(self.crashdb.can_update(segv_report))
 
1460
            self.assertTrue(self.crashdb.can_update(self.get_segv_report()))
1443
1461
            self.assertFalse(self.crashdb.can_update(1))
1444
1462
 
1445
1463
        def test_duplicates(self):
1446
1464
            '''duplicate handling'''
1447
1465
 
1448
1466
            # initially we have no dups
1449
 
            self.assertEqual(self.crashdb.duplicate_of(segv_report), None)
1450
 
            self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
 
1467
            self.assertEqual(self.crashdb.duplicate_of(self.get_segv_report()), None)
 
1468
            self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()), None)
 
1469
 
 
1470
            segv_id = self.get_segv_report()
 
1471
            known_test_id = self.get_uncommon_description_report()
 
1472
            known_test_id2 = self.get_uncommon_description_report(force_fresh=True)
1451
1473
 
1452
1474
            # dupe our segv_report and check that it worked; then undupe it
1453
 
            r = self.crashdb.download(segv_report)
1454
 
            self.crashdb.close_duplicate(r, segv_report, self.known_test_id)
1455
 
            self.assertEqual(self.crashdb.duplicate_of(segv_report), self.known_test_id)
 
1475
            r = self.crashdb.download(segv_id)
 
1476
            self.crashdb.close_duplicate(r, segv_id, known_test_id)
 
1477
            self.assertEqual(self.crashdb.duplicate_of(segv_id), known_test_id)
1456
1478
 
1457
1479
            # this should be a no-op
1458
 
            self.crashdb.close_duplicate(r, segv_report, self.known_test_id)
1459
 
            self.assertEqual(self.crashdb.duplicate_of(segv_report), self.known_test_id)
 
1480
            self.crashdb.close_duplicate(r, segv_id, known_test_id)
 
1481
            self.assertEqual(self.crashdb.duplicate_of(segv_id), known_test_id)
1460
1482
 
1461
 
            self.assertEqual(self.crashdb.get_fixed_version(segv_report), 'invalid')
1462
 
            self.crashdb.close_duplicate(r, segv_report, None)
1463
 
            self.assertEqual(self.crashdb.duplicate_of(segv_report), None)
1464
 
            self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
 
1483
            self.assertEqual(self.crashdb.get_fixed_version(segv_id), 'invalid')
 
1484
            self.crashdb.close_duplicate(r, segv_id, None)
 
1485
            self.assertEqual(self.crashdb.duplicate_of(segv_id), None)
 
1486
            self.assertEqual(self.crashdb.get_fixed_version(segv_id), None)
1465
1487
 
1466
1488
            # this should have removed attachments; note that Stacktrace is
1467
1489
            # short, and thus inline
1468
 
            r = self.crashdb.download(segv_report)
 
1490
            r = self.crashdb.download(self.get_segv_report())
1469
1491
            self.assertFalse('CoreDump' in r)
1470
1492
            self.assertFalse('Disassembly' in r)
1471
1493
            self.assertFalse('ProcMaps' in r)
1472
1494
            self.assertFalse('ProcStatus' in r)
1473
1495
            self.assertFalse('Registers' in r)
1474
 
            self.assertFalse('Stacktrace' in r)
1475
1496
            self.assertFalse('ThreadStacktrace' in r)
1476
1497
 
1477
1498
            # now try duplicating to a duplicate bug; this should automatically
1478
1499
            # transition to the master bug
1479
 
            self.crashdb.close_duplicate(apport.Report(), self.known_test_id,
1480
 
                    self.known_test_id2)
1481
 
            self.crashdb.close_duplicate(r, segv_report, self.known_test_id)
1482
 
            self.assertEqual(self.crashdb.duplicate_of(segv_report),
1483
 
                    self.known_test_id2)
 
1500
            self.crashdb.close_duplicate(apport.Report(), known_test_id,
 
1501
                                         known_test_id2)
 
1502
            self.crashdb.close_duplicate(r, segv_id, known_test_id)
 
1503
            self.assertEqual(self.crashdb.duplicate_of(segv_id),
 
1504
                             known_test_id2)
1484
1505
 
1485
 
            self.crashdb.close_duplicate(apport.Report(), self.known_test_id, None)
1486
 
            self.crashdb.close_duplicate(apport.Report(), self.known_test_id2, None)
1487
 
            self.crashdb.close_duplicate(r, segv_report, None)
 
1506
            self.crashdb.close_duplicate(apport.Report(), known_test_id, None)
 
1507
            self.crashdb.close_duplicate(apport.Report(), known_test_id2, None)
 
1508
            self.crashdb.close_duplicate(r, segv_id, None)
1488
1509
 
1489
1510
            # this should be a no-op
1490
 
            self.crashdb.close_duplicate(apport.Report(), self.known_test_id, None)
1491
 
            self.assertEqual(self.crashdb.duplicate_of(self.known_test_id), None)
 
1511
            self.crashdb.close_duplicate(apport.Report(), known_test_id, None)
 
1512
            self.assertEqual(self.crashdb.duplicate_of(known_test_id), None)
1492
1513
 
1493
 
            self.crashdb.mark_regression(segv_report, self.known_test_id)
1494
 
            self._verify_marked_regression(segv_report)
 
1514
            self.crashdb.mark_regression(segv_id, known_test_id)
 
1515
            self._verify_marked_regression(segv_id)
1495
1516
 
1496
1517
        def test_marking_segv(self):
1497
1518
            '''processing status markings for signal crashes'''
1498
1519
 
1499
1520
            # mark_retraced()
1500
1521
            unretraced_before = self.crashdb.get_unretraced()
1501
 
            self.assertTrue(segv_report in unretraced_before)
1502
 
            self.assertFalse(python_report in unretraced_before)
1503
 
            self.crashdb.mark_retraced(segv_report)
 
1522
            self.assertTrue(self.get_segv_report() in unretraced_before)
 
1523
            self.assertFalse(self.get_python_report() in unretraced_before)
 
1524
            self.crashdb.mark_retraced(self.get_segv_report())
1504
1525
            unretraced_after = self.crashdb.get_unretraced()
1505
 
            self.assertFalse(segv_report in unretraced_after)
 
1526
            self.assertFalse(self.get_segv_report() in unretraced_after)
1506
1527
            self.assertEqual(unretraced_before,
1507
 
                    unretraced_after.union(set([segv_report])))
1508
 
            self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
 
1528
                             unretraced_after.union(set([self.get_segv_report()])))
 
1529
            self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()), None)
1509
1530
 
1510
1531
            # mark_retrace_failed()
1511
 
            self._mark_needs_retrace(segv_report)
1512
 
            self.crashdb.mark_retraced(segv_report)
1513
 
            self.crashdb.mark_retrace_failed(segv_report)
 
1532
            self._mark_needs_retrace(self.get_segv_report())
 
1533
            self.crashdb.mark_retraced(self.get_segv_report())
 
1534
            self.crashdb.mark_retrace_failed(self.get_segv_report())
1514
1535
            unretraced_after = self.crashdb.get_unretraced()
1515
 
            self.assertFalse(segv_report in unretraced_after)
 
1536
            self.assertFalse(self.get_segv_report() in unretraced_after)
1516
1537
            self.assertEqual(unretraced_before,
1517
 
                    unretraced_after.union(set([segv_report])))
1518
 
            self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
 
1538
                             unretraced_after.union(set([self.get_segv_report()])))
 
1539
            self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()), None)
1519
1540
 
1520
1541
            # mark_retrace_failed() of invalid bug
1521
 
            self._mark_needs_retrace(segv_report)
1522
 
            self.crashdb.mark_retraced(segv_report)
1523
 
            self.crashdb.mark_retrace_failed(segv_report, "I don't like you")
 
1542
            self._mark_needs_retrace(self.get_segv_report())
 
1543
            self.crashdb.mark_retraced(self.get_segv_report())
 
1544
            self.crashdb.mark_retrace_failed(self.get_segv_report(), "I don't like you")
1524
1545
            unretraced_after = self.crashdb.get_unretraced()
1525
 
            self.assertFalse(segv_report in unretraced_after)
1526
 
            self.assertEqual(unretraced_before,
1527
 
                    unretraced_after.union(set([segv_report])))
1528
 
            self.assertEqual(self.crashdb.get_fixed_version(segv_report),
1529
 
                    'invalid')
 
1546
            self.assertFalse(self.get_segv_report() in unretraced_after)
 
1547
            self.assertEqual(unretraced_before,
 
1548
                             unretraced_after.union(set([self.get_segv_report()])))
 
1549
            self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()),
 
1550
                             'invalid')
 
1551
 
 
1552
        def test_marking_project(self):
 
1553
            '''processing status markings for a project CrashDB'''
 
1554
 
 
1555
            # create a distro bug
 
1556
            distro_bug = self.crashdb.launchpad.bugs.createBug(
 
1557
                description='foo',
 
1558
                tags=self.crashdb.arch_tag,
 
1559
                target=self.crashdb.lp_distro,
 
1560
                title='ubuntu distro retrace bug')
 
1561
            #print('distro bug: https://staging.launchpad.net/bugs/%i' % distro_bug.id)
 
1562
 
 
1563
            # create a project crash DB and a bug
 
1564
            launchpad_instance = os.environ.get('APPORT_LAUNCHPAD_INSTANCE') or 'staging'
 
1565
 
 
1566
            project_db = CrashDatabase(
 
1567
                os.environ.get('LP_CREDENTIALS'),
 
1568
                {'project': 'langpack-o-matic', 'launchpad_instance': launchpad_instance})
 
1569
            project_bug = project_db.launchpad.bugs.createBug(
 
1570
                description='bar',
 
1571
                tags=project_db.arch_tag,
 
1572
                target=project_db.lp_distro,
 
1573
                title='project retrace bug')
 
1574
            #print('project bug: https://staging.launchpad.net/bugs/%i' % project_bug.id)
 
1575
 
 
1576
            # on project_db, we recognize the project bug and can mark it
 
1577
            unretraced_before = project_db.get_unretraced()
 
1578
            self.assertTrue(project_bug.id in unretraced_before)
 
1579
            self.assertFalse(distro_bug.id in unretraced_before)
 
1580
            project_db.mark_retraced(project_bug.id)
 
1581
            unretraced_after = project_db.get_unretraced()
 
1582
            self.assertFalse(project_bug.id in unretraced_after)
 
1583
            self.assertEqual(unretraced_before,
 
1584
                             unretraced_after.union(set([project_bug.id])))
 
1585
            self.assertEqual(self.crashdb.get_fixed_version(project_bug.id), None)
1530
1586
 
1531
1587
        def test_marking_python(self):
1532
1588
            '''processing status markings for interpreter crashes'''
1533
1589
 
1534
1590
            unchecked_before = self.crashdb.get_dup_unchecked()
1535
 
            self.assertTrue(python_report in unchecked_before)
1536
 
            self.assertFalse(segv_report in unchecked_before)
1537
 
            self.crashdb._mark_dup_checked(python_report, self.ref_report)
 
1591
            self.assertTrue(self.get_python_report() in unchecked_before)
 
1592
            self.assertFalse(self.get_segv_report() in unchecked_before)
 
1593
            self.crashdb._mark_dup_checked(self.get_python_report(), self.ref_report)
1538
1594
            unchecked_after = self.crashdb.get_dup_unchecked()
1539
 
            self.assertFalse(python_report in unchecked_after)
 
1595
            self.assertFalse(self.get_python_report() in unchecked_after)
1540
1596
            self.assertEqual(unchecked_before,
1541
 
                    unchecked_after.union(set([python_report])))
1542
 
            self.assertEqual(self.crashdb.get_fixed_version(python_report),
1543
 
                    None)
 
1597
                             unchecked_after.union(set([self.get_python_report()])))
 
1598
            self.assertEqual(self.crashdb.get_fixed_version(self.get_python_report()), None)
1544
1599
 
1545
1600
        def test_update_traces_invalid(self):
1546
1601
            '''updating an invalid crash
1548
1603
            This simulates a race condition where a crash being processed gets
1549
1604
            invalidated by marking it as a duplicate.
1550
1605
            '''
1551
 
            id = self._file_segv_report()[0]
1552
 
            sys.stderr.write('(https://%s/bugs/%i) ' % (self.hostname, id))
 
1606
            id = self.get_segv_report(force_fresh=True)
1553
1607
 
1554
1608
            r = self.crashdb.download(id)
1555
1609
 
1556
 
            self.crashdb.close_duplicate(r, id, segv_report)
 
1610
            self.crashdb.close_duplicate(r, id, self.get_segv_report())
1557
1611
 
1558
1612
            # updating with a useful stack trace removes core dump
1559
1613
            r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
1564
1618
            r = self.crashdb.download(id)
1565
1619
            self.assertFalse('CoreDump' in r)
1566
1620
 
1567
 
        def test_get_fixed_version(self):
 
1621
        @mock.patch.object(CrashDatabase, '_get_source_version')
 
1622
        def test_get_fixed_version(self, *args):
1568
1623
            '''get_fixed_version() for fixed bugs
1569
1624
 
1570
1625
            Other cases are already checked in test_marking_segv() (invalid
1571
1626
            bugs) and test_duplicates (duplicate bugs) for efficiency.
1572
1627
            '''
1573
 
            self._mark_report_fixed(segv_report)
1574
 
            fixed_ver = self.crashdb.get_fixed_version(segv_report)
1575
 
            self.assertNotEqual(fixed_ver, None)
1576
 
            self.assertTrue(fixed_ver[0].isdigit())
1577
 
            self._mark_report_new(segv_report)
1578
 
            self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
 
1628
            # staging.launchpad.net often does not have Quantal, so mock-patch
 
1629
            # it to a known value
 
1630
            CrashDatabase._get_source_version.return_value = '3.14'
 
1631
            self._mark_report_fixed(self.get_segv_report())
 
1632
            fixed_ver = self.crashdb.get_fixed_version(self.get_segv_report())
 
1633
            self.assertEqual(fixed_ver, '3.14')
 
1634
            self._mark_report_new(self.get_segv_report())
 
1635
            self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()), None)
1579
1636
 
1580
1637
        #
1581
1638
        # Launchpad specific implementation and tests
1588
1645
            launchpad_instance = os.environ.get('APPORT_LAUNCHPAD_INSTANCE') or 'staging'
1589
1646
 
1590
1647
            return CrashDatabase(os.environ.get('LP_CREDENTIALS'),
1591
 
                    {'distro': 'ubuntu',
1592
 
                     'launchpad_instance': launchpad_instance})
 
1648
                                 {'distro': 'ubuntu',
 
1649
                                  'launchpad_instance': launchpad_instance})
1593
1650
 
1594
1651
        def _get_bug_target(self, db, report):
1595
1652
            '''Return the bug_target for this report.'''
1602
1659
            else:
1603
1660
                return self.lp_distro
1604
1661
 
1605
 
        def _get_librarian_hostname(self):
1606
 
            '''Return the librarian hostname according to the LP hostname used.'''
1607
 
 
1608
 
            hostname = self.crashdb.get_hostname()
1609
 
            if 'staging' in hostname:
1610
 
                return 'staging.launchpadlibrarian.net'
1611
 
            else:
1612
 
                return 'launchpad.dev:58080'
1613
 
 
1614
 
        def _file_bug(self, bug_target, report, handle, comment=None):
1615
 
            '''File a bug report.'''
1616
 
 
1617
 
            bug_title = report.get('Title', report.standard_title())
1618
 
 
1619
 
            blob_info = self.crashdb.launchpad.temporary_blobs.fetch(
1620
 
                token=handle)
1621
 
            # XXX 2010-08-03 matsubara bug=612990:
1622
 
            #     Can't fetch the blob directly, so let's load it from the
1623
 
            #     representation.
1624
 
            blob = self.crashdb.launchpad.load(blob_info['self_link'])
1625
 
            #XXX Need to find a way to trigger the job that process the blob
1626
 
            # rather polling like this. This makes the test suite take forever
1627
 
            # to run.
1628
 
            while not blob.hasBeenProcessed():
1629
 
                time.sleep(1)
1630
 
 
1631
 
            # processed_blob contains info about privacy, additional comments
1632
 
            # and attachments.
1633
 
            processed_blob = blob.getProcessedData()
1634
 
 
 
1662
        def _file_bug(self, bug_target, report, description=None):
 
1663
            '''File a bug report for a report.
 
1664
 
 
1665
            Return the bug ID.
 
1666
            '''
 
1667
            # unfortunately staging's +storeblob API hardly ever works, so we
 
1668
            # must avoid using it. Fake it by manually doing the comments and
 
1669
            # attachments that +filebug would ordinarily do itself when given a
 
1670
            # blob handle.
 
1671
 
 
1672
            if description is None:
 
1673
                description = 'some description'
 
1674
 
 
1675
            mime = self.crashdb._generate_upload_blob(report)
 
1676
            msg = email.message_from_file(mime)
 
1677
            mime.close()
 
1678
            msg_iter = msg.walk()
 
1679
 
 
1680
            # first one is the multipart container
 
1681
            header = msg_iter.next()
 
1682
            assert header.is_multipart()
 
1683
 
 
1684
            # second part should be an inline text/plain attachments with all short
 
1685
            # fields
 
1686
            part = msg_iter.next()
 
1687
            assert not part.is_multipart()
 
1688
            assert part.get_content_type() == 'text/plain'
 
1689
            description += '\n\n' + part.get_payload(decode=True).decode('UTF-8', 'replace')
 
1690
 
 
1691
            # create the bug from header and description data
1635
1692
            bug = self.crashdb.launchpad.bugs.createBug(
1636
 
                description=processed_blob['extra_description'],
1637
 
                private=processed_blob['private'],
1638
 
                tags=processed_blob['initial_tags'],
 
1693
                description=description,
 
1694
                private=(header['Private'] == 'yes'),
 
1695
                tags=header['Tags'].split(),
1639
1696
                target=bug_target,
1640
 
                title=bug_title)
1641
 
 
1642
 
            for comment in processed_blob['comments']:
1643
 
                bug.newMessage(content=comment)
1644
 
 
1645
 
            # Ideally, one would be able to retrieve the attachment content
1646
 
            # from the ProblemReport object or from the processed_blob.
1647
 
            # Unfortunately the processed_blob only give us the Launchpad
1648
 
            # librarian file_alias_id, so that's why we need to
1649
 
            # download it again and upload to the bug report. It'd be even
1650
 
            # better if addAttachment could work like linkAttachment, the LP
1651
 
            # api used in the +filebug web UI, but there are security concerns
1652
 
            # about the way linkAttachment works.
1653
 
            librarian_url = 'http://%s' % self._get_librarian_hostname()
1654
 
            for attachment in processed_blob['attachments']:
1655
 
                filename = description = attachment['description']
1656
 
                # Download the attachment data.
1657
 
                data = urlopen(urllib.basejoin(librarian_url,
1658
 
                    str(attachment['file_alias_id']) + '/' + filename)).read()
1659
 
                # Add the attachment to the newly created bug report.
1660
 
                bug.addAttachment(
1661
 
                    comment=filename,
1662
 
                    data=data,
1663
 
                    filename=filename,
1664
 
                    description=description)
1665
 
 
1666
 
            for subscriber in processed_blob['subscribers']:
 
1697
                title=report.get('Title', report.standard_title()))
 
1698
 
 
1699
            # nwo add the attachments
 
1700
            for part in msg_iter:
 
1701
                assert not part.is_multipart()
 
1702
                bug.addAttachment(comment='',
 
1703
                                  description=part.get_filename(),
 
1704
                                  content_type=None,
 
1705
                                  data=part.get_payload(decode=True),
 
1706
                                  filename=part.get_filename(), is_patch=False)
 
1707
 
 
1708
            for subscriber in header['Subscribers'].split():
1667
1709
                sub = self.crashdb.launchpad.people[subscriber]
1668
1710
                if sub:
1669
1711
                    bug.subscribe(person=sub)
1670
1712
 
1671
 
            for submission_key in processed_blob['hwdb_submission_keys']:
1672
 
                # XXX 2010-08-04 matsubara bug=628889:
1673
 
                #     Can't fetch the submission directly, so let's load it
1674
 
                #     from the representation.
1675
 
                submission = self.crashdb.launchpad.load(
1676
 
                    'https://api.%s/beta/+hwdb/+submission/%s'
1677
 
                    % (self.crashdb.get_hostname(), submission_key))
1678
 
                bug.linkHWSubmission(submission=submission)
1679
 
            return int(bug.id)
 
1713
            return bug.id
1680
1714
 
1681
1715
        def _mark_needs_retrace(self, id):
1682
1716
            '''Mark a report ID as needing retrace.'''
1727
1761
            # crash database for langpack-o-matic project (this does not have
1728
1762
            # packages in any distro)
1729
1763
            crashdb = CrashDatabase(os.environ.get('LP_CREDENTIALS'),
1730
 
                {'project': 'langpack-o-matic',
1731
 
                 'launchpad_instance': launchpad_instance})
 
1764
                                    {'project': 'langpack-o-matic',
 
1765
                                     'launchpad_instance': launchpad_instance})
1732
1766
            self.assertEqual(crashdb.distro, None)
1733
1767
 
1734
1768
            # create Python crash report
1741
1775
            r.add_os_info()
1742
1776
            r.add_user_info()
1743
1777
            self.assertEqual(r.standard_title(),
1744
 
                    "foo crashed with NameError in fuzz(): global name 'weird' is not defined")
 
1778
                             "foo crashed with NameError in fuzz(): global name 'weird' is not defined")
1745
1779
 
1746
1780
            # file it
1747
 
            handle = crashdb.upload(r)
1748
 
            self.assertTrue(handle)
1749
1781
            bug_target = self._get_bug_target(crashdb, r)
1750
1782
            self.assertEqual(bug_target.name, 'langpack-o-matic')
1751
1783
 
1752
 
            id = self._file_bug(bug_target, r, handle)
 
1784
            id = self._file_bug(bug_target, r)
1753
1785
            self.assertTrue(id > 0)
1754
1786
            sys.stderr.write('(https://%s/bugs/%i) ' % (self.hostname, id))
1755
1787
 
1763
1795
 
1764
1796
            # test fixed version
1765
1797
            self.assertEqual(crashdb.get_fixed_version(id), None)
1766
 
            crashdb.close_duplicate(r, id, self.known_test_id)
1767
 
            self.assertEqual(crashdb.duplicate_of(id), self.known_test_id)
 
1798
            crashdb.close_duplicate(r, id, self.get_uncommon_description_report())
 
1799
            self.assertEqual(crashdb.duplicate_of(id), self.get_uncommon_description_report())
1768
1800
            self.assertEqual(crashdb.get_fixed_version(id), 'invalid')
1769
1801
            crashdb.close_duplicate(r, id, None)
1770
1802
            self.assertEqual(crashdb.duplicate_of(id), None)
1774
1806
            '''download() of uncommon description formats'''
1775
1807
 
1776
1808
            # only ProblemType/Architecture/DistroRelease in description
1777
 
            r = self.crashdb.download(self.uncommon_description_bug.id)
 
1809
            r = self.crashdb.download(self.get_uncommon_description_report())
1778
1810
            self.assertEqual(r['ProblemType'], 'Package')
1779
1811
            self.assertEqual(r['Architecture'], 'amd64')
1780
1812
            self.assertTrue(r['DistroRelease'].startswith('Ubuntu '))
1782
1814
        def test_escalation(self):
1783
1815
            '''Escalating bugs with more than 10 duplicates'''
1784
1816
 
1785
 
            assert segv_report, 'you need to run test_1_report_segv() first'
1786
 
 
1787
1817
            launchpad_instance = os.environ.get('APPORT_LAUNCHPAD_INSTANCE') or 'staging'
1788
1818
            db = CrashDatabase(os.environ.get('LP_CREDENTIALS'),
1789
 
                    {'distro': 'ubuntu',
1790
 
                     'launchpad_instance': launchpad_instance,
1791
 
                     'escalation_tag': 'omgkittens',
1792
 
                     'escalation_subscription': 'apport-hackers'})
 
1819
                               {'distro': 'ubuntu',
 
1820
                                'launchpad_instance': launchpad_instance,
 
1821
                                'escalation_tag': 'omgkittens',
 
1822
                                'escalation_subscription': 'apport-hackers'})
1793
1823
 
1794
1824
            count = 0
1795
1825
            p = db.launchpad.people[db.options['escalation_subscription']].self_link
1798
1828
                for b in range(first_dup, first_dup + 13):
1799
1829
                    count += 1
1800
1830
                    sys.stderr.write('%i ' % b)
1801
 
                    db.close_duplicate(apport.Report(), b, segv_report)
1802
 
                    b = db.launchpad.bugs[segv_report]
 
1831
                    db.close_duplicate(apport.Report(), b, self.get_segv_report())
 
1832
                    b = db.launchpad.bugs[self.get_segv_report()]
1803
1833
                    has_escalation_tag = db.options['escalation_tag'] in b.tags
1804
1834
                    has_escalation_subscription = any([s.person_link == p for s in b.subscriptions])
1805
1835
                    if count <= 10:
1817
1847
        def test_marking_python_task_mangle(self):
1818
1848
            '''source package task fixup for marking interpreter crashes'''
1819
1849
 
1820
 
            self._mark_needs_dupcheck(python_report)
 
1850
            self._mark_needs_dupcheck(self.get_python_report())
1821
1851
            unchecked_before = self.crashdb.get_dup_unchecked()
1822
 
            self.assertTrue(python_report in unchecked_before)
 
1852
            self.assertTrue(self.get_python_report() in unchecked_before)
1823
1853
 
1824
1854
            # add an upstream task, and remove the package name from the
1825
1855
            # package task; _mark_dup_checked is supposed to restore the
1826
1856
            # package name
1827
 
            b = self.crashdb.launchpad.bugs[python_report]
 
1857
            b = self.crashdb.launchpad.bugs[self.get_python_report()]
1828
1858
            if b.private:
1829
1859
                b.private = False
1830
1860
                b.lp_save()
1833
1863
            t.lp_save()
1834
1864
            b.addTask(target=self.crashdb.launchpad.projects['coreutils'])
1835
1865
 
1836
 
            self.crashdb._mark_dup_checked(python_report, self.ref_report)
 
1866
            self.crashdb._mark_dup_checked(self.get_python_report(), self.ref_report)
1837
1867
 
1838
1868
            unchecked_after = self.crashdb.get_dup_unchecked()
1839
 
            self.assertFalse(python_report in unchecked_after)
 
1869
            self.assertFalse(self.get_python_report() in unchecked_after)
1840
1870
            self.assertEqual(unchecked_before,
1841
 
                    unchecked_after.union(set([python_report])))
 
1871
                             unchecked_after.union(set([self.get_python_report()])))
1842
1872
 
1843
1873
            # upstream task should be unmodified
1844
 
            b = self.crashdb.launchpad.bugs[python_report]
 
1874
            b = self.crashdb.launchpad.bugs[self.get_python_report()]
1845
1875
            self.assertEqual(b.bug_tasks[0].bug_target_name, 'coreutils')
1846
1876
            self.assertEqual(b.bug_tasks[0].status, 'New')
1847
1877
 
1850
1880
            self.assertEqual(b.bug_tasks[1].status, 'New')
1851
1881
 
1852
1882
            # should not confuse get_fixed_version()
1853
 
            self.assertEqual(self.crashdb.get_fixed_version(python_report),
1854
 
                    None)
 
1883
            self.assertEqual(self.crashdb.get_fixed_version(self.get_python_report()), None)
1855
1884
 
1856
1885
        @classmethod
1857
1886
        def _generate_sigsegv_report(klass, signal='11'):
1883
1912
 
1884
1913
                # call it through gdb and dump core
1885
1914
                subprocess.call(['gdb', '--batch', '--ex', 'run', '--ex',
1886
 
                    'generate-core-file core', './crash'], stdout=subprocess.PIPE)
 
1915
                                 'generate-core-file core', './crash'], stdout=subprocess.PIPE)
1887
1916
                assert os.path.exists('core')
 
1917
                subprocess.check_call(['sync'])
1888
1918
                assert subprocess.call(['readelf', '-n', 'core'],
1889
 
                    stdout=subprocess.PIPE) == 0
 
1919
                                       stdout=subprocess.PIPE) == 0
1890
1920
 
1891
1921
                pr['ExecutablePath'] = os.path.join(workdir, 'crash')
1892
1922
                pr['CoreDump'] = (os.path.join(workdir, 'core'),)