OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Provides mailer support for gatekeeper_ng. | |
7 | |
8 This module containes mail templates to notify tree watchers | |
9 when the tree is closed. | |
10 """ | |
11 | |
12 import cStringIO | |
13 import urllib | |
14 | |
15 # From buildbot's results.py. | |
16 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY = range(6) | |
17 Results = ["success", "warnings", "failure", "skipped", "exception", "retry"] | |
18 | |
19 | |
20 def _GenBuildBox(buildstatus, waterfall_url, styles): | |
21 """Generates a box for one build.""" | |
22 class_ = Results[buildstatus['result']] | |
23 style = '' | |
24 if class_ and class_ in styles: | |
25 style = styles[class_] | |
26 reason = buildstatus['reason'] | |
27 url = '%sbuilders/%s/builds/%d' % ( | |
agable
2013/08/01 17:52:44
Make sure that waterfall_url ends with '/'
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
28 waterfall_url, | |
29 urllib.quote(buildstatus['builderName'], safe=''), | |
30 buildstatus['number']) | |
31 fmt = ('<tr><td style="%s"><a title="Reason: %s" href="%s">' | |
32 'Build %d' | |
33 '</a></td></tr>') | |
34 return fmt % (style, reason, url, buildstatus['number']) | |
35 | |
36 | |
37 # escape taken from twisted/web/microdom.py | |
agable
2013/08/01 17:52:44
nit: throughout, make #-style comments full senten
Mike Stip (use stip instead)
2013/08/29 19:56:46
Done.
| |
38 # order is important | |
39 HTML_ESCAPE_CHARS = (('&', '&'), # don't add any entities before this one | |
agable
2013/08/01 17:52:44
Weird to have this between _GenBuildBox and _GenSt
| |
40 ('<', '<'), | |
41 ('>', '>'), | |
42 ('"', '"')) | |
43 | |
44 | |
45 def escape(text, chars=HTML_ESCAPE_CHARS): | |
46 "Escape a few XML special chars with XML entities." | |
agable
2013/08/01 17:52:44
Make docstring.
| |
47 for s, h in chars: | |
48 text = text.replace(s, h) | |
49 return text | |
50 | |
51 | |
52 def _GenStepBox(stepstatus, styles): | |
53 """Generates a box for one step.""" | |
54 class_ = Results[stepstatus['results']] | |
55 # class == running or results | |
agable
2013/08/01 17:52:44
What does this mean? class_ will be one of "succes
| |
56 style = '' | |
57 if class_ and class_ in styles: | |
agable
2013/08/01 17:52:44
class_ is guaranteed to have a value, unless the i
Mike Stip (use stip instead)
2013/08/29 19:56:46
converted to jinja, but will clean up
| |
58 style = styles[class_] | |
59 text = stepstatus['text'] or [] | |
60 text = text[:] | |
61 for steplog in stepstatus['logs']: | |
62 logname = steplog[0] | |
63 url = steplog[1] | |
64 text.append('<a href="%s">%s</a>' % (url, escape(logname))) | |
65 for urlname, target in stepstatus['urls'].iteritems(): | |
66 text.append('<a href="%s">%s</a>' % (target, escape(urlname))) | |
67 fmt = '<tr><td style="%s">%s</td></tr>' | |
68 return fmt % (style, '<br/>'.join(text)) | |
69 | |
70 | |
71 class MailTemplate(object): | |
72 """Encapsulates a buildbot status email.""" | |
73 | |
74 # Extracted from | |
75 # http://src.chromium.org/svn/trunk/tools/buildbot/master.chromium/ | |
76 # public_html/buildbot.css | |
77 DEFAULT_STYLES = { | |
78 'BuildStep': '', | |
79 'start': ('color: #666666; background-color: #fffc6c;' | |
80 'border-color: #C5C56D;'), | |
81 'success': ('color: #FFFFFF; background-color: #8fdf5f; ' | |
82 'border-color: #4F8530;'), | |
83 'failure': ('color: #FFFFFF; background-color: #e98080; ' | |
84 'border-color: #A77272;'), | |
85 'warnings': ('color: #FFFFFF; background-color: #ffc343; ' | |
86 'border-color: #C29D46;'), | |
87 'exception': ('color: #FFFFFF; background-color: #e0b0ff; ' | |
88 'border-color: #ACA0B3;'), | |
89 'offline': ('color: #FFFFFF; background-color: #e0b0ff; ' | |
90 'border-color: #ACA0B3;'), | |
91 } | |
92 | |
93 # Generate a HTML table looking like the waterfall. | |
94 # WARNING: Gmail ignores embedded CSS style. I don't know how to fix that so | |
95 # meanwhile, I just won't embedded the CSS style. | |
96 html_template = ( | |
agable
2013/08/01 17:52:44
Might be a good idea to document each of these tem
| |
97 """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> | |
98 <html xmlns="http://www.w3.org/1999/xhtml"> | |
99 <head> | |
100 <title>%s</title> | |
101 </head> | |
102 <body> | |
103 <a href="%s">%s</a><p> | |
104 %s<p> | |
105 <a href="%s">%s</a><p> | |
106 Revision: %s<br> | |
107 """ | |
108 ) | |
109 | |
110 # Simpler text content for non-html aware clients. | |
111 text_template = ( | |
112 """%s | |
113 | |
114 %s | |
115 | |
116 %swaterfall?builder=%s | |
117 | |
118 --=> %s <=-- | |
119 | |
120 Revision: %s | |
121 Blame list: %s | |
122 | |
123 Buildbot waterfall: http://build.chromium.org/ | |
124 """ | |
125 ) | |
126 | |
127 change_tmpl = ( | |
128 """ | |
129 <p>Changed by: <b>%(who)s</b><br /> | |
130 Changed at: <b>%(at)s</b><br /> | |
131 %(repository)s | |
132 %(branch)s | |
133 %(revision)s | |
134 <br /> | |
135 | |
136 Changed files: | |
137 %(files)s | |
138 | |
139 Comments: | |
140 %(comments)s | |
141 | |
142 Properties: | |
143 %(properties)s | |
144 </p> | |
145 """ | |
146 ) | |
147 | |
148 status_header = 'Automatically closing tree for "%(steps)s" on "%(builder)s"' | |
149 | |
150 def __init__(self, waterfall_url, build_url, | |
151 project_name, | |
152 fromaddr, | |
153 reply_to=None, | |
154 subject='buildbot %(result)s in %(projectName)s on %(builder)s, ' | |
155 'revision %(revision)s'): | |
156 | |
157 self.reply_to = reply_to | |
158 self.fromaddr = fromaddr | |
159 self.subject = subject | |
160 self.waterfall_url = waterfall_url | |
161 self.build_url = build_url | |
162 self.project_name = project_name | |
163 | |
164 @staticmethod | |
165 def UL(lst): | |
agable
2013/08/01 17:52:44
docstrings even for ported methods.
agable
2013/08/01 17:52:44
It's a little weird to have UL as a @staticmethod
| |
166 # from third_party/twisted_10_2/twisted/web/html.py | |
agable
2013/08/01 17:52:44
add tools/build/ to beginning of port paths, since
| |
167 io = cStringIO.StringIO() | |
agable
2013/08/01 17:52:44
Is this necessary? What advantage does this bring
| |
168 io.write("<ul>\n") | |
169 for el in lst: | |
170 io.write("<li> %s</li>\n" % el) | |
171 io.write("</ul>") | |
172 return io.getvalue() | |
173 | |
174 def changeToHTML(self, change): | |
agable
2013/08/01 17:52:44
genChangeHTML?
| |
175 # Ported from third_party/buildbot_8_4p1/buildbot/changes.py. | |
176 | |
177 links = [] | |
178 for f in change['files']: | |
179 if f['url'] is not None: | |
180 # could get confused | |
181 links.append('<a href="%s"><b>%s</b></a>' % ( | |
182 f['url'], f['name'])) | |
183 else: | |
184 links.append('<b>%s</b>' % f['name']) | |
185 | |
186 revision = '' | |
187 if change['revision']: | |
188 if change.get('revlink'): | |
189 revision = 'Revision: <a href="%s"><b>%s</b></a>\n' % ( | |
190 change['revlink'], change['revision']) | |
191 else: | |
192 revision = "Revision: <b>%s</b><br />\n" % change['revision'] | |
193 | |
194 repository = '' | |
195 if change['repository']: | |
196 repository = "Repository: <b>%s</b><br />\n" % change['repository'] | |
197 | |
198 branch = '' | |
199 if change['branch']: | |
200 branch = "Branch: <b>%s</b><br />\n" % change['branch'] | |
201 | |
202 properties = [] | |
203 for prop in change['properties']: | |
204 properties.append("%s: %s<br />" % (prop[0], prop[1])) | |
205 | |
206 kwargs = { 'who' : escape(change['who']), | |
207 'at' : change['at'], | |
208 'files' : self.UL(links) + '\n', | |
209 'repository': repository, | |
210 'revision' : revision, | |
211 'branch' : branch, | |
212 'comments' : '<pre>' + escape(change['comments']) + '</pre>', | |
213 'properties': self.UL(properties) + '\n' } | |
214 return self.change_tmpl % kwargs | |
215 | |
216 def genMessageContent(self, build_status): | |
217 builder_name = build_status['builderName'] | |
218 us_steps = ','.join(build_status['unsatisfied']) | |
219 revisions_list = build_status['revisions'] | |
220 status_text = self.status_header % { | |
221 'builder': builder_name, | |
222 'steps': us_steps, | |
223 } | |
224 # Use the first line as a title. | |
225 status_title = status_text.split('\n', 1)[0] | |
226 blame_list = ','.join(build_status['blamelist']) | |
227 revisions_string = '' | |
228 latest_revision = 0 | |
229 if revisions_list: | |
230 revisions_string = ', '.join([str(rev) for rev in revisions_list]) | |
231 latest_revision = max([rev for rev in revisions_list]) | |
232 if build_status['result'] == FAILURE: | |
233 result = 'failure' | |
234 else: | |
235 result = 'warning' | |
236 | |
237 html_content = self.html_template % (status_title, self.waterfall_url, | |
238 self.waterfall_url, | |
239 status_text.replace('\n', "<br>\n"), self.build_url, | |
240 self.build_url, revisions_string) | |
241 | |
242 # Only include the blame list if relevant. | |
243 html_content += " Blame list: %s<p>\n" % blame_list | |
244 | |
245 build_boxes = [_GenBuildBox(build_status, self.waterfall_url, | |
246 self.DEFAULT_STYLES)] | |
247 build_boxes.extend([_GenStepBox(step, | |
248 self.DEFAULT_STYLES) | |
249 for step in build_status['steps'] | |
250 if step['started'] and step['text']]) | |
251 table_content = ''.join(build_boxes) | |
252 html_content += ( | |
253 ('<table style="border-spacing: 1px 1px; font-weight: bold;' | |
254 ' padding: 3px 0px 3px 0px; text-align: center;">\n') + | |
255 table_content + | |
256 '</table>\n') | |
257 | |
258 html_content += "<p>" | |
259 # Add the change list descriptions. getChanges() returns a tuple of | |
260 # buildbot.changes.changes.Change | |
261 for change in build_status['changes']: | |
262 html_content += self.changeToHTML(change) | |
263 html_content += "</body>\n</html>" | |
264 | |
265 text_content = self.text_template % (status_title, | |
266 self.build_url, | |
267 urllib.quote(self.waterfall_url, '/:'), | |
268 urllib.quote(builder_name), | |
269 status_text, | |
270 revisions_string, | |
271 blame_list) | |
272 | |
273 subject = self.subject % { | |
274 'result': result, | |
275 'projectName': self.project_name, | |
276 'builder': builder_name, | |
277 'reason': build_status['reason'], | |
278 'revision': str(latest_revision), | |
279 'buildnumber': str(build_status['number']), | |
280 } | |
281 | |
282 return text_content, html_content, subject | |
OLD | NEW |