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