Home | History | Annotate | Download | only in share
      1 """Methods for reporting bugs."""
      2 
      3 import subprocess, sys, os
      4 
      5 __all__ = ['ReportFailure', 'BugReport', 'getReporters']
      6 
      7 #
      8 
      9 class ReportFailure(Exception):
     10     """Generic exception for failures in bug reporting."""
     11     def __init__(self, value):        
     12         self.value = value
     13 
     14 # Collect information about a bug.
     15 
     16 class BugReport:
     17     def __init__(self, title, description, files):
     18         self.title = title
     19         self.description = description
     20         self.files = files
     21 
     22 # Reporter interfaces.
     23 
     24 import os
     25 
     26 import email, mimetypes, smtplib
     27 from email import encoders
     28 from email.message import Message
     29 from email.mime.base import MIMEBase
     30 from email.mime.multipart import MIMEMultipart
     31 from email.mime.text import MIMEText
     32 
     33 #===------------------------------------------------------------------------===#
     34 # ReporterParameter
     35 #===------------------------------------------------------------------------===#
     36 
     37 class ReporterParameter:
     38   def __init__(self, n):
     39     self.name = n
     40   def getName(self):
     41     return self.name
     42   def getValue(self,r,bugtype,getConfigOption):
     43      return getConfigOption(r.getName(),self.getName())
     44   def saveConfigValue(self):
     45     return True
     46 
     47 class TextParameter (ReporterParameter):
     48   def getHTML(self,r,bugtype,getConfigOption):
     49     return """\
     50 <tr>
     51 <td class="form_clabel">%s:</td>
     52 <td class="form_value"><input type="text" name="%s_%s" value="%s"></td>
     53 </tr>"""%(self.getName(),r.getName(),self.getName(),self.getValue(r,bugtype,getConfigOption))
     54 
     55 class SelectionParameter (ReporterParameter):
     56   def __init__(self, n, values):
     57     ReporterParameter.__init__(self,n)
     58     self.values = values
     59     
     60   def getHTML(self,r,bugtype,getConfigOption):
     61     default = self.getValue(r,bugtype,getConfigOption)
     62     return """\
     63 <tr>
     64 <td class="form_clabel">%s:</td><td class="form_value"><select name="%s_%s">
     65 %s
     66 </select></td>"""%(self.getName(),r.getName(),self.getName(),'\n'.join(["""\
     67 <option value="%s"%s>%s</option>"""%(o[0],
     68                                      o[0] == default and ' selected="selected"' or '',
     69                                      o[1]) for o in self.values]))
     70 
     71 #===------------------------------------------------------------------------===#
     72 # Reporters
     73 #===------------------------------------------------------------------------===#
     74 
     75 class EmailReporter:
     76     def getName(self):
     77         return 'Email'
     78 
     79     def getParameters(self):
     80         return map(lambda x:TextParameter(x),['To', 'From', 'SMTP Server', 'SMTP Port'])
     81 
     82     # Lifted from python email module examples.
     83     def attachFile(self, outer, path):
     84         # Guess the content type based on the file's extension.  Encoding
     85         # will be ignored, although we should check for simple things like
     86         # gzip'd or compressed files.
     87         ctype, encoding = mimetypes.guess_type(path)
     88         if ctype is None or encoding is not None:
     89             # No guess could be made, or the file is encoded (compressed), so
     90             # use a generic bag-of-bits type.
     91             ctype = 'application/octet-stream'
     92         maintype, subtype = ctype.split('/', 1)
     93         if maintype == 'text':
     94             fp = open(path)
     95             # Note: we should handle calculating the charset
     96             msg = MIMEText(fp.read(), _subtype=subtype)
     97             fp.close()
     98         else:
     99             fp = open(path, 'rb')
    100             msg = MIMEBase(maintype, subtype)
    101             msg.set_payload(fp.read())
    102             fp.close()
    103             # Encode the payload using Base64
    104             encoders.encode_base64(msg)
    105         # Set the filename parameter
    106         msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(path))
    107         outer.attach(msg)
    108 
    109     def fileReport(self, report, parameters):
    110         mainMsg = """\
    111 BUG REPORT
    112 ---
    113 Title: %s
    114 Description: %s
    115 """%(report.title, report.description)
    116 
    117         if not parameters.get('To'):
    118             raise ReportFailure('No "To" address specified.')
    119         if not parameters.get('From'):
    120             raise ReportFailure('No "From" address specified.')
    121 
    122         msg = MIMEMultipart()
    123         msg['Subject'] = 'BUG REPORT: %s'%(report.title)
    124         # FIXME: Get config parameters
    125         msg['To'] = parameters.get('To')
    126         msg['From'] = parameters.get('From')
    127         msg.preamble = mainMsg
    128 
    129         msg.attach(MIMEText(mainMsg, _subtype='text/plain'))
    130         for file in report.files:
    131             self.attachFile(msg, file)
    132 
    133         try:
    134             s = smtplib.SMTP(host=parameters.get('SMTP Server'),
    135                              port=parameters.get('SMTP Port'))
    136             s.sendmail(msg['From'], msg['To'], msg.as_string())
    137             s.close()
    138         except:
    139             raise ReportFailure('Unable to send message via SMTP.')
    140 
    141         return "Message sent!"
    142 
    143 class BugzillaReporter:
    144     def getName(self):
    145         return 'Bugzilla'
    146     
    147     def getParameters(self):
    148         return map(lambda x:TextParameter(x),['URL','Product'])
    149 
    150     def fileReport(self, report, parameters):
    151         raise NotImplementedError
    152  
    153 
    154 class RadarClassificationParameter(SelectionParameter):
    155   def __init__(self):
    156     SelectionParameter.__init__(self,"Classification",
    157             [['1', 'Security'], ['2', 'Crash/Hang/Data Loss'],
    158              ['3', 'Performance'], ['4', 'UI/Usability'], 
    159              ['6', 'Serious Bug'], ['7', 'Other']])
    160 
    161   def saveConfigValue(self):
    162     return False
    163     
    164   def getValue(self,r,bugtype,getConfigOption):
    165     if bugtype.find("leak") != -1:
    166       return '3'
    167     elif bugtype.find("dereference") != -1:
    168       return '2'
    169     elif bugtype.find("missing ivar release") != -1:
    170       return '3'
    171     else:
    172       return '7'
    173 
    174 class RadarReporter:
    175     @staticmethod
    176     def isAvailable():
    177         # FIXME: Find this .scpt better
    178         path = os.path.join(os.path.dirname(__file__),'../share/scan-view/GetRadarVersion.scpt')
    179         try:
    180           p = subprocess.Popen(['osascript',path], 
    181           stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    182         except:
    183             return False
    184         data,err = p.communicate()
    185         res = p.wait()
    186         # FIXME: Check version? Check for no errors?
    187         return res == 0
    188 
    189     def getName(self):
    190         return 'Radar'
    191 
    192     def getParameters(self):
    193         return [ TextParameter('Component'), TextParameter('Component Version'),
    194                  RadarClassificationParameter() ]
    195 
    196     def fileReport(self, report, parameters):
    197         component = parameters.get('Component', '')
    198         componentVersion = parameters.get('Component Version', '')
    199         classification = parameters.get('Classification', '')
    200         personID = ""
    201         diagnosis = ""
    202         config = ""
    203 
    204         if not component.strip():
    205             component = 'Bugs found by clang Analyzer'
    206         if not componentVersion.strip():
    207             componentVersion = 'X'
    208 
    209         script = os.path.join(os.path.dirname(__file__),'../share/scan-view/FileRadar.scpt')
    210         args = ['osascript', script, component, componentVersion, classification, personID, report.title,
    211                 report.description, diagnosis, config] + map(os.path.abspath, report.files)
    212 #        print >>sys.stderr, args
    213         try:
    214           p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    215         except:
    216             raise ReportFailure("Unable to file radar (AppleScript failure).")
    217         data, err = p.communicate()
    218         res = p.wait()
    219 
    220         if res:
    221             raise ReportFailure("Unable to file radar (AppleScript failure).")
    222 
    223         try:
    224             values = eval(data)
    225         except:
    226             raise ReportFailure("Unable to process radar results.")
    227 
    228         # We expect (int: bugID, str: message)
    229         if len(values) != 2 or not isinstance(values[0], int):
    230             raise ReportFailure("Unable to process radar results.")
    231 
    232         bugID,message = values
    233         bugID = int(bugID)
    234         
    235         if not bugID:
    236             raise ReportFailure(message)
    237         
    238         return "Filed: <a href=\"rdar://%d/\">%d</a>"%(bugID,bugID)
    239 
    240 ###
    241 
    242 def getReporters():
    243     reporters = []
    244     if RadarReporter.isAvailable():
    245         reporters.append(RadarReporter())
    246     reporters.append(EmailReporter())
    247     return reporters
    248 
    249