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__),'Resources/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__),'Resources/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