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