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 - def start(self):
300 # Add mysql server logfiles. 301 for mtr in range(0, self.parallel+1): 302 for mysqld in range(1, 4+1): 303 if mtr == 0: 304 logname = "mysqld.%d.err" % mysqld 305 filename = "var/log/mysqld.%d.err" % mysqld 306 else: 307 logname = "mysqld.%d.err.%d" % (mysqld, mtr) 308 filename = "var/%d/log/mysqld.%d.err" % (mtr, mysqld) 309 self.addLogFile(logname, self.mtr_subdir + "/" + filename) 310 311 self.myMtr = self.MyMtrLogObserver(textLimit=self.textLimit, 312 testNameLimit=self.testNameLimit, 313 testType=self.test_type) 314 self.addLogObserver("stdio", self.myMtr) 315 # Insert a row for this test run into the database and set up 316 # build properties, then start the command proper. 317 d = self.registerInDB() 318 d.addCallback(self.afterRegisterInDB) 319 d.addErrback(self.failed)
320
321 - def getText(self, command, results):
322 return self.myMtr.makeText(True)
323
324 - def runInteractionWithRetry(self, actionFn, *args, **kw):
325 """ 326 Run a database transaction with dbpool.runInteraction, but retry the 327 transaction in case of a temporary error (like connection lost). 328 329 This is needed to be robust against things like database connection 330 idle timeouts. 331 332 The passed callable that implements the transaction must be retryable, 333 ie. it must not have any destructive side effects in the case where 334 an exception is thrown and/or rollback occurs that would prevent it 335 from functioning correctly when called again.""" 336 337 def runWithRetry(txn, *args, **kw): 338 retryCount = 0 339 while(True): 340 try: 341 return actionFn(txn, *args, **kw) 342 except txn.OperationalError: 343 retryCount += 1 344 if retryCount >= 5: 345 raise 346 excType, excValue, excTraceback = sys.exc_info() 347 log.msg("Database transaction failed (caught exception %s(%s)), retrying ..." % (excType, excValue)) 348 txn.close() 349 txn.reconnect() 350 txn.reopen()
351 352 return self.dbpool.runInteraction(runWithRetry, *args, **kw)
353
354 - def runQueryWithRetry(self, *args, **kw):
355 """ 356 Run a database query, like with dbpool.runQuery, but retry the query in 357 case of a temporary error (like connection lost). 358 359 This is needed to be robust against things like database connection 360 idle timeouts.""" 361 362 def runQuery(txn, *args, **kw): 363 txn.execute(*args, **kw) 364 return txn.fetchall()
365 366 return self.runInteractionWithRetry(runQuery, *args, **kw) 367
368 - def registerInDB(self):
369 if self.dbpool: 370 return self.runInteractionWithRetry(self.doRegisterInDB) 371 else: 372 return defer.succeed(0)
373 374 # The real database work is done in a thread in a synchronous way.
375 - def doRegisterInDB(self, txn):
376 # Auto create tables. 377 # This is off by default, as it gives warnings in log file 378 # about tables already existing (and I did not find the issue 379 # important enough to find a better fix). 380 if self.autoCreateTables: 381 txn.execute(""" 382 CREATE TABLE IF NOT EXISTS test_run( 383 id INT PRIMARY KEY AUTO_INCREMENT, 384 branch VARCHAR(100), 385 revision VARCHAR(32) NOT NULL, 386 platform VARCHAR(100) NOT NULL, 387 dt TIMESTAMP NOT NULL, 388 bbnum INT NOT NULL, 389 typ VARCHAR(32) NOT NULL, 390 info VARCHAR(255), 391 KEY (branch, revision), 392 KEY (dt), 393 KEY (platform, bbnum) 394 ) ENGINE=innodb 395 """) 396 txn.execute(""" 397 CREATE TABLE IF NOT EXISTS test_failure( 398 test_run_id INT NOT NULL, 399 test_name VARCHAR(100) NOT NULL, 400 test_variant VARCHAR(16) NOT NULL, 401 info_text VARCHAR(255), 402 failure_text TEXT, 403 PRIMARY KEY (test_run_id, test_name, test_variant) 404 ) ENGINE=innodb 405 """) 406 txn.execute(""" 407 CREATE TABLE IF NOT EXISTS test_warnings( 408 test_run_id INT NOT NULL, 409 list_id INT NOT NULL, 410 list_idx INT NOT NULL, 411 test_name VARCHAR(100) NOT NULL, 412 PRIMARY KEY (test_run_id, list_id, list_idx) 413 ) ENGINE=innodb 414 """) 415 416 revision = self.getProperty("got_revision") 417 if revision is None: 418 revision = self.getProperty("revision") 419 typ = "mtr" 420 if self.test_type: 421 typ = self.test_type 422 txn.execute(""" 423 INSERT INTO test_run(branch, revision, platform, dt, bbnum, typ, info) 424 VALUES (%s, %s, %s, CURRENT_TIMESTAMP(), %s, %s, %s) 425 """, (self.getProperty("branch"), revision, 426 self.getProperty("buildername"), self.getProperty("buildnumber"), 427 typ, self.test_info)) 428 429 return txn.lastrowid
430
431 - def afterRegisterInDB(self, insert_id):
432 self.setProperty("mtr_id", insert_id) 433 self.setProperty("mtr_warn_id", 0) 434 435 Test.start(self)
436
437 - def reportError(self, err):
438 log.msg("Error in async insert into database: %s" % err)
439
440 - class MyMtrLogObserver(MtrLogObserver):
441 - def collectTestFail(self, testname, variant, result, info, text):
442 # Insert asynchronously into database. 443 dbpool = self.step.dbpool 444 run_id = self.step.getProperty("mtr_id") 445 if dbpool == None: 446 return defer.succeed(None) 447 if variant == None: 448 variant = "" 449 d = self.step.runQueryWithRetry(""" 450 INSERT INTO test_failure(test_run_id, test_name, test_variant, info_text, failure_text) 451 VALUES (%s, %s, %s, %s, %s) 452 """, (run_id, testname, variant, info, text)) 453 454 d.addErrback(self.step.reportError) 455 return d
456
457 - def collectWarningTests(self, testList):
458 # Insert asynchronously into database. 459 dbpool = self.step.dbpool 460 if dbpool == None: 461 return defer.succeed(None) 462 run_id = self.step.getProperty("mtr_id") 463 warn_id = self.step.getProperty("mtr_warn_id") 464 self.step.setProperty("mtr_warn_id", warn_id + 1) 465 q = ("INSERT INTO test_warnings(test_run_id, list_id, list_idx, test_name) " + 466 "VALUES " + ", ".join(map(lambda x: "(%s, %s, %s, %s)", testList))) 467 v = [] 468 idx = 0 469 for t in testList: 470 v.extend([run_id, warn_id, idx, t]) 471 idx = idx + 1 472 d = self.step.runQueryWithRetry(q, tuple(v)) 473 d.addErrback(self.step.reportError) 474 return d
475