aboutsummaryrefslogtreecommitdiffstats
path: root/lib/python2.7/site-packages/buildbot-0.8.8-py2.7.egg/buildbot/status/tinderbox.py
blob: 1694c24334e80ac1aa9042514146fed3cf433dc7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# This file is part of Buildbot.  Buildbot is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright Buildbot Team Members


from email.Message import Message
from email.Utils import formatdate

from zope.interface import implements
from twisted.internet import defer

from buildbot import interfaces
from buildbot.status import mail
from buildbot.status.results import SUCCESS, WARNINGS, EXCEPTION, RETRY
from buildbot.steps.shell import WithProperties

import gzip, bz2, base64, re, cStringIO

# TODO: docs, maybe a test of some sort just to make sure it actually imports
# and can format email without raising an exception.

class TinderboxMailNotifier(mail.MailNotifier):
    """This is a Tinderbox status notifier. It can send e-mail to a number of
    different tinderboxes or people. E-mails are sent at the beginning and
    upon completion of each build. It can be configured to send out e-mails
    for only certain builds.

    The most basic usage is as follows::
        TinderboxMailNotifier(fromaddr="buildbot@localhost",
                              tree="MyTinderboxTree",
                              extraRecipients=["tinderboxdaemon@host.org"])

    The builder name (as specified in master.cfg) is used as the "build"
    tinderbox option.

    """
    implements(interfaces.IEmailSender)

    compare_attrs = ["extraRecipients", "fromaddr", "categories", "builders",
                     "addLogs", "relayhost", "subject", "binaryURL", "tree",
                     "logCompression", "errorparser", "columnName",
                     "useChangeTime"]

    def __init__(self, fromaddr, tree, extraRecipients,
                 categories=None, builders=None, relayhost="localhost",
                 subject="buildbot %(result)s in %(builder)s", binaryURL="",
                 logCompression="", errorparser="unix", columnName=None,
                 useChangeTime=False):
        """
        @type  fromaddr: string
        @param fromaddr: the email address to be used in the 'From' header.

        @type  tree: string
        @param tree: The Tinderbox tree to post to.
                     When tree is a WithProperties instance it will be
                     interpolated as such. See WithProperties for more detail

        @type  extraRecipients: tuple of string
        @param extraRecipients: E-mail addresses of recipients. This should at
                                least include the tinderbox daemon.

        @type  categories: list of strings
        @param categories: a list of category names to serve status
                           information for. Defaults to None (all
                           categories). Use either builders or categories,
                           but not both.

        @type  builders: list of strings
        @param builders: a list of builder names for which mail should be
                         sent. Defaults to None (send mail for all builds).
                         Use either builders or categories, but not both.

        @type  relayhost: string
        @param relayhost: the host to which the outbound SMTP connection
                          should be made. Defaults to 'localhost'

        @type  subject: string
        @param subject: a string to be used as the subject line of the message.
                        %(builder)s will be replaced with the name of the
                        %builder which provoked the message.
                        This parameter is not significant for the tinderbox
                        daemon.

        @type  binaryURL: string
        @param binaryURL: If specified, this should be the location where final
                          binary for a build is located.
                          (ie. http://www.myproject.org/nightly/08-08-2006.tgz)
                          It will be posted to the Tinderbox.

        @type  logCompression: string
        @param logCompression: The type of compression to use on the log.
                               Valid options are"bzip2" and "gzip". gzip is
                               only known to work on Python 2.4 and above.

        @type  errorparser: string
        @param errorparser: The error parser that the Tinderbox server
                            should use when scanning the log file.
                            Default is "unix".

        @type  columnName: string
        @param columnName: When columnName is None, use the buildername as
                           the Tinderbox column name. When columnName is a
                           string this exact string will be used for all
                           builders that this TinderboxMailNotifier cares
                           about (not recommended). When columnName is a
                           WithProperties instance it will be interpolated
                           as such. See WithProperties for more detail.
        @type  useChangeTime: bool
        @param useChangeTime: When True, the time of the first Change for a
                              build is used as the builddate. When False,
                              the current time is used as the builddate.
        """

        mail.MailNotifier.__init__(self, fromaddr, categories=categories,
                                   builders=builders, relayhost=relayhost,
                                   subject=subject,
                                   extraRecipients=extraRecipients,
                                   sendToInterestedUsers=False)
        assert isinstance(tree, basestring) \
            or isinstance(tree, WithProperties), \
            "tree must be a string or a WithProperties instance"
        self.tree = tree
        self.binaryURL = binaryURL
        self.logCompression = logCompression
        self.errorparser = errorparser
        self.useChangeTime = useChangeTime
        assert columnName is None or type(columnName) is str \
            or isinstance(columnName, WithProperties), \
            "columnName must be None, a string, or a WithProperties instance"
        self.columnName = columnName

    def buildStarted(self, name, build):
        builder = build.getBuilder()
        if self.builders is not None and name not in self.builders:
            return # ignore this Build
        if self.categories is not None and \
                builder.category not in self.categories:
            return # ignore this build
        self.buildMessage(name, build, "building")

    @defer.inlineCallbacks
    def buildMessage(self, name, build, results):
        text = ""
        res = ""
        # shortform
        t = "tinderbox:"

        tree = yield build.render(self.tree)
        text += "%s tree: %s\n" % (t, tree)

        # the start time
        # getTimes() returns a fractioned time that tinderbox doesn't understand
        builddate = int(build.getTimes()[0])
        # attempt to pull a Change time from this Build's Changes.
        # if that doesn't work, fall back on the current time
        if self.useChangeTime:
            try:
                builddate = build.getChanges()[-1].when
            except:
                pass
        text += "%s builddate: %s\n" % (t, builddate)
        text += "%s status: " % t

        if results == "building":
            res = "building"
            text += res
        elif results == SUCCESS:
            res = "success"
            text += res
        elif results == WARNINGS:
            res = "testfailed"
            text += res
        elif results in (EXCEPTION, RETRY):
            res = "exception"
            text += res
        else:
            res += "busted"
            text += res

        text += "\n";

        if self.columnName is None:
            # use the builder name
            text += "%s build: %s\n" % (t, name)
        else:
            columnName = yield build.render(self.columnName)
            text += "%s build: %s\n" % (t, columnName)
        text += "%s errorparser: %s\n" % (t, self.errorparser)

        # if the build just started...
        if results == "building":
            text += "%s END\n" % t
        # if the build finished...
        else:
            text += "%s binaryurl: %s\n" % (t, self.binaryURL)
            text += "%s logcompression: %s\n" % (t, self.logCompression)

            # logs will always be appended
            logEncoding = ""
            tinderboxLogs = ""
            for bs in build.getSteps():
                # Make sure that shortText is a regular string, so that bad
                # data in the logs don't generate UnicodeDecodeErrors
                shortText = "%s\n" % ' '.join(bs.getText()).encode('ascii', 'replace')

                # ignore steps that haven't happened
                if not re.match(".*[^\s].*", shortText):
                    continue
                # we ignore TinderboxPrint's here so we can do things like:
                # ShellCommand(command=['echo', 'TinderboxPrint:', ...])
                if re.match(".+TinderboxPrint.*", shortText):
                    shortText = shortText.replace("TinderboxPrint",
                                                  "Tinderbox Print")
                logs = bs.getLogs()

                tinderboxLogs += "======== BuildStep started ========\n"
                tinderboxLogs += shortText
                tinderboxLogs += "=== Output ===\n"
                for log in logs:
                    logText = log.getTextWithHeaders()
                    # Because we pull in the log headers here we have to ignore
                    # some of them. Basically, if we're TinderboxPrint'ing in
                    # a ShellCommand, the only valid one(s) are at the start
                    # of a line. The others are prendeded by whitespace, quotes,
                    # or brackets/parentheses
                    for line in logText.splitlines():
                        if re.match(".+TinderboxPrint.*", line):
                            line = line.replace("TinderboxPrint",
                                                "Tinderbox Print")
                        tinderboxLogs += line + "\n"

                tinderboxLogs += "=== Output ended ===\n"
                tinderboxLogs += "======== BuildStep ended ========\n"

            if self.logCompression == "bzip2":
                cLog = bz2.compress(tinderboxLogs)
                tinderboxLogs = base64.encodestring(cLog)
                logEncoding = "base64"
            elif self.logCompression == "gzip":
                cLog = cStringIO.StringIO()
                gz = gzip.GzipFile(mode="w", fileobj=cLog)
                gz.write(tinderboxLogs)
                gz.close()
                cLog = cLog.getvalue()
                tinderboxLogs = base64.encodestring(cLog)
                logEncoding = "base64"

            text += "%s logencoding: %s\n" % (t, logEncoding)
            text += "%s END\n\n" % t
            text += tinderboxLogs
            text += "\n"

        m = Message()
        m.set_payload(text)

        m['Date'] = formatdate(localtime=True)
        m['Subject'] = self.subject % { 'result': res,
                                        'builder': name,
                                        }
        m['From'] = self.fromaddr
        # m['To'] is added later

        d = defer.DeferredList([])
        d.addCallback(self._gotRecipients, self.extraRecipients, m)
        defer.returnValue((yield d))