1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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 """
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
45 if isinstance(other, EqConnectionPool):
46 return self._eqKey == other._eqKey
47 else:
48 return False
49
51 return not self.__eq__(other)
52
53
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):
65
67 return self.callback(self.testname, self.variant, self.result, self.info, self.text)
68
69
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
105
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):
144
146 if self.testFail != None:
147 self.testFail.add(line)
148
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
180 - def updateText(self):
182
183 strip_re = re.compile(r"^[a-z]+\.")
184
192
197
203
204
209
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):
298
300
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
316
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
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
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
373
374
376
377
378
379
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
436
438 log.msg("Error in async insert into database: %s" % err)
439
442
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
458
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