Chromium Code Reviews| Index: gatekeeper_mailer.py |
| diff --git a/gatekeeper_mailer.py b/gatekeeper_mailer.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..a1e179125cb278bec2d6405dd095aa303024f6e4 |
| --- /dev/null |
| +++ b/gatekeeper_mailer.py |
| @@ -0,0 +1,282 @@ |
| +#!/usr/bin/env python |
| +# Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +"""Provides mailer support for gatekeeper_ng. |
| + |
| +This module containes mail templates to notify tree watchers |
| +when the tree is closed. |
| +""" |
| + |
| +import cStringIO |
| +import urllib |
| + |
| +# From buildbot's results.py. |
| +SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY = range(6) |
| +Results = ["success", "warnings", "failure", "skipped", "exception", "retry"] |
| + |
| + |
| +def _GenBuildBox(buildstatus, waterfall_url, styles): |
| + """Generates a box for one build.""" |
| + class_ = Results[buildstatus['result']] |
| + style = '' |
| + if class_ and class_ in styles: |
| + style = styles[class_] |
| + reason = buildstatus['reason'] |
| + 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.
|
| + waterfall_url, |
| + urllib.quote(buildstatus['builderName'], safe=''), |
| + buildstatus['number']) |
| + fmt = ('<tr><td style="%s"><a title="Reason: %s" href="%s">' |
| + 'Build %d' |
| + '</a></td></tr>') |
| + return fmt % (style, reason, url, buildstatus['number']) |
| + |
| + |
| +# 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.
|
| +# order is important |
| +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
|
| + ('<', '<'), |
| + ('>', '>'), |
| + ('"', '"')) |
| + |
| + |
| +def escape(text, chars=HTML_ESCAPE_CHARS): |
| + "Escape a few XML special chars with XML entities." |
|
agable
2013/08/01 17:52:44
Make docstring.
|
| + for s, h in chars: |
| + text = text.replace(s, h) |
| + return text |
| + |
| + |
| +def _GenStepBox(stepstatus, styles): |
| + """Generates a box for one step.""" |
| + class_ = Results[stepstatus['results']] |
| + # class == running or results |
|
agable
2013/08/01 17:52:44
What does this mean? class_ will be one of "succes
|
| + style = '' |
| + 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
|
| + style = styles[class_] |
| + text = stepstatus['text'] or [] |
| + text = text[:] |
| + for steplog in stepstatus['logs']: |
| + logname = steplog[0] |
| + url = steplog[1] |
| + text.append('<a href="%s">%s</a>' % (url, escape(logname))) |
| + for urlname, target in stepstatus['urls'].iteritems(): |
| + text.append('<a href="%s">%s</a>' % (target, escape(urlname))) |
| + fmt = '<tr><td style="%s">%s</td></tr>' |
| + return fmt % (style, '<br/>'.join(text)) |
| + |
| + |
| +class MailTemplate(object): |
| + """Encapsulates a buildbot status email.""" |
| + |
| + # Extracted from |
| + # http://src.chromium.org/svn/trunk/tools/buildbot/master.chromium/ |
| + # public_html/buildbot.css |
| + DEFAULT_STYLES = { |
| + 'BuildStep': '', |
| + 'start': ('color: #666666; background-color: #fffc6c;' |
| + 'border-color: #C5C56D;'), |
| + 'success': ('color: #FFFFFF; background-color: #8fdf5f; ' |
| + 'border-color: #4F8530;'), |
| + 'failure': ('color: #FFFFFF; background-color: #e98080; ' |
| + 'border-color: #A77272;'), |
| + 'warnings': ('color: #FFFFFF; background-color: #ffc343; ' |
| + 'border-color: #C29D46;'), |
| + 'exception': ('color: #FFFFFF; background-color: #e0b0ff; ' |
| + 'border-color: #ACA0B3;'), |
| + 'offline': ('color: #FFFFFF; background-color: #e0b0ff; ' |
| + 'border-color: #ACA0B3;'), |
| + } |
| + |
| + # Generate a HTML table looking like the waterfall. |
| + # WARNING: Gmail ignores embedded CSS style. I don't know how to fix that so |
| + # meanwhile, I just won't embedded the CSS style. |
| + html_template = ( |
|
agable
2013/08/01 17:52:44
Might be a good idea to document each of these tem
|
| +"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
| +<html xmlns="http://www.w3.org/1999/xhtml"> |
| +<head> |
| + <title>%s</title> |
| +</head> |
| +<body> |
| + <a href="%s">%s</a><p> |
| + %s<p> |
| + <a href="%s">%s</a><p> |
| + Revision: %s<br> |
| +""" |
| +) |
| + |
| + # Simpler text content for non-html aware clients. |
| + text_template = ( |
| +"""%s |
| + |
| +%s |
| + |
| +%swaterfall?builder=%s |
| + |
| +--=> %s <=-- |
| + |
| +Revision: %s |
| +Blame list: %s |
| + |
| +Buildbot waterfall: http://build.chromium.org/ |
| +""" |
| +) |
| + |
| + change_tmpl = ( |
| +""" |
| +<p>Changed by: <b>%(who)s</b><br /> |
| +Changed at: <b>%(at)s</b><br /> |
| +%(repository)s |
| +%(branch)s |
| +%(revision)s |
| +<br /> |
| + |
| +Changed files: |
| +%(files)s |
| + |
| +Comments: |
| +%(comments)s |
| + |
| +Properties: |
| +%(properties)s |
| +</p> |
| +""" |
| +) |
| + |
| + status_header = 'Automatically closing tree for "%(steps)s" on "%(builder)s"' |
| + |
| + def __init__(self, waterfall_url, build_url, |
| + project_name, |
| + fromaddr, |
| + reply_to=None, |
| + subject='buildbot %(result)s in %(projectName)s on %(builder)s, ' |
| + 'revision %(revision)s'): |
| + |
| + self.reply_to = reply_to |
| + self.fromaddr = fromaddr |
| + self.subject = subject |
| + self.waterfall_url = waterfall_url |
| + self.build_url = build_url |
| + self.project_name = project_name |
| + |
| + @staticmethod |
| + 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
|
| + # 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
|
| + io = cStringIO.StringIO() |
|
agable
2013/08/01 17:52:44
Is this necessary? What advantage does this bring
|
| + io.write("<ul>\n") |
| + for el in lst: |
| + io.write("<li> %s</li>\n" % el) |
| + io.write("</ul>") |
| + return io.getvalue() |
| + |
| + def changeToHTML(self, change): |
|
agable
2013/08/01 17:52:44
genChangeHTML?
|
| + # Ported from third_party/buildbot_8_4p1/buildbot/changes.py. |
| + |
| + links = [] |
| + for f in change['files']: |
| + if f['url'] is not None: |
| + # could get confused |
| + links.append('<a href="%s"><b>%s</b></a>' % ( |
| + f['url'], f['name'])) |
| + else: |
| + links.append('<b>%s</b>' % f['name']) |
| + |
| + revision = '' |
| + if change['revision']: |
| + if change.get('revlink'): |
| + revision = 'Revision: <a href="%s"><b>%s</b></a>\n' % ( |
| + change['revlink'], change['revision']) |
| + else: |
| + revision = "Revision: <b>%s</b><br />\n" % change['revision'] |
| + |
| + repository = '' |
| + if change['repository']: |
| + repository = "Repository: <b>%s</b><br />\n" % change['repository'] |
| + |
| + branch = '' |
| + if change['branch']: |
| + branch = "Branch: <b>%s</b><br />\n" % change['branch'] |
| + |
| + properties = [] |
| + for prop in change['properties']: |
| + properties.append("%s: %s<br />" % (prop[0], prop[1])) |
| + |
| + kwargs = { 'who' : escape(change['who']), |
| + 'at' : change['at'], |
| + 'files' : self.UL(links) + '\n', |
| + 'repository': repository, |
| + 'revision' : revision, |
| + 'branch' : branch, |
| + 'comments' : '<pre>' + escape(change['comments']) + '</pre>', |
| + 'properties': self.UL(properties) + '\n' } |
| + return self.change_tmpl % kwargs |
| + |
| + def genMessageContent(self, build_status): |
| + builder_name = build_status['builderName'] |
| + us_steps = ','.join(build_status['unsatisfied']) |
| + revisions_list = build_status['revisions'] |
| + status_text = self.status_header % { |
| + 'builder': builder_name, |
| + 'steps': us_steps, |
| + } |
| + # Use the first line as a title. |
| + status_title = status_text.split('\n', 1)[0] |
| + blame_list = ','.join(build_status['blamelist']) |
| + revisions_string = '' |
| + latest_revision = 0 |
| + if revisions_list: |
| + revisions_string = ', '.join([str(rev) for rev in revisions_list]) |
| + latest_revision = max([rev for rev in revisions_list]) |
| + if build_status['result'] == FAILURE: |
| + result = 'failure' |
| + else: |
| + result = 'warning' |
| + |
| + html_content = self.html_template % (status_title, self.waterfall_url, |
| + self.waterfall_url, |
| + status_text.replace('\n', "<br>\n"), self.build_url, |
| + self.build_url, revisions_string) |
| + |
| + # Only include the blame list if relevant. |
| + html_content += " Blame list: %s<p>\n" % blame_list |
| + |
| + build_boxes = [_GenBuildBox(build_status, self.waterfall_url, |
| + self.DEFAULT_STYLES)] |
| + build_boxes.extend([_GenStepBox(step, |
| + self.DEFAULT_STYLES) |
| + for step in build_status['steps'] |
| + if step['started'] and step['text']]) |
| + table_content = ''.join(build_boxes) |
| + html_content += ( |
| + ('<table style="border-spacing: 1px 1px; font-weight: bold;' |
| + ' padding: 3px 0px 3px 0px; text-align: center;">\n') + |
| + table_content + |
| + '</table>\n') |
| + |
| + html_content += "<p>" |
| + # Add the change list descriptions. getChanges() returns a tuple of |
| + # buildbot.changes.changes.Change |
| + for change in build_status['changes']: |
| + html_content += self.changeToHTML(change) |
| + html_content += "</body>\n</html>" |
| + |
| + text_content = self.text_template % (status_title, |
| + self.build_url, |
| + urllib.quote(self.waterfall_url, '/:'), |
| + urllib.quote(builder_name), |
| + status_text, |
| + revisions_string, |
| + blame_list) |
| + |
| + subject = self.subject % { |
| + 'result': result, |
| + 'projectName': self.project_name, |
| + 'builder': builder_name, |
| + 'reason': build_status['reason'], |
| + 'revision': str(latest_revision), |
| + 'buildnumber': str(build_status['number']), |
| + } |
| + |
| + return text_content, html_content, subject |