aboutsummaryrefslogtreecommitdiffstats
path: root/lib/python2.7/site-packages/buildbot-0.8.8-py2.7.egg/buildbot/process/mtrlogobserver.py
blob: a49b90e2129c5555a2d6fb6205181e5203bc1529 (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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# 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

import sys
import re
from twisted.python import log
from twisted.internet import defer
from twisted.enterprise import adbapi
from buildbot.process.buildstep import LogLineObserver
from buildbot.steps.shell import Test

class EqConnectionPool(adbapi.ConnectionPool):
    """This class works the same way as
twisted.enterprise.adbapi.ConnectionPool. But it adds the ability to
compare connection pools for equality (by comparing the arguments
passed to the constructor).

This is useful when passing the ConnectionPool to a BuildStep, as
otherwise Buildbot will consider the buildstep (and hence the
containing buildfactory) to have changed every time the configuration
is reloaded.

It also sets some defaults differently from adbapi.ConnectionPool that
are more suitable for use in MTR.
"""
    def __init__(self, *args, **kwargs):
        self._eqKey = (args, kwargs)
        return adbapi.ConnectionPool.__init__(self,
                                              cp_reconnect=True, cp_min=1, cp_max=3,
                                              *args, **kwargs)

    def __eq__(self, other):
        if isinstance(other, EqConnectionPool):
            return self._eqKey == other._eqKey
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)


class MtrTestFailData:
    def __init__(self, testname, variant, result, info, text, callback):
        self.testname = testname
        self.variant = variant
        self.result = result
        self.info = info
        self.text = text
        self.callback = callback

    def add(self, line):
        self.text+= line

    def fireCallback(self):
        return self.callback(self.testname, self.variant, self.result, self.info, self.text)


