| Index: third_party/buildbot_7_12/buildbot/process/mtrlogobserver.py
|
| diff --git a/third_party/buildbot_7_12/buildbot/process/mtrlogobserver.py b/third_party/buildbot_7_12/buildbot/process/mtrlogobserver.py
|
| deleted file mode 100644
|
| index bf433f67a4bd98e0fa82fab96c8d6e177d77faa1..0000000000000000000000000000000000000000
|
| --- a/third_party/buildbot_7_12/buildbot/process/mtrlogobserver.py
|
| +++ /dev/null
|
| @@ -1,472 +0,0 @@
|
| -import sys
|
| -import re
|
| -import exceptions
|
| -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."""
|
| -
|
| - 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',)
|
| -
|
| - self.addFactoryArguments(dbpool=self.dbpool,
|
| - test_type=self.test_type,
|
| - test_info=self.test_info,
|
| - autoCreateTables=self.autoCreateTables,
|
| - textLimit=self.textLimit,
|
| - testNameLimit=self.testNameLimit,
|
| - parallel=self.parallel,
|
| - mtr_subdir=self.mtr_subdir)
|
| -
|
| - def start(self):
|
| - properties = self.build.getProperties()
|
| - subdir = properties.render(self.mtr_subdir)
|
| -
|
| - # 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, 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 = None
|
| - try:
|
| - revision = self.getProperty("got_revision")
|
| - except exceptions.KeyError:
|
| - 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
|
|
|