1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3 4"""Methods for reporting bugs.""" 5 6import subprocess, sys, os 7 8__all__ = ['ReportFailure', 'BugReport', 'getReporters'] 9 10# 11 12class 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 19class 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 27import os 28 29import email, mimetypes, smtplib 30from email import encoders 31from email.message import Message 32from email.mime.base import MIMEBase 33from email.mime.multipart import MIMEMultipart 34from email.mime.text import MIMEText 35 36#===------------------------------------------------------------------------===# 37# ReporterParameter 38#===------------------------------------------------------------------------===# 39 40class 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 50class 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 58class 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 78class 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 = """\ 114BUG REPORT 115--- 116Title: %s 117Description: %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 146class 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 157class 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 177class 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 245def getReporters(): 246 reporters = [] 247 if RadarReporter.isAvailable(): 248 reporters.append(RadarReporter()) 249 reporters.append(EmailReporter()) 250 return reporters 251 252