Back to index

apport  2.4
memory.py
Go to the documentation of this file.
00001 '''Simple in-memory CrashDatabase implementation, mainly useful for testing.'''
00002 
00003 # Copyright (C) 2007 - 2009 Canonical Ltd.
00004 # Author: Martin Pitt <martin.pitt@ubuntu.com>
00005 #
00006 # This program is free software; you can redistribute it and/or modify it
00007 # under the terms of the GNU General Public License as published by the
00008 # Free Software Foundation; either version 2 of the License, or (at your
00009 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
00010 # the full text of the license.
00011 
00012 import apport.crashdb
00013 import apport
00014 
00015 
00016 class CrashDatabase(apport.crashdb.CrashDatabase):
00017     '''Simple implementation of crash database interface which keeps everything
00018     in memory.
00019 
00020     This is mainly useful for testing and debugging.'''
00021 
00022     def __init__(self, auth_file, options):
00023         '''Initialize crash database connection.
00024 
00025         This class does not support bug patterns and authentication.'''
00026 
00027         apport.crashdb.CrashDatabase.__init__(self, auth_file, options)
00028 
00029         self.reports = []  # list of dictionaries with keys: report, fixed_version, dup_of, comment
00030         self.unretraced = set()
00031         self.dup_unchecked = set()
00032 
00033         if 'dummy_data' in options:
00034             self.add_dummy_data()
00035 
00036     def upload(self, report, progress_callback=None):
00037         '''Store the report and return a handle number (starting from 0).
00038 
00039         This does not support (nor need) progress callbacks.
00040         '''
00041         assert self.accepts(report)
00042 
00043         self.reports.append({'report': report, 'fixed_version': None, 'dup_of':
00044                              None, 'comment:': ''})
00045         id = len(self.reports) - 1
00046         if 'Traceback' in report:
00047             self.dup_unchecked.add(id)
00048         else:
00049             self.unretraced.add(id)
00050         return id
00051 
00052     def get_comment_url(self, report, handle):
00053         '''Return http://<sourcepackage>.bugs.example.com/<handle> for package bugs
00054         or http://bugs.example.com/<handle> for reports without a SourcePackage.'''
00055 
00056         if 'SourcePackage' in report:
00057             return 'http://%s.bugs.example.com/%i' % (report['SourcePackage'], handle)
00058         else:
00059             return 'http://bugs.example.com/%i' % handle
00060 
00061     def get_id_url(self, report, id):
00062         '''Return URL for a given report ID.
00063 
00064         The report is passed in case building the URL needs additional
00065         information from it, such as the SourcePackage name.
00066 
00067         Return None if URL is not available or cannot be determined.
00068         '''
00069         return self.get_comment_url(report, id)
00070 
00071     def download(self, id):
00072         '''Download the problem report from given ID and return a Report.'''
00073 
00074         return self.reports[id]['report']
00075 
00076     def get_affected_packages(self, id):
00077         '''Return list of affected source packages for given ID.'''
00078 
00079         return [self.reports[id]['report']['SourcePackage']]
00080 
00081     def is_reporter(self, id):
00082         '''Check whether the user is the reporter of given ID.'''
00083 
00084         return True
00085 
00086     def can_update(self, id):
00087         '''Check whether the user is eligible to update a report.
00088 
00089         A user should add additional information to an existing ID if (s)he is
00090         the reporter or subscribed, the bug is open, not a duplicate, etc. The
00091         exact policy and checks should be done according to  the particular
00092         implementation.
00093         '''
00094         return self.is_reporter(id)
00095 
00096     def update(self, id, report, comment, change_description=False,
00097                attachment_comment=None, key_filter=None):
00098         '''Update the given report ID with all data from report.
00099 
00100         This creates a text comment with the "short" data (see
00101         ProblemReport.write_mime()), and creates attachments for all the
00102         bulk/binary data.
00103 
00104         If change_description is True, and the crash db implementation supports
00105         it, the short data will be put into the description instead (like in a
00106         new bug).
00107 
00108         comment will be added to the "short" data. If attachment_comment is
00109         given, it will be added to the attachment uploads.
00110 
00111         If key_filter is a list or set, then only those keys will be added.
00112         '''
00113         r = self.reports[id]
00114         r['comment'] = comment
00115 
00116         if key_filter:
00117             for f in key_filter:
00118                 if f in report:
00119                     r['report'][f] = report[f]
00120         else:
00121             r['report'].update(report)
00122 
00123     def get_distro_release(self, id):
00124         '''Get 'DistroRelease: <release>' from the given report ID and return
00125         it.'''
00126 
00127         return self.reports[id]['report']['DistroRelease']
00128 
00129     def get_unfixed(self):
00130         '''Return an ID set of all crashes which are not yet fixed.
00131 
00132         The list must not contain bugs which were rejected or duplicate.
00133 
00134         This function should make sure that the returned list is correct. If
00135         there are any errors with connecting to the crash database, it should
00136         raise an exception (preferably IOError).'''
00137 
00138         result = set()
00139         for i in range(len(self.reports)):
00140             if self.reports[i]['dup_of'] is None and self.reports[i]['fixed_version'] is None:
00141                 result.add(i)
00142 
00143         return result
00144 
00145     def get_fixed_version(self, id):
00146         '''Return the package version that fixes a given crash.
00147 
00148         Return None if the crash is not yet fixed, or an empty string if the
00149         crash is fixed, but it cannot be determined by which version. Return
00150         'invalid' if the crash report got invalidated, such as closed a
00151         duplicate or rejected.
00152 
00153         This function should make sure that the returned result is correct. If
00154         there are any errors with connecting to the crash database, it should
00155         raise an exception (preferably IOError).'''
00156 
00157         try:
00158             if self.reports[id]['dup_of'] is not None:
00159                 return 'invalid'
00160             return self.reports[id]['fixed_version']
00161         except IndexError:
00162             return 'invalid'
00163 
00164     def duplicate_of(self, id):
00165         '''Return master ID for a duplicate bug.
00166 
00167         If the bug is not a duplicate, return None.
00168         '''
00169         return self.reports[id]['dup_of']
00170 
00171     def close_duplicate(self, report, id, master):
00172         '''Mark a crash id as duplicate of given master ID.
00173 
00174         If master is None, id gets un-duplicated.
00175         '''
00176         self.reports[id]['dup_of'] = master
00177 
00178     def mark_regression(self, id, master):
00179         '''Mark a crash id as reintroducing an earlier crash which is
00180         already marked as fixed (having ID 'master').'''
00181 
00182         assert self.reports[master]['fixed_version'] is not None
00183         self.reports[id]['comment'] = 'regression, already fixed in #%i' % master
00184 
00185     def _mark_dup_checked(self, id, report):
00186         '''Mark crash id as checked for being a duplicate.'''
00187 
00188         try:
00189             self.dup_unchecked.remove(id)
00190         except KeyError:
00191             pass  # happens when trying to check for dup twice
00192 
00193     def mark_retraced(self, id):
00194         '''Mark crash id as retraced.'''
00195 
00196         self.unretraced.remove(id)
00197 
00198     def get_unretraced(self):
00199         '''Return an ID set of all crashes which have not been retraced yet and
00200         which happened on the current host architecture.'''
00201 
00202         return self.unretraced
00203 
00204     def get_dup_unchecked(self):
00205         '''Return an ID set of all crashes which have not been checked for
00206         being a duplicate.
00207 
00208         This is mainly useful for crashes of scripting languages such as
00209         Python, since they do not need to be retraced. It should not return
00210         bugs that are covered by get_unretraced().'''
00211 
00212         return self.dup_unchecked
00213 
00214     def latest_id(self):
00215         '''Return the ID of the most recently filed report.'''
00216 
00217         return len(self.reports) - 1
00218 
00219     def add_dummy_data(self):
00220         '''Add some dummy crash reports.
00221 
00222         This is mostly useful for test suites.'''
00223 
00224         # signal crash with source package and complete stack trace
00225         r = apport.Report()
00226         r['Package'] = 'libfoo1 1.2-3'
00227         r['SourcePackage'] = 'foo'
00228         r['DistroRelease'] = 'FooLinux Pi/2'
00229         r['Signal'] = '11'
00230         r['ExecutablePath'] = '/bin/crash'
00231 
00232         r['StacktraceTop'] = '''foo_bar (x=1) at crash.c:28
00233 d01 (x=1) at crash.c:29
00234 raise () from /lib/libpthread.so.0
00235 <signal handler called>
00236 __frob (x=1) at crash.c:30'''
00237         self.upload(r)
00238 
00239         # duplicate of above crash (slightly different arguments and
00240         # package version)
00241         r = apport.Report()
00242         r['Package'] = 'libfoo1 1.2-4'
00243         r['SourcePackage'] = 'foo'
00244         r['DistroRelease'] = 'Testux 1.0'
00245         r['Signal'] = '11'
00246         r['ExecutablePath'] = '/bin/crash'
00247 
00248         r['StacktraceTop'] = '''foo_bar (x=2) at crash.c:28
00249 d01 (x=3) at crash.c:29
00250 raise () from /lib/libpthread.so.0
00251 <signal handler called>
00252 __frob (x=4) at crash.c:30'''
00253         self.upload(r)
00254 
00255         # unrelated signal crash
00256         r = apport.Report()
00257         r['Package'] = 'bar 42-4'
00258         r['SourcePackage'] = 'bar'
00259         r['DistroRelease'] = 'Testux 1.0'
00260         r['Signal'] = '11'
00261         r['ExecutablePath'] = '/usr/bin/broken'
00262 
00263         r['StacktraceTop'] = '''h (p=0x0) at crash.c:25
00264 g (x=1, y=42) at crash.c:26
00265 f (x=1) at crash.c:27
00266 e (x=1) at crash.c:28
00267 d (x=1) at crash.c:29'''
00268         self.upload(r)
00269 
00270         # Python crash
00271         r = apport.Report()
00272         r['Package'] = 'python-goo 3epsilon1'
00273         r['SourcePackage'] = 'pygoo'
00274         r['DistroRelease'] = 'Testux 2.2'
00275         r['ExecutablePath'] = '/usr/bin/pygoo'
00276         r['Traceback'] = '''Traceback (most recent call last):
00277   File "test.py", line 7, in <module>
00278     print(_f(5))
00279   File "test.py", line 5, in _f
00280     return g_foo00(x+1)
00281   File "test.py", line 2, in g_foo00
00282     return x/0
00283 ZeroDivisionError: integer division or modulo by zero'''
00284         self.upload(r)
00285 
00286         # Python crash reoccurs in a later version (used for regression detection)
00287         r = apport.Report()
00288         r['Package'] = 'python-goo 5'
00289         r['SourcePackage'] = 'pygoo'
00290         r['DistroRelease'] = 'Testux 2.2'
00291         r['ExecutablePath'] = '/usr/bin/pygoo'
00292         r['Traceback'] = '''Traceback (most recent call last):
00293   File "test.py", line 7, in <module>
00294     print(_f(5))
00295   File "test.py", line 5, in _f
00296     return g_foo00(x+1)
00297   File "test.py", line 2, in g_foo00
00298     return x/0
00299 ZeroDivisionError: integer division or modulo by zero'''
00300         self.upload(r)