class MtrLogObserver(LogLineObserver):
    """
    Class implementing a log observer (can be passed to
    BuildStep.addLogObserver().

    It parses the output of mysql-test-run.pl as used in MySQL,
    MariaDB, Drizzle, etc.

    It counts number of tests run and uses it to provide more accurate
    completion estimates.

    It parses out test failures from the output and summarises the results on
    the Waterfall page. It also passes the information to methods that can be
    overridden in a subclass to do further processing on the information."""

    _line_re = re.compile(r"^([-._0-9a-zA-z]+)( '[-_ a-zA-Z]+')?\s+(w[0-9]+\s+)?\[ (fail|pass) \]\s*(.*)$")
    _line_re2 = re.compile(r"^[-._0-9a-zA-z]+( '[-_ a-zA-Z]+')?\s+(w[0-9]+\s+)?\[ [-a-z]+ \]")
    _line_re3 = re.compile(r"^\*\*\*Warnings generated in error logs during shutdown after running tests: (.*)")
    _line_re4 = re.compile(r"^The servers were restarted [0-9]+ times$")
    _line_re5 = re.compile(r"^Only\s+[0-9]+\s+of\s+[0-9]+\s+completed.$")

    def __init__(self, textLimit=5, testNameLimit=16, testType=None):
        self.textLimit = textLimit
        self.testNameLimit = testNameLimit
        self.testType = testType
        self.numTests = 0
        self.testFail = None
        self.failList = []
        self.warnList = []
        LogLineObserver.__init__(self)

    def setLog(self, loog):
        LogLineObserver.setLog(self, loog)
        d= loog.waitUntilFinished()
        d.addCallback(lambda l: self.closeTestFail())

    def outLineReceived(self, line):
        stripLine = line.strip("\r\n")
        m = self._line_re.search(stripLine)
        if m:
            testname, variant, worker, result, info = m.groups()
            self.closeTestFail()
            self.numTests += 1
            self.step.setProgress('tests', self.numTests)

            if result == "fail":
                if variant == None:
                    variant = ""
                else:
                    variant = variant[2:-1]
                self.openTestFail(testname, variant, result, info, stripLine + "\n")

        else:
            m = self._line_re3.search(stripLine)
            if m:
                stuff = m.group(1)
                self.closeTestFail()
                testList = stuff.split(" ")
                self.doCollectWarningTests(testList)

            elif (self._line_re2.search(stripLine) or
                  self._line_re4.search(stripLine) or
                  self._line_re5.search(stripLine) or
                  stripLine == "Test suite timeout! Terminating..." or
                  stripLine.startswith("mysql-test-run: *** ERROR: Not all tests completed") or
                  (stripLine.startswith("------------------------------------------------------------")
                   and self.testFail != None)):
                self.closeTestFail()

            else:
                self.addTestFailOutput(stripLine + "\n")

    def openTestFail(self, testname, variant, result, info, line):
        self.testFail = MtrTestFailData(testname, variant, result, info, line, self.doCollectTestFail)

    def addTestFailOutput(self, line):
        if self.testFail != None:
            self.testFail.add(line)

    def closeTestFail(self):
        if self.testFail != None:
            self.testFail.fireCallback()
            self.testFail = None

    def addToText(self, src, dst):
        lastOne = None
        count = 0
        for t in src:
            if t != lastOne:
                dst.append(t)
                count += 1
                if count >= self.textLimit:
                    break

    def makeText(self, done):
        if done:
            text = ["test"]
        else:
            text = ["testing"]
        if self.testType:
            text.append(self.testType)
        fails = self.failList[:]
        fails.sort()
        self.addToText(fails, text)
        warns = self.warnList[:]
        warns.sort()
        self.addToText(warns, text)
        return text

    # Update waterfall status.
    def updateText(self):
        self.step.step_status.setText(self.makeText(False))

    strip_re = re.compile(r"^[a-z]+\.")

    def displayTestName(self, testname):

        displayTestName = self.strip_re.sub("", testname)

        if len(displayTestName) > self.testNameLimit:
            displayTestName = displayTestName[:(self.testNameLimit-2)] + "..."
        return displayTestName

    def doCollectTestFail(self, testname, variant, result, info, text):
        self.failList.append("F:" + self.displayTestName(testname))
        self.updateText()
        self.collectTestFail(testname, variant, result, info, text)

    def doCollectWarningTests(self, testList):
        for t in testList:
            self.warnList.append("W:" + self.displayTestName(t))
        self.updateText()
        self.collectWarningTests(testList)

    # These two methods are overridden to actually do something with the data.
    def collectTestFail(self, testname, variant, result, info, text):
        pass
    def collectWarningTests(self, testList):
        pass

