home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / apport / crashdb_impl / launchpad.py next >
Encoding:
Python Source  |  2009-04-06  |  33.7 KB  |  859 lines

  1. '''Crash database implementation for Launchpad.
  2.  
  3. Copyright (C) 2007 Canonical Ltd.
  4. Author: Martin Pitt <martin.pitt@ubuntu.com>
  5.  
  6. This program is free software; you can redistribute it and/or modify it
  7. under the terms of the GNU General Public License as published by the
  8. Free Software Foundation; either version 2 of the License, or (at your
  9. option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
  10. the full text of the license.
  11. '''
  12.  
  13. import urllib, tempfile, shutil, os.path, re, gzip, sys
  14. from cStringIO import StringIO
  15.  
  16. import launchpadbugs.storeblob
  17. import launchpadbugs.connector as Connector
  18.  
  19. import apport.crashdb
  20. import apport
  21.  
  22. Bug = Connector.ConnectBug()
  23. BugList = Connector.ConnectBugList()
  24.  
  25. def get_source_version(distro, package, hostname):
  26.     '''Return the version of given source package in the latest release of
  27.     given distribution.
  28.  
  29.     If 'distro' is None, we will look for a launchpad project . 
  30.     '''
  31.  
  32.     if distro:
  33.         result = urllib.urlopen('https://%s/%s/+source/%s' % (hostname, distro, package)).read()
  34.         m = re.search('href="/%s/\w+/\+source/%s/([^"]+)"' % (distro, re.escape(package)), result)
  35.         if not m:
  36.             raise ValueError, 'source package %s does not exist in %s' % (package, distro)
  37.     else:
  38.         # non distro packages
  39.         result = urllib.urlopen('https://%s/%s/+series' % (hostname, package)).read()
  40.         m = re.search('href="/%s/([^"]+)"' % (re.escape(package)), result)
  41.         if not m:
  42.             raise ValueError, 'Series for %s does not exist in Launchpad' % (package)
  43.         
  44.     return m.group(1)
  45.  
  46. class CrashDatabase(apport.crashdb.CrashDatabase):
  47.     '''Launchpad implementation of crash database interface.'''
  48.  
  49.     def __init__(self, cookie_file, bugpattern_baseurl, options):
  50.         '''Initialize Launchpad crash database connection. 
  51.         
  52.         You need to specify a Mozilla-style cookie file for download() and
  53.         update(). For upload() and get_comment_url() you can use None.'''
  54.  
  55.         apport.crashdb.CrashDatabase.__init__(self, cookie_file,
  56.             bugpattern_baseurl, options)
  57.  
  58.         self.distro = options.get('distro')
  59.         self.arch_tag = 'need-%s-retrace' % apport.packaging.get_system_architecture()
  60.         self.options = options
  61.         self.cookie_file = cookie_file
  62.  
  63.         if self.options.get('staging', False):
  64.             from launchpadbugs.lpconstants import HTTPCONNECTION
  65.             Bug.set_connection_mode(HTTPCONNECTION.MODE.STAGING)
  66.             BugList.set_connection_mode(HTTPCONNECTION.MODE.STAGING)
  67.             self.hostname = 'staging.launchpad.net'
  68.         else:
  69.             self.hostname = 'launchpad.net'
  70.  
  71.         if self.cookie_file:
  72.             Bug.authentication = self.cookie_file
  73.             BugList.authentication = self.cookie_file
  74.  
  75.     def upload(self, report, progress_callback = None):
  76.         '''Upload given problem report return a handle for it. 
  77.         
  78.         This should happen noninteractively. 
  79.         
  80.         If the implementation supports it, and a function progress_callback is
  81.         passed, that is called repeatedly with two arguments: the number of
  82.         bytes already sent, and the total number of bytes to send. This can be
  83.         used to provide a proper upload progress indication on frontends.'''
  84.  
  85.         # set reprocessing tags
  86.         hdr = {}
  87.         hdr['Tags'] = 'apport-%s' % report['ProblemType'].lower()
  88.         # append tags defined in the report
  89.         if report.has_key('Tags'):
  90.             hdr['Tags'] += ' ' + report['Tags']
  91.         a = report.get('PackageArchitecture')
  92.         if not a or a == 'all':
  93.             a = report.get('Architecture')
  94.         if a:
  95.             hdr['Tags'] += ' ' + a
  96.         if 'CoreDump' in report and a:
  97.             hdr['Tags'] += ' need-%s-retrace' % a
  98.             # FIXME: ugly Ubuntu specific hack until LP has a real crash db
  99.             if report['DistroRelease'].split()[0] == 'Ubuntu':
  100.                 hdr['Private'] = 'yes'
  101.                 hdr['Subscribers'] = 'apport'
  102.         # set dup checking tag for Python crashes
  103.         elif report.has_key('Traceback'):
  104.             hdr['Tags'] += ' need-duplicate-check'
  105.             # FIXME: ugly Ubuntu specific hack until LP has a real crash db
  106.             if report['DistroRelease'].split()[0] == 'Ubuntu':
  107.                 hdr['Private'] = 'yes'
  108.                 hdr['Subscribers'] = 'apport'
  109.  
  110.         # write MIME/Multipart version into temporary file
  111.         mime = tempfile.TemporaryFile()
  112.         report.write_mime(mime, extra_headers=hdr, skip_keys=['Date'])
  113.         mime.flush()
  114.         mime.seek(0)
  115.  
  116.         ticket = launchpadbugs.storeblob.upload(mime, progress_callback, 
  117.                 staging=self.options.get('staging', False))
  118.         assert ticket
  119.         return ticket
  120.  
  121.     def get_comment_url(self, report, handle):
  122.         '''Return an URL that should be opened after report has been uploaded
  123.         and upload() returned handle.
  124.  
  125.         Should return None if no URL should be opened (anonymous filing without
  126.         user comments); in that case this function should do whichever
  127.         interactive steps it wants to perform.'''
  128.  
  129.         args = {}
  130.         title = report.standard_title()
  131.         if title:
  132.             args['field.title'] = title
  133.  
  134.         project = self.options.get('project')
  135.         if 'ThirdParty' in report:
  136.             project = report['SourcePackage']
  137.         
  138.         if not project:
  139.             if report.has_key('SourcePackage'):
  140.                 return 'https://bugs.%s/%s/+source/%s/+filebug/%s?%s' % (
  141.                     self.hostname, self.distro, report['SourcePackage'], handle, urllib.urlencode(args))
  142.             else:
  143.                 return 'https://bugs.%s/%s/+filebug/%s?%s' % (
  144.                     self.hostname, self.distro, handle, urllib.urlencode(args))
  145.         else:
  146.             return 'https://bugs.%s/%s/+filebug/%s?%s' % (
  147.                 self.hostname, project, handle, urllib.urlencode(args))
  148.  
  149.     def download(self, id):
  150.         '''Download the problem report from given ID and return a Report.'''
  151.  
  152.         report = apport.Report()
  153.         attachment_path = tempfile.mkdtemp()
  154.         Bug.content_types.append('application/x-gzip')
  155.         try:
  156.             b = Bug(id) 
  157.  
  158.             # parse out fields from summary
  159.             m = re.search(r"(ProblemType:.*)$", b.description_raw, re.S)
  160.             if not m:
  161.                 m = re.search(r"^--- \r?$[\r\n]*(.*)", b.description_raw, re.M | re.S)
  162.             assert m, 'bug description must contain standard apport format data'
  163.  
  164.             description = m.group(1).replace('\xc2\xa0', ' ')
  165.  
  166.             if '\r\n\r\n' in description:
  167.                 # this often happens, remove all empty lines between top and
  168.                 # "Uname"
  169.                 if 'Uname:' in description:
  170.                     # this will take care of bugs like LP #315728 where stuff
  171.                     # is added after the apport data
  172.                     (part1, part2) = description.split('Uname:', 1)
  173.                     description = part1.replace('\r\n\r\n', '\r\n') + 'Uname:' \
  174.                         + part2.split('\r\n\r\n', 1)[0]
  175.                 else:
  176.                     description = description.replace('\r\n\r\n', '\r\n')
  177.  
  178.             report.load(StringIO(description))
  179.  
  180.             report['Date'] = b.date.ctime()
  181.             if 'ProblemType' not in report:
  182.                 if 'apport-bug' in b.tags:
  183.                     report['ProblemType'] = 'Bug'
  184.                 elif 'apport-crash' in b.tags:
  185.                     report['ProblemType'] = 'Crash'
  186.                 elif 'apport-kernelcrash' in b.tags:
  187.                     report['ProblemType'] = 'KernelCrash'
  188.                 elif 'apport-package' in b.tags:
  189.                     report['ProblemType'] = 'Package'
  190.                 else:
  191.                     raise ValueError, 'cannot determine ProblemType from tags: ' + str(b.tags)
  192.  
  193.             for att in b.attachments.filter(lambda a: re.match(
  194.                     'Dependencies.txt|CoreDump.gz|ProcMaps.txt|Traceback.txt|DpkgTerminalLog.txt',
  195.                     a.lp_filename)):
  196.  
  197.                 key = os.path.splitext(att.lp_filename)[0]
  198.                 
  199.                 att.download(os.path.join(attachment_path, att.lp_filename))
  200.                 if att.lp_filename.endswith('.txt'):
  201.                     report[key] = att.text
  202.                 elif att.lp_filename.endswith('.gz'):
  203.                     report[key] = gzip.GzipFile(fileobj=StringIO(att.text)).read()#TODO: is this the best solution?
  204.                 else:
  205.                     raise Exception, 'Unknown attachment type: ' + att.lp_filename
  206.  
  207.             return report
  208.         finally:
  209.             shutil.rmtree(attachment_path)
  210.  
  211.     def update(self, id, report, comment = ''):
  212.         '''Update the given report ID with the retraced results from the report
  213.         (Stacktrace, ThreadStacktrace, StacktraceTop; also Disassembly if
  214.         desired) and an optional comment.'''
  215.  
  216.         bug = Bug(id)
  217.  
  218.         comment += '\n\nStacktraceTop:' + report['StacktraceTop'].decode('utf-8',
  219.             'replace').encode('utf-8')
  220.  
  221.         # we need properly named files here, otherwise they will be displayed
  222.         # as '<fdopen>'
  223.         tmpdir = tempfile.mkdtemp()
  224.         t = {}
  225.         try:
  226.             t[0] = open(os.path.join(tmpdir, 'Stacktrace.txt'), 'w+')
  227.             t[0].write(report['Stacktrace'])
  228.             t[0].flush()
  229.             t[0].seek(0)
  230.             att = Bug.NewAttachment(localfileobject=t[0],
  231.                     description='Stacktrace.txt (retraced)')
  232.             new_comment = Bug.NewComment(subject='Symbolic stack trace',
  233.                     text=comment, attachment=att)
  234.             bug.comments.add(new_comment)
  235.  
  236.             t[1] = open(os.path.join(tmpdir, 'ThreadStacktrace.txt'), 'w+')
  237.             t[1].write(report['ThreadStacktrace'])
  238.             t[1].flush()
  239.             t[1].seek(0)
  240.             att = Bug.NewAttachment(localfileobject=t[1],
  241.                     description='ThreadStacktrace.txt (retraced)')
  242.             new_comment = Bug.NewComment(subject='Symbolic threaded stack trace',
  243.                     attachment=att)
  244.             bug.comments.add(new_comment)
  245.  
  246.             if report.has_key('StacktraceSource'):
  247.                 t[2] = open(os.path.join(tmpdir, 'StacktraceSource.txt'), 'w+')
  248.                 t[2].write(report['StacktraceSource'])
  249.                 t[2].flush()
  250.                 t[2].seek(0)
  251.                 att = Bug.NewAttachment(localfileobject=t[2],
  252.                         description='StacktraceSource.txt')
  253.                 new_comment = Bug.NewComment(subject='Stack trace with source code',
  254.                         attachment=att)
  255.                 bug.comments.add(new_comment)
  256.  
  257.             if report.has_key('SourcePackage') and bug.sourcepackage == 'ubuntu':
  258.                 bug.set_sourcepackage(report['SourcePackage'])
  259.         finally:
  260.             shutil.rmtree(tmpdir)
  261.  
  262.         # remove core dump if stack trace is usable
  263.         if report.has_useful_stacktrace():
  264.             bug.attachments.remove(
  265.                     func=lambda a: re.match('^CoreDump.gz$', a.lp_filename or a.description))
  266.             bug.commit()
  267.             try:
  268.                 bug.importance='Medium'
  269.                 bug.commit()
  270.             except IOError:
  271.                 # bug was marked as a duplicate underneath us; LP#349407
  272.                 pass
  273.         else:
  274.             bug.commit()
  275.         for x in t.itervalues():
  276.             x.close()
  277.         self._subscribe_triaging_team(bug, report)
  278.  
  279.     def get_distro_release(self, id):
  280.         '''Get 'DistroRelease: <release>' from the given report ID and return
  281.         it.'''
  282.         #using py-lp-bugs
  283.         bug = Bug(url='https://%s/bugs/%s' % (self.hostname, str(id)))
  284.         m = re.search('DistroRelease: ([-a-zA-Z0-9.+/ ]+)', bug.description)
  285.         if m:
  286.             return m.group(1)
  287.         raise ValueError, 'URL does not contain DistroRelease: field'
  288.  
  289.     def get_unretraced(self):
  290.         '''Return an ID set of all crashes which have not been retraced yet and
  291.         which happened on the current host architecture.'''
  292.  
  293.         bugs = BugList('https://bugs.%s/ubuntu/+bugs?field.tag=%s' % (self.hostname, self.arch_tag))
  294.         return set(int(i) for i in bugs)
  295.  
  296.     def get_dup_unchecked(self):
  297.         '''Return an ID set of all crashes which have not been checked for
  298.         being a duplicate.
  299.  
  300.         This is mainly useful for crashes of scripting languages such as
  301.         Python, since they do not need to be retraced. It should not return
  302.         bugs that are covered by get_unretraced().'''
  303.  
  304.         bugs = BugList('https://bugs.%s/ubuntu/+bugs?field.tag=need-duplicate-check&batch=300' % self.hostname)
  305.         return set(int(i) for i in bugs)
  306.  
  307.     def get_unfixed(self):
  308.         '''Return an ID set of all crashes which are not yet fixed.
  309.  
  310.         The list must not contain bugs which were rejected or duplicate.
  311.         
  312.         This function should make sure that the returned list is correct. If
  313.         there are any errors with connecting to the crash database, it should
  314.         raise an exception (preferably IOError).'''
  315.  
  316.         bugs = BugList('https://bugs.%s/ubuntu/+bugs?field.tag=apport-crash&batch=300' % self.hostname)
  317.         return set(int(i) for i in bugs)
  318.  
  319.     def get_fixed_version(self, id):
  320.         '''Return the package version that fixes a given crash.
  321.  
  322.         Return None if the crash is not yet fixed, or an empty string if the
  323.         crash is fixed, but it cannot be determined by which version. Return
  324.         'invalid' if the crash report got invalidated, such as closed a
  325.         duplicate or rejected.
  326.  
  327.         This function should make sure that the returned result is correct. If
  328.         there are any errors with connecting to the crash database, it should
  329.         raise an exception (preferably IOError).'''
  330.  
  331.         # do not do version tracking yet; for that, we need to get the current
  332.         # distrorelease and the current package version in that distrorelease
  333.         # (or, of course, proper version tracking in Launchpad itself)
  334.         try:
  335.             b = Bug(id)
  336.         except Bug.Error.LPUrlError, e:
  337.             if e.value.startswith('Page not found'):
  338.                 return 'invalid'
  339.             else:
  340.                 raise
  341.  
  342.         if b.status == 'Fix Released':
  343.             if b.sourcepackage:
  344.                 try:
  345.                     return get_source_version(self.distro, b.sourcepackage, self.hostname)
  346.                 except ValueError:
  347.                     return '' # broken bug
  348.             return ''
  349.         if b.status == 'Invalid' or b.duplicate_of:
  350.             return 'invalid'
  351.         return None
  352.  
  353.     def duplicate_of(self, id):
  354.         '''Return master ID for a duplicate bug.
  355.  
  356.         If the bug is not a duplicate, return None.
  357.         '''
  358.         b =  Bug(id)
  359.         return b.duplicate_of
  360.  
  361.     def close_duplicate(self, id, master):
  362.         '''Mark a crash id as duplicate of given master ID.
  363.         
  364.         If master is None, id gets un-duplicated.
  365.         '''
  366.         bug = Bug(id)
  367.  
  368.         # check whether the master itself is a dup
  369.         if master:
  370.             m = Bug(master)
  371.             if m.duplicate_of:
  372.                 master = m.duplicate_of
  373.  
  374.             bug.attachments.remove(
  375.                 func=lambda a: re.match('^(CoreDump.gz$|Stacktrace.txt|ThreadStacktrace.txt|\
  376. Dependencies.txt$|ProcMaps.txt$|ProcStatus.txt$|Registers.txt$|\
  377. Disassembly.txt$)', a.lp_filename))
  378.             if bug.private:
  379.                 bug.private = None
  380.             bug.commit()
  381.  
  382.             # set duplicate last, since we cannot modify already dup'ed bugs
  383.             bug = Bug(id)
  384.             bug.duplicate_of = int(master)
  385.         else:
  386.             bug.duplicate_of = None
  387.         bug.commit()
  388.  
  389.     def mark_regression(self, id, master):
  390.         '''Mark a crash id as reintroducing an earlier crash which is
  391.         already marked as fixed (having ID 'master').'''
  392.         
  393.         bug = Bug(id)
  394.         comment = Bug.NewComment(subject='Possible regression detected',
  395.             text='This crash has the same stack trace characteristics as bug #%i. \
  396. However, the latter was already fixed in an earlier package version than the \
  397. one in this report. This might be a regression or because the problem is \
  398. in a dependent package.' % master)
  399.         bug.comments.add(comment)
  400.         bug.tags.append('regression-retracer')
  401.         bug.commit()
  402.  
  403.     def mark_retraced(self, id):
  404.         '''Mark crash id as retraced.'''
  405.  
  406.         b = Bug(id)
  407.         if self.arch_tag in b.tags:
  408.             b.tags.remove(self.arch_tag)
  409.         b.commit()
  410.  
  411.     def mark_retrace_failed(self, id, invalid_msg=None):
  412.         '''Mark crash id as 'failed to retrace'.'''
  413.  
  414.         b = Bug(id)
  415.         if invalid_msg:
  416.             comment = Bug.NewComment(subject='Crash report cannot be processed',
  417.                 text=invalid_msg)
  418.             b.comments.add(comment)
  419.             b.status = 'Invalid'
  420.  
  421.             b.attachments.remove(
  422.                 func=lambda a: re.match('^(CoreDump.gz$|Stacktrace.txt|ThreadStacktrace.txt|\
  423. Dependencies.txt$|ProcMaps.txt$|ProcStatus.txt$|Registers.txt$|\
  424. Disassembly.txt$)', a.lp_filename))
  425.         else:
  426.             if 'apport-failed-retrace' not in b.tags:
  427.                 b.tags.append('apport-failed-retrace')
  428.         b.commit()
  429.  
  430.     def _mark_dup_checked(self, id, report):
  431.         '''Mark crash id as checked for being a duplicate.'''
  432.  
  433.         b = Bug(id)
  434.         if 'need-duplicate-check' in b.tags:
  435.             b.tags.remove('need-duplicate-check')
  436.         
  437.         self._subscribe_triaging_team(b, report)
  438.         b.commit()
  439.  
  440.     def _subscribe_triaging_team(self, bug, report):
  441.         '''Subscribe the right triaging team to the bug.'''
  442.  
  443.         #FIXME: this entire function is an ugly Ubuntu specific hack until LP
  444.         #gets a real crash db; see https://wiki.ubuntu.com/CrashReporting
  445.  
  446.         if report['DistroRelease'].split()[0] != 'Ubuntu':
  447.             return # only Ubuntu bugs are filed private
  448.  
  449.         try:
  450.             bug.subscriptions.add('ubuntu-crashes-universe')
  451.         except ValueError:
  452.             # already subscribed
  453.             pass
  454.  
  455. #
  456. # Unit tests
  457. #
  458.  
  459. if __name__ == '__main__':
  460.     import unittest, urllib2, cookielib
  461.  
  462.     crashdb = None
  463.     segv_report = None
  464.     python_report = None
  465.  
  466.     class _Tests(unittest.TestCase):
  467.         # this assumes that a source package "coreutils" exists and builds a
  468.         # binary package "coreutils"
  469.         test_package = 'coreutils'
  470.         test_srcpackage = 'coreutils'
  471.         known_test_id = 89040
  472.         known_test_id2 = 302779
  473.  
  474.         #
  475.         # Generic tests, should work for all CrashDB implementations
  476.         #
  477.  
  478.         def setUp(self):
  479.             global crashdb
  480.             if not crashdb:
  481.                 crashdb = self._get_instance()
  482.             self.crashdb = crashdb
  483.  
  484.             # create a local reference report so that we can compare
  485.             # DistroRelease, Architecture, etc.
  486.             self.ref_report = apport.Report()
  487.             self.ref_report.add_os_info()
  488.             self.ref_report.add_user_info()
  489.  
  490.         def _file_segv_report(self):
  491.             '''File a SEGV crash report.
  492.  
  493.             Return crash ID.
  494.             '''
  495.             r = apport.report._ApportReportTest._generate_sigsegv_report()
  496.             r.add_package_info(self.test_package)
  497.             r.add_os_info()
  498.             r.add_gdb_info()
  499.             r.add_user_info()
  500.             self.assertEqual(r.standard_title(), 'crash crashed with SIGSEGV in f()')
  501.  
  502.             handle = self.crashdb.upload(r)
  503.             self.assert_(handle)
  504.             url = self.crashdb.get_comment_url(r, handle)
  505.             self.assert_(url)
  506.  
  507.             id = self._fill_bug_form(url)
  508.             self.assert_(id > 0)
  509.             return id
  510.  
  511.         def test_1_report_segv(self):
  512.             '''upload() and get_comment_url() for SEGV crash
  513.             
  514.             This needs to run first, since it sets segv_report.
  515.             '''
  516.             global segv_report
  517.             id = self._file_segv_report()
  518.             segv_report = id
  519.             print >> sys.stderr, '(https://staging.launchpad.net/bugs/%i) ' % id,
  520.  
  521.         def test_1_report_python(self):
  522.             '''upload() and get_comment_url() for Python crash
  523.             
  524.             This needs to run early, since it sets python_report.
  525.             '''
  526.             r = apport.Report('Crash')
  527.             r['ExecutablePath'] = '/bin/foo'
  528.             r['Traceback'] = '''Traceback (most recent call last):
  529.   File "/bin/foo", line 67, in fuzz
  530.     print weird
  531. NameError: global name 'weird' is not defined'''
  532.             r.add_package_info(self.test_package)
  533.             r.add_os_info()
  534.             r.add_user_info()
  535.             self.assertEqual(r.standard_title(), 'foo crashed with NameError in fuzz()')
  536.  
  537.             handle = self.crashdb.upload(r)
  538.             self.assert_(handle)
  539.             url = self.crashdb.get_comment_url(r, handle)
  540.             self.assert_(url)
  541.  
  542.             id = self._fill_bug_form(url)
  543.             self.assert_(id > 0)
  544.             global python_report
  545.             python_report = id
  546.             print >> sys.stderr, '(https://staging.launchpad.net/bugs/%i) ' % id,
  547.  
  548.         def test_2_download(self):
  549.             '''download()'''
  550.  
  551.             r = self.crashdb.download(segv_report)
  552.             self.assertEqual(r['ProblemType'], 'Crash')
  553.             self.assertEqual(r['DistroRelease'], self.ref_report['DistroRelease'])
  554.             self.assertEqual(r['Architecture'], self.ref_report['Architecture'])
  555.             self.assertEqual(r['Uname'], self.ref_report['Uname'])
  556.             self.assertEqual(r.get('NonfreeKernelModules'),
  557.                 self.ref_report.get('NonfreeKernelModules'))
  558.             self.assertEqual(r.get('UserGroups'), self.ref_report.get('UserGroups'))
  559.  
  560.             self.assertEqual(r['Signal'], '11')
  561.             self.assert_(r['ExecutablePath'].endswith('/crash'))
  562.             self.assertEqual(r['SourcePackage'], self.test_srcpackage)
  563.             self.assert_(r['Package'].startswith(self.test_package + ' '))
  564.             self.assert_('f (x=42)' in r['Stacktrace'])
  565.             self.assert_('f (x=42)' in r['StacktraceTop'])
  566.             self.assert_(len(r['CoreDump']) > 1000)
  567.             self.assert_('Dependencies' in r)
  568.  
  569.         def test_3_update(self):
  570.             '''update()'''
  571.  
  572.             r = self.crashdb.download(segv_report)
  573.  
  574.             # updating with an useless stack trace retains core dump
  575.             r['StacktraceTop'] = '?? ()'
  576.             r['Stacktrace'] = 'long\ntrace'
  577.             r['ThreadStacktrace'] = 'thread\neven longer\ntrace'
  578.             self.crashdb.update(segv_report, r, 'I can has a better retrace?')
  579.             r = self.crashdb.download(segv_report)
  580.             self.assert_('CoreDump' in r)
  581.  
  582.             # updating with an useful stack trace removes core dump
  583.             r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
  584.             r['Stacktrace'] = 'long\ntrace'
  585.             r['ThreadStacktrace'] = 'thread\neven longer\ntrace'
  586.             self.crashdb.update(segv_report, r, 'good retrace!')
  587.             r = self.crashdb.download(segv_report)
  588.             self.failIf('CoreDump' in r)
  589.  
  590.         def test_get_distro_release(self):
  591.             '''get_distro_release()'''
  592.  
  593.             self.assertEqual(self.crashdb.get_distro_release(segv_report),
  594.                     self.ref_report['DistroRelease'])
  595.  
  596.         def test_duplicates(self):
  597.             '''duplicate handling'''
  598.  
  599.             # initially we have no dups
  600.             self.assertEqual(self.crashdb.duplicate_of(segv_report), None)
  601.             self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
  602.  
  603.             # dupe our segv_report and check that it worked; then undupe it
  604.             self.crashdb.close_duplicate(segv_report, self.known_test_id)
  605.             self.assertEqual(self.crashdb.duplicate_of(segv_report), self.known_test_id)
  606.             self.assertEqual(self.crashdb.get_fixed_version(segv_report), 'invalid')
  607.             self.crashdb.close_duplicate(segv_report, None)
  608.             self.assertEqual(self.crashdb.duplicate_of(segv_report), None)
  609.             self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
  610.  
  611.             # this should have removed attachments
  612.             r = self.crashdb.download(segv_report)
  613.             self.failIf('CoreDump' in r)
  614.  
  615.             # now try duplicating to a duplicate bug; this should automatically
  616.             # transition to the master bug
  617.             self.crashdb.close_duplicate(self.known_test_id,
  618.                     self.known_test_id2)
  619.             self.crashdb.close_duplicate(segv_report, self.known_test_id)
  620.             self.assertEqual(self.crashdb.duplicate_of(segv_report),
  621.                     self.known_test_id2)
  622.  
  623.             self.crashdb.close_duplicate(self.known_test_id, None)
  624.             self.crashdb.close_duplicate(self.known_test_id2, None)
  625.             self.crashdb.close_duplicate(segv_report, None)
  626.  
  627.         def test_marking_segv(self):
  628.             '''processing status markings for signal crashes'''
  629.  
  630.             # mark_retraced()
  631.             unretraced_before = self.crashdb.get_unretraced()
  632.             self.assert_(segv_report in unretraced_before)
  633.             self.failIf(python_report in unretraced_before)
  634.             self.crashdb.mark_retraced(segv_report)
  635.             unretraced_after = self.crashdb.get_unretraced()
  636.             self.failIf(segv_report in unretraced_after)
  637.             self.assertEqual(unretraced_before,
  638.                     unretraced_after.union(set([segv_report])))
  639.             self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
  640.  
  641.             # mark_retrace_failed()
  642.             self._mark_needs_retrace(segv_report)
  643.             self.crashdb.mark_retraced(segv_report)
  644.             self.crashdb.mark_retrace_failed(segv_report)
  645.             unretraced_after = self.crashdb.get_unretraced()
  646.             self.failIf(segv_report in unretraced_after)
  647.             self.assertEqual(unretraced_before,
  648.                     unretraced_after.union(set([segv_report])))
  649.             self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
  650.  
  651.             # mark_retrace_failed() of invalid bug
  652.             self._mark_needs_retrace(segv_report)
  653.             self.crashdb.mark_retraced(segv_report)
  654.             self.crashdb.mark_retrace_failed(segv_report, "I don't like you")
  655.             unretraced_after = self.crashdb.get_unretraced()
  656.             self.failIf(segv_report in unretraced_after)
  657.             self.assertEqual(unretraced_before,
  658.                     unretraced_after.union(set([segv_report])))
  659.             self.assertEqual(self.crashdb.get_fixed_version(segv_report),
  660.                     'invalid')
  661.  
  662.         def test_marking_python(self):
  663.             '''processing status markings for interpreter crashes'''
  664.  
  665.             unchecked_before = self.crashdb.get_dup_unchecked()
  666.             self.assert_(python_report in unchecked_before)
  667.             self.failIf(segv_report in unchecked_before)
  668.             self.crashdb._mark_dup_checked(python_report, self.ref_report)
  669.             unchecked_after = self.crashdb.get_dup_unchecked()
  670.             self.failIf(python_report in unchecked_after)
  671.             self.assertEqual(unchecked_before,
  672.                     unchecked_after.union(set([python_report])))
  673.             self.assertEqual(self.crashdb.get_fixed_version(python_report),
  674.                     None)
  675.  
  676.         def test_update_invalid(self):
  677.             '''updating a invalid crash
  678.             
  679.             This simulates a race condition where a crash being processed gets
  680.             invalidated by marking it as a duplicate.
  681.             '''
  682.             id = self._file_segv_report()
  683.             print >> sys.stderr, '(https://staging.launchpad.net/bugs/%i) ' % id,
  684.  
  685.             r = self.crashdb.download(id)
  686.  
  687.             self.crashdb.close_duplicate(id, segv_report)
  688.  
  689.             # updating with an useful stack trace removes core dump
  690.             r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
  691.             r['Stacktrace'] = 'long\ntrace'
  692.             r['ThreadStacktrace'] = 'thread\neven longer\ntrace'
  693.             self.crashdb.update(id, r, 'good retrace!')
  694.  
  695.             r = self.crashdb.download(id)
  696.             self.failIf('CoreDump' in r)
  697.  
  698.         #
  699.         # Launchpad specific implementation and tests
  700.         #
  701.  
  702.         @classmethod
  703.         def _get_instance(klass):
  704.             '''Create a CrashDB instance'''
  705.  
  706.             return CrashDatabase(os.path.expanduser('~/.lpcookie.txt'), 
  707.                     '', {'distro': 'ubuntu', 'staging': True})
  708.  
  709.         def _fill_bug_form(self, url):
  710.             '''Fill form for a distro bug and commit the bug.
  711.  
  712.             Return the report ID.
  713.             '''
  714.             cj = cookielib.MozillaCookieJar()
  715.             cj.load(self.crashdb.cookie_file)
  716.             opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
  717.  
  718.             re_pkg = re.compile('<input type="text" value="([^"]+)" id="field.packagename"')
  719.             re_title = re.compile('<input.*id="field.title".*value="([^"]+)"')
  720.             re_tags = re.compile('<input.*id="field.tags".*value="([^"]+)"')
  721.  
  722.             # parse default field values from reporting page
  723.             url = url.replace('+filebug/', '+filebug-advanced/')
  724.             
  725.             res = opener.open(url)
  726.             self.assertEqual(res.getcode(), 200)
  727.             content = res.read()
  728.  
  729.             m_pkg = re_pkg.search(content)
  730.             m_title = re_title.search(content)
  731.             m_tags = re_tags.search(content)
  732.  
  733.             # strip off GET arguments from URL
  734.             url = url.split('?')[0]
  735.  
  736.             # create request to file bug
  737.             args = {
  738.                 'packagename_option': 'choose',
  739.                 'field.packagename': m_pkg.group(1),
  740.                 'field.title': m_title.group(1),
  741.                 'field.tags': m_tags.group(1),
  742.                 'field.comment': 'ZOMG!',
  743.                 'field.actions.submit_bug': '1',
  744.             }
  745.  
  746.             res = opener.open(url, data=urllib.urlencode(args))
  747.             self.assertEqual(res.getcode(), 200)
  748.             self.assert_('+source/%s/+bug/' % m_pkg.group(1) in res.geturl())
  749.             id = res.geturl().split('/')[-1]
  750.             return int(id)
  751.  
  752.         def _fill_bug_form_project(self, url):
  753.             '''Fill form for a project bug and commit the bug.
  754.  
  755.             Return the report ID.
  756.             '''
  757.             cj = cookielib.MozillaCookieJar()
  758.             cj.load(self.crashdb.cookie_file)
  759.             opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
  760.  
  761.             m = re.search('launchpad.net/([^/]+)/\+filebug', url)
  762.             assert m
  763.             project = m.group(1)
  764.  
  765.             re_title = re.compile('<input.*id="field.title".*value="([^"]+)"')
  766.             re_tags = re.compile('<input.*id="field.tags".*value="([^"]+)"')
  767.  
  768.             # parse default field values from reporting page
  769.             url = url.replace('+filebug/', '+filebug-advanced/')
  770.             
  771.             res = opener.open(url)
  772.             self.assertEqual(res.getcode(), 200)
  773.             content = res.read()
  774.  
  775.             m_title = re_title.search(content)
  776.             m_tags = re_tags.search(content)
  777.  
  778.             # strip off GET arguments from URL
  779.             url = url.split('?')[0]
  780.  
  781.             # create request to file bug
  782.             args = {
  783.                 'field.title': m_title.group(1),
  784.                 'field.tags': m_tags.group(1),
  785.                 'field.comment': 'ZOMG!',
  786.                 'field.actions.submit_bug': '1',
  787.             }
  788.  
  789.             res = opener.open(url, data=urllib.urlencode(args))
  790.             self.assertEqual(res.getcode(), 200)
  791.             self.assert_(('launchpad.net/%s/+bug' % project) in res.geturl())
  792.             id = res.geturl().split('/')[-1]
  793.             return int(id)
  794.  
  795.         def _mark_needs_retrace(self, id):
  796.             '''Mark a report ID as needing retrace.'''
  797.  
  798.             b = Bug(id)
  799.             if self.crashdb.arch_tag not in b.tags:
  800.                 b.tags.append(self.crashdb.arch_tag)
  801.             b.commit()
  802.  
  803.         def _mark_needs_dupcheck(self, id):
  804.             '''Mark a report ID as needing duplicate check.'''
  805.  
  806.             b = Bug(id)
  807.             if 'need-duplicate-check' not in b.tags:
  808.                 b.tags.append('need-duplicate-check')
  809.             b.commit()
  810.  
  811.         def test_project(self):
  812.             '''reporting crashes against a project instead of a distro'''
  813.  
  814.             # crash database for langpack-o-matic project (this does not have
  815.             # packages in any distro)
  816.             crashdb = CrashDatabase(os.path.expanduser('~/.lpcookie.txt'), 
  817.                 '', {'project': 'langpack-o-matic', 'staging': True})
  818.             self.assertEqual(crashdb.distro, None)
  819.  
  820.             # create Python crash report
  821.             r = apport.Report('Crash')
  822.             r['ExecutablePath'] = '/bin/foo'
  823.             r['Traceback'] = '''Traceback (most recent call last):
  824.   File "/bin/foo", line 67, in fuzz
  825.     print weird
  826. NameError: global name 'weird' is not defined'''
  827.             r.add_os_info()
  828.             r.add_user_info()
  829.             self.assertEqual(r.standard_title(), 'foo crashed with NameError in fuzz()')
  830.  
  831.             # file it
  832.             handle = crashdb.upload(r)
  833.             self.assert_(handle)
  834.             url = crashdb.get_comment_url(r, handle)
  835.             self.assert_('launchpad.net/langpack-o-matic/+filebug' in url)
  836.  
  837.             id = self._fill_bug_form_project(url)
  838.             self.assert_(id > 0)
  839.             print >> sys.stderr, '(https://staging.launchpad.net/bugs/%i) ' % id,
  840.  
  841.             # update
  842.             r = crashdb.download(id)
  843.             r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
  844.             r['Stacktrace'] = 'long\ntrace'
  845.             r['ThreadStacktrace'] = 'thread\neven longer\ntrace'
  846.             crashdb.update(id, r, 'good retrace!')
  847.             r = crashdb.download(id)
  848.  
  849.             # test fixed version
  850.             self.assertEqual(crashdb.get_fixed_version(id), None)
  851.             crashdb.close_duplicate(id, self.known_test_id)
  852.             self.assertEqual(crashdb.duplicate_of(id), self.known_test_id)
  853.             self.assertEqual(crashdb.get_fixed_version(id), 'invalid')
  854.             crashdb.close_duplicate(id, None)
  855.             self.assertEqual(crashdb.duplicate_of(id), None)
  856.             self.assertEqual(crashdb.get_fixed_version(id), None)
  857.  
  858.     unittest.main()
  859.