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      renderables = [ 'mtr_subdir' ] 
271   
272 -    def __init__(self, dbpool=None, test_type=None, test_info="", 
273                   description=None, descriptionDone=None, 
274                   autoCreateTables=False, textLimit=5, testNameLimit=16, 
275                   parallel=4, logfiles = {}, lazylogfiles = True, 
276                   warningPattern="MTR's internal check of the test case '.*' failed", 
277                   mtr_subdir="mysql-test", **kwargs): 
 278   
279          if description is None: 
280              description = ["testing"] 
281              if test_type: 
282                  description.append(test_type) 
283          if descriptionDone is None: 
284              descriptionDone = ["test"] 
285              if test_type: 
286                  descriptionDone.append(test_type) 
287          Test.__init__(self, logfiles=logfiles, lazylogfiles=lazylogfiles, 
288                        description=description, descriptionDone=descriptionDone, 
289                        warningPattern=warningPattern, **kwargs) 
290          self.dbpool = dbpool 
291          self.test_type = test_type 
292          self.test_info = test_info 
293          self.autoCreateTables = autoCreateTables 
294          self.textLimit = textLimit 
295          self.testNameLimit = testNameLimit 
296          self.parallel = parallel 
297          self.mtr_subdir = mtr_subdir 
298          self.progressMetrics += ('tests',) 
299   
300          self.addFactoryArguments(dbpool=self.dbpool, 
301                                   test_type=self.test_type, 
302                                   test_info=self.test_info, 
303                                   autoCreateTables=self.autoCreateTables, 
304                                   textLimit=self.textLimit, 
305                                   testNameLimit=self.testNameLimit, 
306                                   parallel=self.parallel, 
307                                   mtr_subdir=self.mtr_subdir) 
 308   
310           
311          for mtr in range(0, self.parallel+1): 
312              for mysqld in range(1, 4+1): 
313                  if mtr == 0: 
314                      logname = "mysqld.%d.err" % mysqld 
315                      filename = "var/log/mysqld.%d.err" % mysqld 
316                  else: 
317                      logname = "mysqld.%d.err.%d" % (mysqld, mtr) 
318                      filename = "var/%d/log/mysqld.%d.err" % (mtr, mysqld) 
319                  self.addLogFile(logname, self.mtr_subdir + "/" + filename) 
320   
321          self.myMtr = self.MyMtrLogObserver(textLimit=self.textLimit, 
322                                             testNameLimit=self.testNameLimit, 
323                                             testType=self.test_type) 
324          self.addLogObserver("stdio", self.myMtr) 
325           
326           
327          d = self.registerInDB() 
328          d.addCallback(self.afterRegisterInDB) 
329          d.addErrback(self.failed) 
 330   
331 -    def getText(self, command, results): 
 332          return self.myMtr.makeText(True) 
 333   
335          """ 
336          Run a database transaction with dbpool.runInteraction, but retry the 
337          transaction in case of a temporary error (like connection lost). 
338   
339          This is needed to be robust against things like database connection 
340          idle timeouts. 
341   
342          The passed callable that implements the transaction must be retryable, 
343          ie. it must not have any destructive side effects in the case where 
344          an exception is thrown and/or rollback occurs that would prevent it 
345          from functioning correctly when called again.""" 
346   
347          def runWithRetry(txn, *args, **kw): 
348              retryCount = 0 
349              while(True): 
350                  try: 
351                      return actionFn(txn, *args, **kw) 
352                  except txn.OperationalError: 
353                      retryCount += 1 
354                      if retryCount >= 5: 
355                          raise 
356                      excType, excValue, excTraceback = sys.exc_info() 
357                      log.msg("Database transaction failed (caught exception %s(%s)), retrying ..." % (excType, excValue)) 
358                      txn.close() 
359                      txn.reconnect() 
360                      txn.reopen() 
 361   
362          return self.dbpool.runInteraction(runWithRetry, *args, **kw) 
 363   
