blob: 800af03b1a2c35a3950f6e0a8fd4bd381cb0f42d [file] [log] [blame]
Andrew Scull5e1ddfa2018-08-14 10:06:54 +01001#!/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