Package buildbot :: Package steps :: Module python_twisted
[frames] | no frames]

Source Code for Module buildbot.steps.python_twisted

  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   
 17  from twisted.python import log 
 18   
 19  from buildbot.status import builder 
 20  from buildbot.status.builder import SUCCESS, FAILURE, WARNINGS, SKIPPED 
 21  from buildbot.process.buildstep import LogLineObserver, OutputProgressObserver 
 22  from buildbot.process.buildstep import RemoteShellCommand 
 23  from buildbot.steps.shell import ShellCommand 
 24   
 25  try: 
 26      import cStringIO 
 27      StringIO = cStringIO 
 28  except ImportError: 
 29      import StringIO 
 30  import re 
 31   
 32  # BuildSteps that are specific to the Twisted source tree 
 33   
34 -class HLint(ShellCommand):
35 """I run a 'lint' checker over a set of .xhtml files. Any deviations 36 from recommended style is flagged and put in the output log. 37 38 This step looks at .changes in the parent Build to extract a list of 39 Lore XHTML files to check.""" 40 41 name = "hlint" 42 description = ["running", "hlint"] 43 descriptionDone = ["hlint"] 44 warnOnWarnings = True 45 warnOnFailure = True 46 # TODO: track time, but not output 47 warnings = 0 48
49 - def __init__(self, python=None, **kwargs):
50 ShellCommand.__init__(self, **kwargs) 51 self.addFactoryArguments(python=python) 52 self.python = python
53
54 - def start(self):
55 # create the command 56 htmlFiles = {} 57 for f in self.build.allFiles(): 58 if f.endswith(".xhtml") and not f.startswith("sandbox/"): 59 htmlFiles[f] = 1 60 # remove duplicates 61 hlintTargets = htmlFiles.keys() 62 hlintTargets.sort() 63 if not hlintTargets: 64 return SKIPPED 65 self.hlintFiles = hlintTargets 66 c = [] 67 if self.python: 68 c.append(self.python) 69 c += ["bin/lore", "-p", "--output", "lint"] + self.hlintFiles 70 self.setCommand(c) 71 72 # add an extra log file to show the .html files we're checking 73 self.addCompleteLog("files", "\n".join(self.hlintFiles)+"\n") 74 75 ShellCommand.start(self)
76
77 - def commandComplete(self, cmd):
78 # TODO: remove the 'files' file (a list of .xhtml files that were 79 # submitted to hlint) because it is available in the logfile and 80 # mostly exists to give the user an idea of how long the step will 81 # take anyway). 82 lines = cmd.logs['stdio'].getText().split("\n") 83 warningLines = filter(lambda line:':' in line, lines) 84 if warningLines: 85 self.addCompleteLog("warnings", "".join(warningLines)) 86 warnings = len(warningLines) 87 self.warnings = warnings
88
89 - def evaluateCommand(self, cmd):
90 # warnings are in stdout, rc is always 0, unless the tools break 91 if cmd.rc != 0: 92 return FAILURE 93 if self.warnings: 94 return WARNINGS 95 return SUCCESS
96
97 - def getText2(self, cmd, results):
98 if cmd.rc != 0: 99 return ["hlint"] 100 return ["%d hlin%s" % (self.warnings, 101 self.warnings == 1 and 't' or 'ts')]
102
103 -def countFailedTests(output):
104 # start scanning 10kb from the end, because there might be a few kb of 105 # import exception tracebacks between the total/time line and the errors 106 # line 107 chunk = output[-10000:] 108 lines = chunk.split("\n") 109 lines.pop() # blank line at end 110 # lines[-3] is "Ran NN tests in 0.242s" 111 # lines[-2] is blank 112 # lines[-1] is 'OK' or 'FAILED (failures=1, errors=12)' 113 # or 'FAILED (failures=1)' 114 # or "PASSED (skips=N, successes=N)" (for Twisted-2.0) 115 # there might be other lines dumped here. Scan all the lines. 116 res = {'total': None, 117 'failures': 0, 118 'errors': 0, 119 'skips': 0, 120 'expectedFailures': 0, 121 'unexpectedSuccesses': 0, 122 } 123 for l in lines: 124 out = re.search(r'Ran (\d+) tests', l) 125 if out: 126 res['total'] = int(out.group(1)) 127 if (l.startswith("OK") or 128 l.startswith("FAILED ") or 129 l.startswith("PASSED")): 130 # the extra space on FAILED_ is to distinguish the overall 131 # status from an individual test which failed. The lack of a 132 # space on the OK is because it may be printed without any 133 # additional text (if there are no skips,etc) 134 out = re.search(r'failures=(\d+)', l) 135 if out: res['failures'] = int(out.group(1)) 136 out = re.search(r'errors=(\d+)', l) 137 if out: res['errors'] = int(out.group(1)) 138 out = re.search(r'skips=(\d+)', l) 139 if out: res['skips'] = int(out.group(1)) 140 out = re.search(r'expectedFailures=(\d+)', l) 141 if out: res['expectedFailures'] = int(out.group(1)) 142 out = re.search(r'unexpectedSuccesses=(\d+)', l) 143 if out: res['unexpectedSuccesses'] = int(out.group(1)) 144 # successes= is a Twisted-2.0 addition, and is not currently used 145 out = re.search(r'successes=(\d+)', l) 146 if out: res['successes'] = int(out.group(1)) 147 148 return res
149 150
151 -class TrialTestCaseCounter(LogLineObserver):
152 _line_re = re.compile(r'^(?:Doctest: )?([\w\.]+) \.\.\. \[([^\]]+)\]$') 153 numTests = 0 154 finished = False 155
156 - def outLineReceived(self, line):
157 # different versions of Twisted emit different per-test lines with 158 # the bwverbose reporter. 159 # 2.0.0: testSlave (buildbot.test.test_runner.Create) ... [OK] 160 # 2.1.0: buildbot.test.test_runner.Create.testSlave ... [OK] 161 # 2.4.0: buildbot.test.test_runner.Create.testSlave ... [OK] 162 # Let's just handle the most recent version, since it's the easiest. 163 # Note that doctests create lines line this: 164 # Doctest: viff.field.GF ... [OK] 165 166 if self.finished: 167 return 168 if line.startswith("=" * 40): 169 self.finished = True 170 return 171 172 m = self._line_re.search(line.strip()) 173 if m: 174 testname, result = m.groups() 175 self.numTests += 1 176 self.step.setProgress('tests', self.numTests)
177 178 179 UNSPECIFIED=() # since None is a valid choice 180
181 -class Trial(ShellCommand):
182 """ 183 There are some class attributes which may be usefully overridden 184 by subclasses. 'trialMode' and 'trialArgs' can influence the trial 185 command line. 186 """ 187 188 name = "trial" 189 progressMetrics = ('output', 'tests', 'test.log') 190 # note: the slash only works on unix buildslaves, of course, but we have 191 # no way to know what the buildslave uses as a separator. 192 # TODO: figure out something clever. 193 logfiles = {"test.log": "_trial_temp/test.log"} 194 # we use test.log to track Progress at the end of __init__() 195 196 flunkOnFailure = True 197 python = None 198 trial = "trial" 199 trialMode = ["--reporter=bwverbose"] # requires Twisted-2.1.0 or newer 200 # for Twisted-2.0.0 or 1.3.0, use ["-o"] instead 201 trialArgs = [] 202 testpath = UNSPECIFIED # required (but can be None) 203 testChanges = False # TODO: needs better name 204 recurse = False 205 reactor = None 206 randomly = False 207 tests = None # required 208
209 - def __init__(self, reactor=UNSPECIFIED, python=None, trial=None, 210 testpath=UNSPECIFIED, 211 tests=None, testChanges=None, 212 recurse=None, randomly=None, 213 trialMode=None, trialArgs=None, 214 **kwargs):
215 """ 216 @type testpath: string 217 @param testpath: use in PYTHONPATH when running the tests. If 218 None, do not set PYTHONPATH. Setting this to '.' will 219 cause the source files to be used in-place. 220 221 @type python: string (without spaces) or list 222 @param python: which python executable to use. Will form the start of 223 the argv array that will launch trial. If you use this, 224 you should set 'trial' to an explicit path (like 225 /usr/bin/trial or ./bin/trial). Defaults to None, which 226 leaves it out entirely (running 'trial args' instead of 227 'python ./bin/trial args'). Likely values are 'python', 228 ['python2.2'], ['python', '-Wall'], etc. 229 230 @type trial: string 231 @param trial: which 'trial' executable to run. 232 Defaults to 'trial', which will cause $PATH to be 233 searched and probably find /usr/bin/trial . If you set 234 'python', this should be set to an explicit path (because 235 'python2.3 trial' will not work). 236 237 @type trialMode: list of strings 238 @param trialMode: a list of arguments to pass to trial, specifically 239 to set the reporting mode. This defaults to ['-to'] 240 which means 'verbose colorless output' to the trial 241 that comes with Twisted-2.0.x and at least -2.1.0 . 242 Newer versions of Twisted may come with a trial 243 that prefers ['--reporter=bwverbose']. 244 245 @type trialArgs: list of strings 246 @param trialArgs: a list of arguments to pass to trial, available to 247 turn on any extra flags you like. Defaults to []. 248 249 @type tests: list of strings 250 @param tests: a list of test modules to run, like 251 ['twisted.test.test_defer', 'twisted.test.test_process']. 252 If this is a string, it will be converted into a one-item 253 list. 254 255 @type testChanges: boolean 256 @param testChanges: if True, ignore the 'tests' parameter and instead 257 ask the Build for all the files that make up the 258 Changes going into this build. Pass these filenames 259 to trial and ask it to look for test-case-name 260 tags, running just the tests necessary to cover the 261 changes. 262 263 @type recurse: boolean 264 @param recurse: If True, pass the --recurse option to trial, allowing 265 test cases to be found in deeper subdirectories of the 266 modules listed in 'tests'. This does not appear to be 267 necessary when using testChanges. 268 269 @type reactor: string 270 @param reactor: which reactor to use, like 'gtk' or 'java'. If not 271 provided, the Twisted's usual platform-dependent 272 default is used. 273 274 @type randomly: boolean 275 @param randomly: if True, add the --random=0 argument, which instructs 276 trial to run the unit tests in a random order each 277 time. This occasionally catches problems that might be 278 masked when one module always runs before another 279 (like failing to make registerAdapter calls before 280 lookups are done). 281 282 @type kwargs: dict 283 @param kwargs: parameters. The following parameters are inherited from 284 L{ShellCommand} and may be useful to set: workdir, 285 haltOnFailure, flunkOnWarnings, flunkOnFailure, 286 warnOnWarnings, warnOnFailure, want_stdout, want_stderr, 287 timeout. 288 """ 289 ShellCommand.__init__(self, **kwargs) 290 self.addFactoryArguments(reactor=reactor, 291 python=python, 292 trial=trial, 293 testpath=testpath, 294 tests=tests, 295 testChanges=testChanges, 296 recurse=recurse, 297 randomly=randomly, 298 trialMode=trialMode, 299 trialArgs=trialArgs, 300 ) 301 302 if python: 303 self.python = python 304 if self.python is not None: 305 if type(self.python) is str: 306 self.python = [self.python] 307 for s in self.python: 308 if " " in s: 309 # this is not strictly an error, but I suspect more 310 # people will accidentally try to use python="python2.3 311 # -Wall" than will use embedded spaces in a python flag 312 log.msg("python= component '%s' has spaces") 313 log.msg("To add -Wall, use python=['python', '-Wall']") 314 why = "python= value has spaces, probably an error" 315 raise ValueError(why) 316 317 if trial: 318 self.trial = trial 319 if " " in self.trial: 320 raise ValueError("trial= value has spaces") 321 if trialMode is not None: 322 self.trialMode = trialMode 323 if trialArgs is not None: 324 self.trialArgs = trialArgs 325 326 if testpath is not UNSPECIFIED: 327 self.testpath = testpath 328 if self.testpath is UNSPECIFIED: 329 raise ValueError("You must specify testpath= (it can be None)") 330 assert isinstance(self.testpath, str) or self.testpath is None 331 332 if reactor is not UNSPECIFIED: 333 self.reactor = reactor 334 335 if tests is not None: 336 self.tests = tests 337 if type(self.tests) is str: 338 self.tests = [self.tests] 339 if testChanges is not None: 340 self.testChanges = testChanges 341 #self.recurse = True # not sure this is necessary 342 343 if not self.testChanges and self.tests is None: 344 raise ValueError("Must either set testChanges= or provide tests=") 345 346 if recurse is not None: 347 self.recurse = recurse 348 if randomly is not None: 349 self.randomly = randomly 350 351 # build up most of the command, then stash it until start() 352 command = [] 353 if self.python: 354 command.extend(self.python) 355 command.append(self.trial) 356 command.extend(self.trialMode) 357 if self.recurse: 358 command.append("--recurse") 359 if self.reactor: 360 command.append("--reactor=%s" % reactor) 361 if self.randomly: 362 command.append("--random=0") 363 command.extend(self.trialArgs) 364 self.command = command 365 366 if self.reactor: 367 self.description = ["testing", "(%s)" % self.reactor] 368 self.descriptionDone = ["tests"] 369 # commandComplete adds (reactorname) to self.text 370 else: 371 self.description = ["testing"] 372 self.descriptionDone = ["tests"] 373 374 # this counter will feed Progress along the 'test cases' metric 375 self.addLogObserver('stdio', TrialTestCaseCounter()) 376 # this one just measures bytes of output in _trial_temp/test.log 377 self.addLogObserver('test.log', OutputProgressObserver('test.log'))
378
379 - def setupEnvironment(self, cmd):
380 ShellCommand.setupEnvironment(self, cmd) 381 if self.testpath != None: 382 e = cmd.args['env'] 383 if e is None: 384 cmd.args['env'] = {'PYTHONPATH': self.testpath} 385 else: 386 # TODO: somehow, each build causes another copy of 387 # self.testpath to get prepended 388 if e.get('PYTHONPATH', "") == "": 389 e['PYTHONPATH'] = self.testpath 390 else: 391 e['PYTHONPATH'] = self.testpath + ":" + e['PYTHONPATH'] 392 try: 393 p = cmd.args['env']['PYTHONPATH'] 394 if type(p) is not str: 395 log.msg("hey, not a string:", p) 396 assert False 397 except (KeyError, TypeError): 398 # KeyError if args doesn't have ['env'] 399 # KeyError if args['env'] doesn't have ['PYTHONPATH'] 400 # TypeError if args is None 401 pass
402
403 - def start(self):
404 # now that self.build.allFiles() is nailed down, finish building the 405 # command 406 if self.testChanges: 407 for f in self.build.allFiles(): 408 if f.endswith(".py"): 409 self.command.append("--testmodule=%s" % f) 410 else: 411 self.command.extend(self.tests) 412 log.msg("Trial.start: command is", self.command) 413 414 # if our slave is too old to understand logfiles=, fetch them 415 # manually. This is a fallback for the Twisted buildbot and some old 416 # buildslaves. 417 self._needToPullTestDotLog = False 418 if self.slaveVersionIsOlderThan("shell", "2.1"): 419 log.msg("Trial: buildslave %s is too old to accept logfiles=" % 420 self.getSlaveName()) 421 log.msg(" falling back to 'cat _trial_temp/test.log' instead") 422 self.logfiles = {} 423 self._needToPullTestDotLog = True 424 425 ShellCommand.start(self)
426 427
428 - def commandComplete(self, cmd):
429 if not self._needToPullTestDotLog: 430 return self._gotTestDotLog(cmd) 431 432 # if the buildslave was too old, pull test.log now 433 catcmd = ["cat", "_trial_temp/test.log"] 434 c2 = RemoteShellCommand(command=catcmd, workdir=self.workdir) 435 loog = self.addLog("test.log") 436 c2.useLog(loog, True, logfileName="stdio") 437 self.cmd = c2 # to allow interrupts 438 d = c2.run(self, self.remote) 439 d.addCallback(lambda res: self._gotTestDotLog(cmd)) 440 return d
441
442 - def rtext(self, fmt='%s'):
443 if self.reactor: 444 rtext = fmt % self.reactor 445 return rtext.replace("reactor", "") 446 return ""
447
448 - def _gotTestDotLog(self, cmd):
449 # figure out all status, then let the various hook functions return 450 # different pieces of it 451 452 # 'cmd' is the original trial command, so cmd.logs['stdio'] is the 453 # trial output. We don't have access to test.log from here. 454 output = cmd.logs['stdio'].getText() 455 counts = countFailedTests(output) 456 457 total = counts['total'] 458 failures, errors = counts['failures'], counts['errors'] 459 parsed = (total != None) 460 text = [] 461 text2 = "" 462 463 if cmd.rc == 0: 464 if parsed: 465 results = SUCCESS 466 if total: 467 text += ["%d %s" % \ 468 (total, 469 total == 1 and "test" or "tests"), 470 "passed"] 471 else: 472 text += ["no tests", "run"] 473 else: 474 results = FAILURE 475 text += ["testlog", "unparseable"] 476 text2 = "tests" 477 else: 478 # something failed 479 results = FAILURE 480 if parsed: 481 text.append("tests") 482 if failures: 483 text.append("%d %s" % \ 484 (failures, 485 failures == 1 and "failure" or "failures")) 486 if errors: 487 text.append("%d %s" % \ 488 (errors, 489 errors == 1 and "error" or "errors")) 490 count = failures + errors 491 text2 = "%d tes%s" % (count, (count == 1 and 't' or 'ts')) 492 else: 493 text += ["tests", "failed"] 494 text2 = "tests" 495 496 if counts['skips']: 497 text.append("%d %s" % \ 498 (counts['skips'], 499 counts['skips'] == 1 and "skip" or "skips")) 500 if counts['expectedFailures']: 501 text.append("%d %s" % \ 502 (counts['expectedFailures'], 503 counts['expectedFailures'] == 1 and "todo" 504 or "todos")) 505 if 0: # TODO 506 results = WARNINGS 507 if not text2: 508 text2 = "todo" 509 510 if 0: 511 # ignore unexpectedSuccesses for now, but it should really mark 512 # the build WARNING 513 if counts['unexpectedSuccesses']: 514 text.append("%d surprises" % counts['unexpectedSuccesses']) 515 results = WARNINGS 516 if not text2: 517 text2 = "tests" 518 519 if self.reactor: 520 text.append(self.rtext('(%s)')) 521 if text2: 522 text2 = "%s %s" % (text2, self.rtext('(%s)')) 523 524 self.results = results 525 self.text = text 526 self.text2 = [text2]
527
528 - def addTestResult(self, testname, results, text, tlog):
529 if self.reactor is not None: 530 testname = (self.reactor,) + testname 531 tr = builder.TestResult(testname, results, text, logs={'log': tlog}) 532 #self.step_status.build.addTestResult(tr) 533 self.build.build_status.addTestResult(tr)
534
535 - def createSummary(self, loog):
536 output = loog.getText() 537 problems = "" 538 sio = StringIO.StringIO(output) 539 warnings = {} 540 while 1: 541 line = sio.readline() 542 if line == "": 543 break 544 if line.find(" exceptions.DeprecationWarning: ") != -1: 545 # no source 546 warning = line # TODO: consider stripping basedir prefix here 547 warnings[warning] = warnings.get(warning, 0) + 1 548 elif (line.find(" DeprecationWarning: ") != -1 or 549 line.find(" UserWarning: ") != -1): 550 # next line is the source 551 warning = line + sio.readline() 552 warnings[warning] = warnings.get(warning, 0) + 1 553 elif line.find("Warning: ") != -1: 554 warning = line 555 warnings[warning] = warnings.get(warning, 0) + 1 556 557 if line.find("=" * 60) == 0 or line.find("-" * 60) == 0: 558 problems += line 559 problems += sio.read() 560 break 561 562 if problems: 563 self.addCompleteLog("problems", problems) 564 # now parse the problems for per-test results 565 pio = StringIO.StringIO(problems) 566 pio.readline() # eat the first separator line 567 testname = None 568 done = False 569 while not done: 570 while 1: 571 line = pio.readline() 572 if line == "": 573 done = True 574 break 575 if line.find("=" * 60) == 0: 576 break 577 if line.find("-" * 60) == 0: 578 # the last case has --- as a separator before the 579 # summary counts are printed 580 done = True 581 break 582 if testname is None: 583 # the first line after the === is like: 584 # EXPECTED FAILURE: testLackOfTB (twisted.test.test_failure.FailureTestCase) 585 # SKIPPED: testRETR (twisted.test.test_ftp.TestFTPServer) 586 # FAILURE: testBatchFile (twisted.conch.test.test_sftp.TestOurServerBatchFile) 587 r = re.search(r'^([^:]+): (\w+) \(([\w\.]+)\)', line) 588 if not r: 589 # TODO: cleanup, if there are no problems, 590 # we hit here 591 continue 592 result, name, case = r.groups() 593 testname = tuple(case.split(".") + [name]) 594 results = {'SKIPPED': SKIPPED, 595 'EXPECTED FAILURE': SUCCESS, 596 'UNEXPECTED SUCCESS': WARNINGS, 597 'FAILURE': FAILURE, 598 'ERROR': FAILURE, 599 'SUCCESS': SUCCESS, # not reported 600 }.get(result, WARNINGS) 601 text = result.lower().split() 602 loog = line 603 # the next line is all dashes 604 loog += pio.readline() 605 else: 606 # the rest goes into the log 607 loog += line 608 if testname: 609 self.addTestResult(testname, results, text, loog) 610 testname = None 611 612 if warnings: 613 lines = warnings.keys() 614 lines.sort() 615 self.addCompleteLog("warnings", "".join(lines))
616
617 - def evaluateCommand(self, cmd):
618 return self.results
619
620 - def getText(self, cmd, results):
621 return self.text
622 - def getText2(self, cmd, results):
623 return self.text2
624 625
626 -class RemovePYCs(ShellCommand):
627 name = "remove-.pyc" 628 command = ['find', '.', '-name', '*.pyc', '-exec', 'rm', '{}', ';'] 629 description = ["removing", ".pyc", "files"] 630 descriptionDone = ["remove", ".pycs"]
631