365          """ 
366          Run a database query, like with dbpool.runQuery, but retry the query in 
367          case of a temporary error (like connection lost). 
368   
369          This is needed to be robust against things like database connection 
370          idle timeouts.""" 
371   
372          def runQuery(txn, *args, **kw): 
373              txn.execute(*args, **kw) 
374              return txn.fetchall() 
 375   
376          return self.runInteractionWithRetry(runQuery, *args, **kw) 
377   
383   
384       
386           
387           
388           
389           
390          if self.autoCreateTables: 
391              txn.execute(""" 
392  CREATE TABLE IF NOT EXISTS test_run( 
393      id INT PRIMARY KEY AUTO_INCREMENT, 
394      branch VARCHAR(100), 
395      revision VARCHAR(32) NOT NULL, 
396      platform VARCHAR(100) NOT NULL, 
397      dt TIMESTAMP NOT NULL, 
398      bbnum INT NOT NULL, 
399      typ VARCHAR(32) NOT NULL, 
400      info VARCHAR(255), 
401      KEY (branch, revision), 
402      KEY (dt), 
403      KEY (platform, bbnum) 
404  ) ENGINE=innodb 
405  """) 
406              txn.execute(""" 
407  CREATE TABLE IF NOT EXISTS test_failure( 
408      test_run_id INT NOT NULL, 
409      test_name VARCHAR(100) NOT NULL, 
410      test_variant VARCHAR(16) NOT NULL, 
411      info_text VARCHAR(255), 
412      failure_text TEXT, 
413      PRIMARY KEY (test_run_id, test_name, test_variant) 
414  ) ENGINE=innodb 
415  """) 
416              txn.execute(""" 
417  CREATE TABLE IF NOT EXISTS test_warnings( 
418      test_run_id INT NOT NULL, 
419      list_id INT NOT NULL, 
420      list_idx INT NOT NULL, 
421      test_name VARCHAR(100) NOT NULL, 
422      PRIMARY KEY (test_run_id, list_id, list_idx) 
423  ) ENGINE=innodb 
424  """) 
425   
426          revision = None 
427          try: 
428              revision = self.getProperty("got_revision") 
429          except exceptions.KeyError: 
430              revision = self.getProperty("revision") 
431          typ = "mtr" 
432          if self.test_type: 
433              typ = self.test_type 
434          txn.execute(""" 
435  INSERT INTO test_run(branch, revision, platform, dt, bbnum, typ, info) 
436  VALUES (%s, %s, %s, CURRENT_TIMESTAMP(), %s, %s, %s) 
437  """, (self.getProperty("branch"), revision, 
438        self.getProperty("buildername"), self.getProperty("buildnumber"), 
439        typ, self.test_info)) 
440   
441          return txn.lastrowid 
 442   
448   
450          log.msg("Error in async insert into database: %s" % err) 
 451   
454               
455              dbpool = self.step.dbpool 
456              run_id = self.step.getProperty("mtr_id") 
457              if dbpool == None: 
458                  return defer.succeed(None) 
459              if variant == None: 
460                  variant = "" 
461              d = self.step.runQueryWithRetry(""" 
462  INSERT INTO test_failure(test_run_id, test_name, test_variant, info_text, failure_text) 
463  VALUES (%s, %s, %s, %s, %s) 
464  """, (run_id, testname, variant, info, text)) 
465   
466              d.addErrback(self.step.reportError) 
467              return d 
 468   
470               
471              dbpool = self.step.dbpool 
472              if dbpool == None: 
473                  return defer.succeed(None) 
474              run_id = self.step.getProperty("mtr_id") 
475              warn_id = self.step.getProperty("mtr_warn_id") 
476              self.step.setProperty("mtr_warn_id", warn_id + 1) 
477              q = ("INSERT INTO test_warnings(test_run_id, list_id, list_idx, test_name) " + 
478                   "VALUES " + ", ".join(map(lambda x: "(%s, %s, %s, %s)", testList))) 
479              v = [] 
480              idx = 0 
481              for t in testList: 
482                  v.extend([run_id, warn_id, idx, t]) 
483                  idx = idx + 1 
484              d = self.step.runQueryWithRetry(q, tuple(v)) 
485              d.addErrback(self.step.reportError) 
486              return d 
  487