Package buildbot :: Package process :: Module mtrlogobserver
[frames] | no frames]

Source Code for Module buildbot.process.mtrlogobserver

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16  import sys 
 17  import re 
 18  from twisted.python import log 
 19  from twisted.internet import defer 
 20  from twisted.enterprise import adbapi 
 21  from buildbot.process.buildstep import LogLineObserver 
 22  from buildbot.steps.shell import Test 
 23   
24 -class EqConnectionPool(adbapi.ConnectionPool):
25 """This class works the same way as 26 twisted.enterprise.adbapi.ConnectionPool. But it adds the ability to 27 compare connection pools for equality (by comparing the arguments 28 passed to the constructor). 29 30 This is useful when passing the ConnectionPool to a BuildStep, as 31 otherwise Buildbot will consider the buildstep (and hence the 32 containing buildfactory) to have changed every time the configuration 33 is reloaded. 34 35 It also sets some defaults differently from adbapi.ConnectionPool that 36 are more suitable for use in MTR. 37 """
38 - def __init__(self, *args, **kwargs):
39 self._eqKey = (args, kwargs) 40 return adbapi.ConnectionPool.__init__(self, 41 cp_reconnect=True, cp_min=1, cp_max=3, 42 *args, **kwargs)
43
44 - def __eq__(self, other):
45 if isinstance(other, EqConnectionPool): 46 return self._eqKey == other._eqKey 47 else: 48 return False
49
50 - def __ne__(self, other):
51 return not self.__eq__(other)
52 53
54 -class MtrTestFailData:
55 - def __init__(self, testname, variant, result, info, text, callback):
56 self.testname = testname 57 self.variant = variant 58 self.result = result 59 self.info = info 60 self.text = text 61 self.callback = callback
62
63 - def add(self, line):
64 self.text+= line
65
66 - def fireCallback(self):
67 return self.callback(self.testname, self.variant, self.result, self.info, self.text)
68 69
70 -class MtrLogObserver(LogLineObserver):
71 """ 72 Class implementing a log observer (can be passed to 73 BuildStep.addLogObserver(). 74 75 It parses the output of mysql-test-run.pl as used in MySQL, 76 MariaDB, Drizzle, etc. 77 78 It counts number of tests run and uses it to provide more accurate 79 completion estimates. 80 81 It parses out test failures from the output and summarises the results on 82 the Waterfall page. It also passes the information to methods that can be 83 overridden in a subclass to do further processing on the information.""" 84 85 _line_re = re.compile(r"^([-._0-9a-zA-z]+)( '[-_ a-zA-Z]+')?\s+(w[0-9]+\s+)?\[ (fail|pass) \]\s*(.*)$") 86 _line_re2 = re.compile(r"^[-._0-9a-zA-z]+( '[-_ a-zA-Z]+')?\s+(w[0-9]+\s+)?\[ [-a-z]+ \]") 87 _line_re3 = re.compile(r"^\*\*\*Warnings generated in error logs during shutdown after running tests: (.*)") 88 _line_re4 = re.compile(r"^The servers were restarted [0-9]+ times$") 89 _line_re5 = re.compile(r"^Only\s+[0-9]+\s+of\s+[0-9]+\s+completed.$") 90
91 - def __init__(self, textLimit=5, testNameLimit=16, testType=None):
92 self.textLimit = textLimit 93 self.testNameLimit = testNameLimit 94 self.testType = testType 95 self.numTests = 0 96 self.testFail = None 97 self.failList = [] 98 self.warnList = [] 99 LogLineObserver.__init__(self)
100
101 - def setLog(self, loog):
102 LogLineObserver.setLog(self, loog) 103 d= loog.waitUntilFinished() 104 d.addCallback(lambda l: self.closeTestFail())
105
106 - def outLineReceived(self, line):
107 stripLine = line.strip("\r\n") 108 m = self._line_re.search(stripLine) 109 if m: 110 testname, variant, worker, result, info = m.groups() 111 self.closeTestFail() 112 self.numTests += 1 113 self.step.setProgress('tests', self.numTests) 114 115 if result == "fail": 116 if variant == None: 117 variant = "" 118 else: 119 variant = variant[2:-1] 120 self.openTestFail(testname, variant, result, info, stripLine + "\n") 121 122 else: 123 m = self._line_re3.search(stripLine) 124 if m: 125 stuff = m.group(1) 126 self.closeTestFail() 127 testList = stuff.split(" ") 128 self.doCollectWarningTests(testList) 129 130 elif (self._line_re2.search(stripLine) or 131 self._line_re4.search(stripLine) or 132 self._line_re5.search(stripLine) or 133 stripLine == "Test suite timeout! Terminating..." or 134 stripLine.startswith("mysql-test-run: *** ERROR: Not all tests completed") or 135 (stripLine.startswith("------------------------------------------------------------") 136 and self.testFail != None)): 137 self.closeTestFail() 138 139 else: 140 self.addTestFailOutput(stripLine + "\n")
141
142 - def openTestFail(self, testname, variant, result, info, line):
143 self.testFail = MtrTestFailData(testname, variant, result, info, line, self.doCollectTestFail)
144
145 - def addTestFailOutput(self, line):
146 if self.testFail != None: 147 self.testFail.add(line)
148
149 - def closeTestFail(self):
150 if self.testFail != None: 151 self.testFail.fireCallback() 152 self.testFail = None
153
154 - def addToText(self, src, dst):
155 lastOne = None 156 count = 0 157 for t in src: 158 if t != lastOne: 159 dst.append(t) 160 count += 1 161 if count >= self.textLimit: 162 break
163
164 - def makeText(self, done):
165 if done: 166 text = ["test"] 167 else: 168 text = ["testing"] 169 if self.testType: 170 text.append(self.testType) 171 fails = self.failList[:] 172 fails.sort() 173 self.addToText(fails, text) 174 warns = self.warnList[:] 175 warns.sort() 176 self.addToText(warns, text) 177 return text
178 179 # Update waterfall status.
180 - def updateText(self):
181 self.step.step_status.setText(self.makeText(False))
182 183 strip_re = re.compile(r"^[a-z]+\.") 184
185 - def displayTestName(self, testname):
186 187 displayTestName = self.strip_re.sub("", testname) 188 189 if len(displayTestName) > self.testNameLimit: 190 displayTestName = displayTestName[:(self.testNameLimit-2)] + "..." 191 return displayTestName
192
193 - def doCollectTestFail(self, testname, variant, result, info, text):
194 self.failList.append("F:" + self.displayTestName(testname)) 195 self.updateText() 196 self.collectTestFail(testname, variant, result, info, text)
197
198 - def doCollectWarningTests(self, testList):
199 for t in testList: 200 self.warnList.append("W:" + self.displayTestName(t)) 201 self.updateText() 202 self.collectWarningTests(testList)
203 204 # These two methods are overridden to actually do something with the data.
205 - def collectTestFail(self, testname, variant, result, info, text):
206 pass
207 - def collectWarningTests(self, testList):
208 pass
209
210 -class MTR(Test):
211 """ 212 Build step that runs mysql-test-run.pl, as used in MySQL, Drizzle, 213 MariaDB, etc. 214 215 It uses class MtrLogObserver to parse test results out from the 216 output of mysql-test-run.pl, providing better completion time 217 estimates and summarising test failures on the waterfall page. 218 219 It also provides access to mysqld server error logs from the test 220 run to help debugging any problems. 221 222 Optionally, it can insert into a database data about the test run, 223 including details of any test failures. 224 225 Parameters: 226 227 textLimit 228 Maximum number of test failures to show on the waterfall page 229 (to not flood the page in case of a large number of test 230 failures. Defaults to 5. 231 232 testNameLimit 233 Maximum length of test names to show unabbreviated in the 234 waterfall page, to avoid excessive column width. Defaults to 16. 235 236 parallel 237 Value of --parallel option used for mysql-test-run.pl (number 238 of processes used to run the test suite in parallel). Defaults 239 to 4. This is used to determine the number of server error log 240 files to download from the slave. Specifying a too high value 241 does not hurt (as nonexisting error logs will be ignored), 242 however if using --parallel value greater than the default it 243 needs to be specified, or some server error logs will be 244 missing. 245 246 dbpool 247 An instance of twisted.enterprise.adbapi.ConnectionPool, or None. 248 Defaults to None. If specified, results are inserted into the database 249 using the ConnectionPool. 250 251 The class process.mtrlogobserver.EqConnectionPool subclass of 252 ConnectionPool can be useful to pass as value for dbpool, to 253 avoid having config reloads think the Buildstep is changed 254 just because it gets a new ConnectionPool instance (even 255 though connection parameters are unchanged). 256 257 autoCreateTables 258 Boolean, defaults to False. If True (and dbpool is specified), the 259 necessary database tables will be created automatically if they do 260 not exist already. Alternatively, the tables can be created manually 261 from the SQL statements found in the mtrlogobserver.py source file. 262 263 test_type 264 test_info 265 Two descriptive strings that will be inserted in the database tables if 266 dbpool is specified. The test_type string, if specified, will also 267 appear on the waterfall page.""" 268 269 renderables = [ 'mtr_subdir' ] 270
271 - def __init__(self, dbpool=None, test_type=None, test_info="", 272 description=None, descriptionDone=None, 273 autoCreateTables=False, textLimit=5, testNameLimit=16, 274 parallel=4, logfiles = {}, lazylogfiles = True, 275 warningPattern="MTR's internal check of the test case '.*' failed", 276 mtr_subdir="mysql-test", **kwargs):
277 278 if description is None: 279 description = ["testing"] 280 if test_type: 281 description.append(test_type) 282 if descriptionDone is None: 283 descriptionDone = ["test"] 284 if test_type: 285 descriptionDone.append(test_type) 286 Test.__init__(self, logfiles=logfiles, lazylogfiles=lazylogfiles, 287 description=description, descriptionDone=descriptionDone, 288 warningPattern=warningPattern, **kwargs) 289 self.dbpool = dbpool 290 self.test_type = test_type 291 self.test_info = test_info 292 self.autoCreateTables = autoCreateTables 293 self.textLimit = textLimit 294 self.testNameLimit = testNameLimit 295 self.parallel = parallel 296 self.mtr_subdir = mtr_subdir 297 self.progressMetrics += ('tests',) 298 299 self.addFactoryArguments(dbpool=self.dbpool, 300 test_type=self.test_type, 301 test_info=self.test_info, 302 autoCreateTables=self.autoCreateTables, 303 textLimit=self.textLimit, 304 testNameLimit=self.testNameLimit, 305 parallel=self.parallel, 306 mtr_subdir=self.mtr_subdir)
307
308 - def start(self):
309 # Add mysql server logfiles. 310 for mtr in range(0, self.parallel+1): 311 for mysqld in range(1, 4+1): 312 if mtr == 0: 313 logname = "mysqld.%d.err" % mysqld 314 filename = "var/log/mysqld.%d.err" % mysqld 315 else: 316 logname = "mysqld.%d.err.%d" % (mysqld, mtr) 317 filename = "var/%d/log/mysqld.%d.err" % (mtr, mysqld) 318 self.addLogFile(logname, self.mtr_subdir + "/" + filename) 319 320 self.myMtr = self.MyMtrLogObserver(textLimit=self.textLimit, 321 testNameLimit=self.testNameLimit, 322 testType=self.test_type) 323 self.addLogObserver("stdio", self.myMtr) 324 # Insert a row for this test run into the database and set up 325 # build properties, then start the command proper. 326 d = self.registerInDB() 327 d.addCallback(self.afterRegisterInDB) 328 d.addErrback(self.failed)
329
330 - def getText(self, command, results):
331 return self.myMtr.makeText(True)
332
333 - def runInteractionWithRetry(self, actionFn, *args, **kw):
334 """ 335 Run a database transaction with dbpool.runInteraction, but retry the 336 transaction in case of a temporary error (like connection lost). 337 338 This is needed to be robust against things like database connection 339 idle timeouts. 340 341 The passed callable that implements the transaction must be retryable, 342 ie. it must not have any destructive side effects in the case where 343 an exception is thrown and/or rollback occurs that would prevent it 344 from functioning correctly when called again.""" 345 346 def runWithRetry(txn, *args, **kw): 347 retryCount = 0 348 while(True): 349 try: 350 return actionFn(txn, *args, **kw) 351 except txn.OperationalError: 352 retryCount += 1 353 if retryCount >= 5: 354 raise 355 excType, excValue, excTraceback = sys.exc_info() 356 log.msg("Database transaction failed (caught exception %s(%s)), retrying ..." % (excType, excValue)) 357 txn.close() 358 txn.reconnect() 359 txn.reopen()
360 361 return self.dbpool.runInteraction(runWithRetry, *args, **kw)
362
363 - def runQueryWithRetry(self, *args, **kw):
364 """ 365 Run a database query, like with dbpool.runQuery, but retry the query in 366 case of a temporary error (like connection lost). 367 368 This is needed to be robust against things like database connection 369 idle timeouts.""" 370 371 def runQuery(txn, *args, **kw): 372 txn.execute(*args, **kw) 373 return txn.fetchall()
374 375 return self.runInteractionWithRetry(runQuery, *args, **kw) 376
377 - def registerInDB(self):
378 if self.dbpool: 379 return self.runInteractionWithRetry(self.doRegisterInDB) 380 else: 381 return defer.succeed(0)
382 383 # The real database work is done in a thread in a synchronous way.
384 - def doRegisterInDB(self, txn):
385 # Auto create tables. 386 # This is off by default, as it gives warnings in log file 387 # about tables already existing (and I did not find the issue 388 # important enough to find a better fix). 389 if self.autoCreateTables: 390 txn.execute(""" 391 CREATE TABLE IF NOT EXISTS test_run( 392 id INT PRIMARY KEY AUTO_INCREMENT, 393 branch VARCHAR(100), 394 revision VARCHAR(32) NOT NULL, 395 platform VARCHAR(100) NOT NULL, 396 dt TIMESTAMP NOT NULL, 397 bbnum INT NOT NULL, 398 typ VARCHAR(32) NOT NULL, 399 info VARCHAR(255), 400 KEY (branch, revision), 401 KEY (dt), 402 KEY (platform, bbnum) 403 ) ENGINE=innodb 404 """) 405 txn.execute(""" 406 CREATE TABLE IF NOT EXISTS test_failure( 407 test_run_id INT NOT NULL, 408 test_name VARCHAR(100) NOT NULL, 409 test_variant VARCHAR(16) NOT NULL, 410 info_text VARCHAR(255), 411 failure_text TEXT, 412 PRIMARY KEY (test_run_id, test_name, test_variant) 413 ) ENGINE=innodb 414 """) 415 txn.execute(""" 416 CREATE TABLE IF NOT EXISTS test_warnings( 417 test_run_id INT NOT NULL, 418 list_id INT NOT NULL, 419 list_idx INT NOT NULL, 420 test_name VARCHAR(100) NOT NULL, 421 PRIMARY KEY (test_run_id, list_id, list_idx) 422 ) ENGINE=innodb 423 """) 424 425 revision = self.getProperty("got_revision") 426 if revision is None: 427 revision = self.getProperty("revision") 428 typ = "mtr" 429 if self.test_type: 430 typ = self.test_type 431 txn.execute(""" 432 INSERT INTO test_run(branch, revision, platform, dt, bbnum, typ, info) 433 VALUES (%s, %s, %s, CURRENT_TIMESTAMP(), %s, %s, %s) 434 """, (self.getProperty("branch"), revision, 435 self.getProperty("buildername"), self.getProperty("buildnumber"), 436 typ, self.test_info)) 437 438 return txn.lastrowid
439
440 - def afterRegisterInDB(self, insert_id):
441 self.setProperty("mtr_id", insert_id) 442 self.setProperty("mtr_warn_id", 0) 443 444 Test.start(self)
445
446 - def reportError(self, err):
447 log.msg("Error in async insert into database: %s" % err)
448
449 - class MyMtrLogObserver(MtrLogObserver):
450 - def collectTestFail(self, testname, variant, result, info, text):
451 # Insert asynchronously into database. 452 dbpool = self.step.dbpool 453 run_id = self.step.getProperty("mtr_id") 454 if dbpool == None: 455 return defer.succeed(None) 456 if variant == None: 457 variant = "" 458 d = self.step.runQueryWithRetry(""" 459 INSERT INTO test_failure(test_run_id, test_name, test_variant, info_text, failure_text) 460 VALUES (%s, %s, %s, %s, %s) 461 """, (run_id, testname, variant, info, text)) 462 463 d.addErrback(self.step.reportError) 464 return d
465
466 - def collectWarningTests(self, testList):
467 # Insert asynchronously into database. 468 dbpool = self.step.dbpool 469 if dbpool == None: 470 return defer.succeed(None) 471 run_id = self.step.getProperty("mtr_id") 472 warn_id = self.step.getProperty("mtr_warn_id") 473 self.step.setProperty("mtr_warn_id", warn_id + 1) 474 q = ("INSERT INTO test_warnings(test_run_id, list_id, list_idx, test_name) " + 475 "VALUES " + ", ".join(map(lambda x: "(%s, %s, %s, %s)", testList))) 476 v = [] 477 idx = 0 478 for t in testList: 479 v.extend([run_id, warn_id, idx, t]) 480 idx = idx + 1 481 d = self.step.runQueryWithRetry(q, tuple(v)) 482 d.addErrback(self.step.reportError) 483 return d
484