Chromium Code Reviews| 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 |