blob: b1ff16142e279cb480d1f2f73f8b211ca7970e1a [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
Andrew Walbran16937d02019-10-22 13:54:20 +010019class BugReport(object):
Andrew Scull5e1ddfa2018-08-14 10:06:54 +010020 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
Andrew Walbran16937d02019-10-22 13:54:20 +010040class ReporterParameter(object):
Andrew Scull5e1ddfa2018-08-14 10:06:54 +010041 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
Andrew Walbran16937d02019-10-22 13:54:20 +010078class EmailReporter(object):
Andrew Scull5e1ddfa2018-08-14 10:06:54 +010079 def getName(self):
80 return 'Email'
81
82 def getParameters(self):
Andrew Walbran16937d02019-10-22 13:54:20 +010083 return [TextParameter(x) for x in ['To', 'From', 'SMTP Server', 'SMTP Port']]
Andrew Scull5e1ddfa2018-08-14 10:06:54 +010084
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
Andrew Walbran16937d02019-10-22 13:54:20 +0100146class BugzillaReporter(object):
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100147 def getName(self):
148 return 'Bugzilla'
149
150 def getParameters(self):
Andrew Walbran16937d02019-10-22 13:54:20 +0100151 return [TextParameter(x) for x in ['URL','Product']]
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100152
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
Andrew Walbran16937d02019-10-22 13:54:20 +0100177class RadarReporter(object):
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100178 @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,
Andrew Walbran16937d02019-10-22 13:54:20 +0100214 report.description, diagnosis, config] + [os.path.abspath(f) for f in report.files]
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100215# 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