| """Methods for reporting bugs.""" |
| |
| import subprocess, sys, os |
| |
| __all__ = ['ReportFailure', 'BugReport', 'getReporters'] |
| |
| # |
| |
| class ReportFailure(Exception): |
| """Generic exception for failures in bug reporting.""" |
| def __init__(self, value): |
| self.value = value |
| |
| # Collect information about a bug. |
| |
| class BugReport: |
| def __init__(self, title, description, files): |
| self.title = title |
| self.description = description |
| self.files = files |
| |
| # Reporter interfaces. |
| |
| import os |
| |
| import email, mimetypes, smtplib |
| from email import encoders |
| from email.message import Message |
| from email.mime.base import MIMEBase |
| from email.mime.multipart import MIMEMultipart |
| from email.mime.text import MIMEText |
| |
| #===------------------------------------------------------------------------===# |
| # ReporterParameter |
| #===------------------------------------------------------------------------===# |
| |
| class ReporterParameter: |
| def __init__(self, n): |
| self.name = n |
| def getName(self): |
| return self.name |
| def getValue(self,r,bugtype,getConfigOption): |
| return getConfigOption(r.getName(),self.getName()) |
| def saveConfigValue(self): |
| return True |
| |
| class TextParameter (ReporterParameter): |
| def getHTML(self,r,bugtype,getConfigOption): |
| return """\ |
| <tr> |
| <td class="form_clabel">%s:</td> |
| <td class="form_value"><input type="text" name="%s_%s" value="%s"></td> |
| </tr>"""%(self.getName(),r.getName(),self.getName(),self.getValue(r,bugtype,getConfigOption)) |
| |
| class SelectionParameter (ReporterParameter): |
| def __init__(self, n, values): |
| ReporterParameter.__init__(self,n) |
| self.values = values |
| |
| def getHTML(self,r,bugtype,getConfigOption): |
| default = self.getValue(r,bugtype,getConfigOption) |
| return """\ |
| <tr> |
| <td class="form_clabel">%s:</td><td class="form_value"><select name="%s_%s"> |
| %s |
| </select></td>"""%(self.getName(),r.getName(),self.getName(),'\n'.join(["""\ |
| <option value="%s"%s>%s</option>"""%(o[0], |
| o[0] == default and ' selected="selected"' or '', |
| o[1]) for o in self.values])) |
| |
| #===------------------------------------------------------------------------===# |
| # Reporters |
| #===------------------------------------------------------------------------===# |
| |
| class EmailReporter: |
| def getName(self): |
| return 'Email' |
| |
| def getParameters(self): |
| return map(lambda x:TextParameter(x),['To', 'From', 'SMTP Server', 'SMTP Port']) |
| |
| # Lifted from python email module examples. |
| def attachFile(self, outer, path): |
| # Guess the content type based on the file's extension. Encoding |
| # will be ignored, although we should check for simple things like |
| # gzip'd or compressed files. |
| ctype, encoding = mimetypes.guess_type(path) |
| if ctype is None or encoding is not None: |
| # No guess could be made, or the file is encoded (compressed), so |
| # use a generic bag-of-bits type. |
| ctype = 'application/octet-stream' |
| maintype, subtype = ctype.split('/', 1) |
| if maintype == 'text': |
| fp = open(path) |
| # Note: we should handle calculating the charset |
| msg = MIMEText(fp.read(), _subtype=subtype) |
| fp.close() |
| else: |
| fp = open(path, 'rb') |
| msg = MIMEBase(maintype, subtype) |
| msg.set_payload(fp.read()) |
| fp.close() |
| # Encode the payload using Base64 |
| encoders.encode_base64(msg) |
| # Set the filename parameter |
| msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(path)) |
| outer.attach(msg) |
| |
| def fileReport(self, report, parameters): |
| mainMsg = """\ |
| BUG REPORT |
| --- |
| Title: %s |
| Description: %s |
| """%(report.title, report.description) |
| |
| if not parameters.get('To'): |
| raise ReportFailure('No "To" address specified.') |
| if not parameters.get('From'): |
| raise ReportFailure('No "From" address specified.') |
| |
| msg = MIMEMultipart() |
| msg['Subject'] = 'BUG REPORT: %s'%(report.title) |
| # FIXME: Get config parameters |
| msg['To'] = parameters.get('To') |
| msg['From'] = parameters.get('From') |
| msg.preamble = mainMsg |
| |
| msg.attach(MIMEText(mainMsg, _subtype='text/plain')) |
| for file in report.files: |
| self.attachFile(msg, file) |
| |
| try: |
| s = smtplib.SMTP(host=parameters.get('SMTP Server'), |
| port=parameters.get('SMTP Port')) |
| s.sendmail(msg['From'], msg['To'], msg.as_string()) |
| s.close() |
| except: |
| raise ReportFailure('Unable to send message via SMTP.') |
| |
| return "Message sent!" |
| |
| class BugzillaReporter: |
| def getName(self): |
| return 'Bugzilla' |
| |
| def getParameters(self): |
| return map(lambda x:TextParameter(x),['URL','Product']) |
| |
| def fileReport(self, report, parameters): |
| raise NotImplementedError |
| |
| |
| class RadarClassificationParameter(SelectionParameter): |
| def __init__(self): |
| SelectionParameter.__init__(self,"Classification", |
| [['1', 'Security'], ['2', 'Crash/Hang/Data Loss'], |
| ['3', 'Performance'], ['4', 'UI/Usability'], |
| ['6', 'Serious Bug'], ['7', 'Other']]) |
| |
| def saveConfigValue(self): |
| return False |
| |
| def getValue(self,r,bugtype,getConfigOption): |
| if bugtype.find("leak") != -1: |
| return '3' |
| elif bugtype.find("dereference") != -1: |
| return '2' |
| else: |
| return '7' |
| |
| class RadarReporter: |
| @staticmethod |
| def isAvailable(): |
| # FIXME: Find this .scpt better |
| path = os.path.join(os.path.dirname(__file__),'Resources/GetRadarVersion.scpt') |
| try: |
| p = subprocess.Popen(['osascript',path], |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| except: |
| return False |
| data,err = p.communicate() |
| res = p.wait() |
| # FIXME: Check version? Check for no errors? |
| return res == 0 |
| |
| def getName(self): |
| return 'Radar' |
| |
| def getParameters(self): |
| return [ TextParameter('Component'), TextParameter('Component Version'), |
| RadarClassificationParameter() ] |
| |
| def fileReport(self, report, parameters): |
| component = parameters.get('Component', '') |
| componentVersion = parameters.get('Component Version', '') |
| classification = parameters.get('Classification', '') |
| personID = "" |
| diagnosis = "" |
| config = "" |
| |
| if not component.strip(): |
| component = 'Bugs found by clang Analyzer' |
| if not componentVersion.strip(): |
| componentVersion = 'X' |
| |
| script = os.path.join(os.path.dirname(__file__),'Resources/FileRadar.scpt') |
| args = ['osascript', script, component, componentVersion, classification, personID, report.title, |
| report.description, diagnosis, config] + map(os.path.abspath, report.files) |
| # print >>sys.stderr, args |
| try: |
| p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| except: |
| raise ReportFailure("Unable to file radar (AppleScript failure).") |
| data, err = p.communicate() |
| res = p.wait() |
| |
| if res: |
| raise ReportFailure("Unable to file radar (AppleScript failure).") |
| |
| try: |
| values = eval(data) |
| except: |
| raise ReportFailure("Unable to process radar results.") |
| |
| # We expect (int: bugID, str: message) |
| if len(values) != 2 or not isinstance(values[0], int): |
| raise ReportFailure("Unable to process radar results.") |
| |
| bugID,message = values |
| bugID = int(bugID) |
| |
| if not bugID: |
| raise ReportFailure(message) |
| |
| return "Filed: <a href=\"rdar://%d/\">%d</a>"%(bugID,bugID) |
| |
| ### |
| |
| def getReporters(): |
| reporters = [] |
| if RadarReporter.isAvailable(): |
| reporters.append(RadarReporter()) |
| reporters.append(EmailReporter()) |
| return reporters |
| |