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