class MTR(Test):
    """
    Build step that runs mysql-test-run.pl, as used in MySQL, Drizzle,
    MariaDB, etc.

    It uses class MtrLogObserver to parse test results out from the
    output of mysql-test-run.pl, providing better completion time
    estimates and summarising test failures on the waterfall page.

    It also provides access to mysqld server error logs from the test
    run to help debugging any problems.

    Optionally, it can insert into a database data about the test run,
    including details of any test failures.

    Parameters:

    textLimit
        Maximum number of test failures to show on the waterfall page
        (to not flood the page in case of a large number of test
        failures. Defaults to 5.

    testNameLimit
        Maximum length of test names to show unabbreviated in the
        waterfall page, to avoid excessive column width. Defaults to 16.

    parallel
        Value of --parallel option used for mysql-test-run.pl (number
        of processes used to run the test suite in parallel). Defaults
        to 4. This is used to determine the number of server error log
        files to download from the slave. Specifying a too high value
        does not hurt (as nonexisting error logs will be ignored),
        however if using --parallel value greater than the default it
        needs to be specified, or some server error logs will be
        missing.

    dbpool
        An instance of twisted.enterprise.adbapi.ConnectionPool, or None.
        Defaults to None. If specified, results are inserted into the database
        using the ConnectionPool.

        The class process.mtrlogobserver.EqConnectionPool subclass of
        ConnectionPool can be useful to pass as value for dbpool, to
        avoid having config reloads think the Buildstep is changed
        just because it gets a new ConnectionPool instance (even
        though connection parameters are unchanged).

    autoCreateTables
        Boolean, defaults to False. If True (and dbpool is specified), the
        necessary database tables will be created automatically if they do
        not exist already. Alternatively, the tables can be created manually
        from the SQL statements found in the mtrlogobserver.py source file.

    test_type
    test_info
        Two descriptive strings that will be inserted in the database tables if
        dbpool is specified. The test_type string, if specified, will also
        appear on the waterfall page."""

    renderables = [ 'mtr_subdir' ]

    def __init__(self, dbpool=None, test_type=None, test_info="",
                 description=None, descriptionDone=None,
                 autoCreateTables=False, textLimit=5, testNameLimit=16,
                 parallel=4, logfiles = {}, lazylogfiles = True,
                 warningPattern="MTR's internal check of the test case '.*' failed",
                 mtr_subdir="mysql-test", **kwargs):

        if description is None:
            description = ["testing"]
            if test_type:
                description.append(test_type)
        if descriptionDone is None:
            descriptionDone = ["test"]
            if test_type:
                descriptionDone.append(test_type)
        Test.__init__(self, logfiles=logfiles, lazylogfiles=lazylogfiles,
                      description=description, descriptionDone=descriptionDone,
                      warningPattern=warningPattern, **kwargs)
        self.dbpool = dbpool
        self.test_type = test_type
        self.test_info = test_info
        self.autoCreateTables = autoCreateTables
        self.textLimit = textLimit
        self.testNameLimit = testNameLimit
        self.parallel = parallel
        self.mtr_subdir = mtr_subdir
        self.progressMetrics += ('tests',)

    def start(self):
        # Add mysql server logfiles.
        for mtr in range(0, self.parallel+1):
            for mysqld in range(1, 4+1):
                if mtr == 0:
                    logname = "mysqld.%d.err" % mysqld
                    filename = "var/log/mysqld.%d.err" % mysqld
                else:
                    logname = "mysqld.%d.err.%d" % (mysqld, mtr)
                    filename = "var/%d/log/mysqld.%d.err" % (mtr, mysqld)
                self.addLogFile(logname, self.mtr_subdir + "/" + filename)

        self.myMtr = self.MyMtrLogObserver(textLimit=self.textLimit,
                                           testNameLimit=self.testNameLimit,
                                           testType=self.test_type)
        self.addLogObserver("stdio", self.myMtr)
        # Insert a row for this test run into the database and set up
        # build properties, then start the command proper.
        d = self.registerInDB()
        d.addCallback(self.afterRegisterInDB)
        d.addErrback(self.failed)

    def getText(self, command, results):
        return self.myMtr.makeText(True)

    def runInteractionWithRetry(self, actionFn, *args, **kw):
        """
        Run a database transaction with dbpool.runInteraction, but retry the
        transaction in case of a temporary error (like connection lost).

        This is needed to be robust against things like database connection
        idle timeouts.

        The passed callable that implements the transaction must be retryable,
        ie. it must not have any destructive side effects in the case where
        an exception is thrown and/or rollback occurs that would prevent it
        from functioning correctly when called again."""

        def runWithRetry(txn, *args, **kw):
            retryCount = 0
            while(True):
                try:
                    return actionFn(txn, *args, **kw)
                except txn.OperationalError:
                    retryCount += 1
                    if retryCount >= 5:
                        raise
                    excType, excValue, excTraceback = sys.exc_info()
                    log.msg("Database transaction failed (caught exception %s(%s)), retrying ..." % (excType, excValue))
                    txn.close()
                    txn.reconnect()
                    txn.reopen()

        return self.dbpool.runInteraction(runWithRetry, *args, **kw)

    def runQueryWithRetry(self, *args, **kw):
        """
        Run a database query, like with dbpool.runQuery, but retry the query in
        case of a temporary error (like connection lost).

        This is needed to be robust against things like database connection
        idle timeouts."""

        def runQuery(txn, *args, **kw):
            txn.execute(*args, **kw)
            return txn.fetchall()

        return self.runInteractionWithRetry(runQuery, *args, **kw)

    def registerInDB(self):
        if self.dbpool:
            return self.runInteractionWithRetry(self.doRegisterInDB)
        else:
            return defer.succeed(0)

    # The real database work is done in a thread in a synchronous way.
    def doRegisterInDB(self, txn):
        # Auto create tables.
        # This is off by default, as it gives warnings in log file
        # about tables already existing (and I did not find the issue
        # important enough to find a better fix).
        if self.autoCreateTables:
            txn.execute("""
CREATE TABLE IF NOT EXISTS test_run(
    id INT PRIMARY KEY AUTO_INCREMENT,
    branch VARCHAR(100),
    revision VARCHAR(32) NOT NULL,
    platform VARCHAR(100) NOT NULL,
    dt TIMESTAMP NOT NULL,
    bbnum INT NOT NULL,
    typ VARCHAR(32) NOT NULL,
    info VARCHAR(255),
    KEY (branch, revision),
    KEY (dt),
    KEY (platform, bbnum)
) ENGINE=innodb
""")
            txn.execute("""
CREATE TABLE IF NOT EXISTS test_failure(
    test_run_id INT NOT NULL,
    test_name VARCHAR(100) NOT NULL,
    test_variant VARCHAR(16) NOT NULL,
    info_text VARCHAR(255),
    failure_text TEXT,
    PRIMARY KEY (test_run_id, test_name, test_variant)
) ENGINE=innodb
""")
            txn.execute("""
CREATE TABLE IF NOT EXISTS test_warnings(
    test_run_id INT NOT NULL,
    list_id INT NOT NULL,
    list_idx INT NOT NULL,
    test_name VARCHAR(100) NOT NULL,
    PRIMARY KEY (test_run_id, list_id, list_idx)
) ENGINE=innodb
""")

        revision = self.getProperty("got_revision")
        if revision is None:
            revision = self.getProperty("revision")
        typ = "mtr"
        if self.test_type:
            typ = self.test_type
        txn.execute("""
INSERT INTO test_run(branch, revision, platform, dt, bbnum, typ, info)
VALUES (%s, %s, %s, CURRENT_TIMESTAMP(), %s, %s, %s)
""", (self.getProperty("branch"), revision,
      self.getProperty("buildername"), self.getProperty("buildnumber"),
      typ, self.test_info))

        return txn.lastrowid

    def afterRegisterInDB(self, insert_id):
        self.setProperty("mtr_id", insert_id)
        self.setProperty("mtr_warn_id", 0)

        Test.start(self)

    def reportError(self, err):
        log.msg("Error in async insert into database: %s" % err)

    class MyMtrLogObserver(MtrLogObserver):
        def collectTestFail(self, testname, variant, result, info, text):
            # Insert asynchronously into database.
            dbpool = self.step.dbpool
            run_id = self.step.getProperty("mtr_id")
            if dbpool == None:
                return defer.succeed(None)
            if variant == None:
                variant = ""
            d = self.step.runQueryWithRetry("""
INSERT INTO test_failure(test_run_id, test_name, test_variant, info_text, failure_text)
VALUES (%s, %s, %s, %s, %s)
""", (run_id, testname, variant, info, text))

            d.addErrback(self.step.reportError)
            return d

        def collectWarningTests(self, testList):
            # Insert asynchronously into database.
            dbpool = self.step.dbpool
            if dbpool == None:
                return defer.succeed(None)
            run_id = self.step.getProperty("mtr_id")
            warn_id = self.step.getProperty("mtr_warn_id")
            self.step.setProperty("mtr_warn_id", warn_id + 1)
            q = ("INSERT INTO test_warnings(test_run_id, list_id, list_idx, test_name) " +
                 "VALUES " + ", ".join(map(lambda x: "(%s, %s, %s, %s)", testList)))
            v = []
            idx = 0
            for t in testList:
                v.extend([run_id, warn_id, idx, t])
                idx = idx + 1
            d = self.step.runQueryWithRetry(q, tuple(v))
            d.addErrback(self.step.reportError)
            return d