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

Source Code for Module buildbot.steps.python_twisted

  1  # -*- test-case-name: buildbot.test.test_twisted -*- 
  2   
  3  from twisted.python import log 
  4   
  5  from buildbot.status import builder 
  6  from buildbot.status.builder import SUCCESS, FAILURE, WARNINGS, SKIPPED 
  7  from buildbot.process.buildstep import LogLineObserver, OutputProgressObserver 
  8  from buildbot.process.buildstep import RemoteShellCommand 
  9  from buildbot.steps.shell import ShellCommand 
 10   
 11  try: 
 12      import cStringIO 
 13      StringIO = cStringIO 
 14  except ImportError: 
 15      import StringIO 
 16  import re 
 17   
 18  # BuildSteps that are specific to the Twisted source tree 
 19   
20 -class HLint(ShellCommand):
21 """I run a 'lint' checker over a set of .xhtml files. Any deviations 22 from recommended style is flagged and put in the output log. 23 24 This step looks at .changes in the parent Build to extract a list of 25 Lore XHTML files to check.""" 26 27 name = "hlint" 28 description = ["running", "hlint"] 29 descriptionDone = ["hlint"] 30 warnOnWarnings = True 31 warnOnFailure = True 32 # TODO: track time, but not output 33 warnings = 0 34
35 - def __init__(self, python=None, **kwargs):
36 ShellCommand.__init__(self, **kwargs) 37 self.addFactoryArguments(python=python) 38 self.python = python
39
40 - def start(self):
41 # create the command 42 htmlFiles = {} 43 for f in self.build.allFiles(): 44 if f.endswith(".xhtml") and not f.startswith("sandbox/"): 45 htmlFiles[f] = 1 46 # remove duplicates 47 hlintTargets = htmlFiles.keys() 48 hlintTargets.sort() 49 if not hlintTargets: 50 return SKIPPED 51 self.hlintFiles = hlintTargets 52 c = [] 53 if self.python: 54 c.append(self.python) 55 c += ["bin/lore", "-p", "--output", "lint"] + self.hlintFiles 56 self.setCommand(c) 57 58 # add an extra log file to show the .html files we're checking 59 self.addCompleteLog("files", "\n".join(self.hlintFiles)+"\n") 60 61 ShellCommand.start(self)
62
63 - def commandComplete(self, cmd):
64 # TODO: remove the 'files' file (a list of .xhtml files that were 65 # submitted to hlint) because it is available in the logfile and 66 # mostly exists to give the user an idea of how long the step will 67 # take anyway). 68 lines = cmd.logs['stdio'].getText().split("\n") 69 warningLines = filter(lambda line:':' in line, lines) 70 if warningLines: 71 self.addCompleteLog("warnings", "".join(warningLines)) 72 warnings = len(warningLines) 73 self.warnings = warnings
74
75 - def evaluateCommand(self, cmd):
76 # warnings are in stdout, rc is always 0, unless the tools break 77 if cmd.rc != 0: 78 return FAILURE 79 if self.warnings: 80 return WARNINGS 81 return SUCCESS
82
83 - def getText2(self, cmd, results):
84 if cmd.rc != 0: 85 return ["hlint"] 86 return ["%d hlin%s" % (self.warnings, 87 self.warnings == 1 and 't' or 'ts')]
88
89 -def countFailedTests(output):
90 # start scanning 10kb from the end, because there might be a few kb of 91 # import exception tracebacks between the total/time line and the errors 92 # line 93 chunk = output[-10000:] 94 lines = chunk.split("\n") 95 lines.pop() # blank line at end 96 # lines[-3] is "Ran NN tests in 0.242s" 97 # lines[-2] is blank 98 # lines[-1] is 'OK' or 'FAILED (failures=1, errors=12)' 99 # or 'FAILED (failures=1)' 100 # or "PASSED (skips=N, successes=N)" (for Twisted-2.0) 101 # there might be other lines dumped here. Scan all the lines. 102 res = {'total': None, 103 'failures': 0, 104 'errors': 0, 105 'skips': 0, 106 'expectedFailures': 0, 107 'unexpectedSuccesses': 0, 108 } 109 for l in lines: 110 out = re.search(r'Ran (\d+) tests', l) 111 if out: 112 res['total'] = int(out.group(1)) 113 if (l.startswith("OK") or 114 l.startswith("FAILED ") or 115 l.startswith("PASSED")): 116 # the extra space on FAILED_ is to distinguish the overall 117 # status from an individual test which failed. The lack of a 118 # space on the OK is because it may be printed without any 119 # additional text (if there are no skips,etc) 120 out = re.search(r'failures=(\d+)', l) 121 if out: res['failures'] = int(out.group(1)) 122 out = re.search(r'errors=(\d+)', l) 123 if out: res['errors'] = int(out.group(1)) 124 out = re.search(r'skips=(\d+)', l) 125 if out: res['skips'] = int(out.group(1)) 126 out = re.search(r'expectedFailures=(\d+)', l) 127 if out: res['expectedFailures'] = int(out.group(1)) 128 out = re.search(r'unexpectedSuccesses=(\d+)', l) 129 if out: res['unexpectedSuccesses'] = int(out.group(1)) 130 # successes= is a Twisted-2.0 addition, and is not currently used 131 out = re.search(r'successes=(\d+)', l) 132 if out: res['successes'] = int(out.group(1)) 133 134 return res
135 136
137 -class TrialTestCaseCounter(LogLineObserver):
138 _line_re = re.compile(r'^(?:Doctest: )?([\w\.]+) \.\.\. \[([^\]]+)\]$') 139 numTests = 0 140 finished = False 141
142 - def outLineReceived(self, line):
143 # different versions of Twisted emit different per-test lines with 144 # the bwverbose reporter. 145 # 2.0.0: testSlave (buildbot.test.test_runner.Create) ... [OK] 146 # 2.1.0: buildbot.test.test_runner.Create.testSlave ... [OK] 147 # 2.4.0: buildbot.test.test_runner.Create.testSlave ... [OK] 148 # Let's just handle the most recent version, since it's the easiest. 149 # Note that doctests create lines line this: 150 # Doctest: viff.field.GF ... [OK] 151 152 if self.finished: 153 return 154 if line.startswith("=" * 40): 155 self.finished = True 156 return 157 158 m = self._line_re.search(line.strip()) 159 if m: 160 testname, result = m.groups() 161 self.numTests += 1 162 self.step.setProgress('tests', self.numTests)
163 164 165 UNSPECIFIED=() # since None is a valid choice 166
167 -class Trial(ShellCommand):
168 """I run a unit test suite using 'trial', a unittest-like testing 169 framework that comes with Twisted. Trial is used to implement Twisted's 170 own unit tests, and is the unittest-framework of choice for many projects 171 that use Twisted internally. 172 173 Projects that use trial typically have all their test cases in a 'test' 174 subdirectory of their top-level library directory. I.e. for my package 175 'petmail', the tests are in 'petmail/test/test_*.py'. More complicated 176 packages (like Twisted itself) may have multiple test directories, like 177 'twisted/test/test_*.py' for the core functionality and 178 'twisted/mail/test/test_*.py' for the email-specific tests. 179 180 To run trial tests, you run the 'trial' executable and tell it where the 181 test cases are located. The most common way of doing this is with a 182 module name. For petmail, I would run 'trial petmail.test' and it would 183 locate all the test_*.py files under petmail/test/, running every test 184 case it could find in them. Unlike the unittest.py that comes with 185 Python, you do not run the test_foo.py as a script; you always let trial 186 do the importing and running. The 'tests' parameter controls which tests 187 trial will run: it can be a string or a list of strings. 188 189 To find these test cases, you must set a PYTHONPATH that allows something 190 like 'import petmail.test' to work. For packages that don't use a 191 separate top-level 'lib' directory, PYTHONPATH=. will work, and will use 192 the test cases (and the code they are testing) in-place. 193 PYTHONPATH=build/lib or PYTHONPATH=build/lib.$ARCH are also useful when 194 you do a'setup.py build' step first. The 'testpath' attribute of this 195 class controls what PYTHONPATH= is set to. 196 197 Trial has the ability (through the --testmodule flag) to run only the set 198 of test cases named by special 'test-case-name' tags in source files. We 199 can get the list of changed source files from our parent Build and 200 provide them to trial, thus running the minimal set of test cases needed 201 to cover the Changes. This is useful for quick builds, especially in 202 trees with a lot of test cases. The 'testChanges' parameter controls this 203 feature: if set, it will override 'tests'. 204 205 The trial executable itself is typically just 'trial' (which is usually 206 found on your $PATH as /usr/bin/trial), but it can be overridden with the 207 'trial' parameter. This is useful for Twisted's own unittests, which want 208 to use the copy of bin/trial that comes with the sources. (when bin/trial 209 discovers that it is living in a subdirectory named 'Twisted', it assumes 210 it is being run from the source tree and adds that parent directory to 211 PYTHONPATH. Therefore the canonical way to run Twisted's own unittest 212 suite is './bin/trial twisted.test' rather than 'PYTHONPATH=. 213 /usr/bin/trial twisted.test', especially handy when /usr/bin/trial has 214 not yet been installed). 215 216 To influence the version of python being used for the tests, or to add 217 flags to the command, set the 'python' parameter. This can be a string 218 (like 'python2.2') or a list (like ['python2.3', '-Wall']). 219 220 Trial creates and switches into a directory named _trial_temp/ before 221 running the tests, and sends the twisted log (which includes all 222 exceptions) to a file named test.log . This file will be pulled up to 223 the master where it can be seen as part of the status output. 224 225 There are some class attributes which may be usefully overridden 226 by subclasses. 'trialMode' and 'trialArgs' can influence the trial 227 command line. 228 """ 229 230 name = "trial" 231 progressMetrics = ('output', 'tests', 'test.log') 232 # note: the slash only works on unix buildslaves, of course, but we have 233 # no way to know what the buildslave uses as a separator. 234 # TODO: figure out something clever. 235 logfiles = {"test.log": "_trial_temp/test.log"} 236 # we use test.log to track Progress at the end of __init__() 237 238 flunkOnFailure = True 239 python = None 240 trial = "trial" 241 trialMode = ["--reporter=bwverbose"] # requires Twisted-2.1.0 or newer 242 # for Twisted-2.0.0 or 1.3.0, use ["-o"] instead 243 trialArgs = [] 244 testpath = UNSPECIFIED # required (but can be None) 245 testChanges = False # TODO: needs better name 246 recurse = False 247 reactor = None 248 randomly = False 249 tests = None # required 250
251 - def __init__(self, reactor=UNSPECIFIED, python=None, trial=None, 252 testpath=UNSPECIFIED, 253 tests=None, testChanges=None, 254 recurse=None, randomly=None, 255 trialMode=None, trialArgs=None, 256 **kwargs):
257 """ 258 @type testpath: string 259 @param testpath: use in PYTHONPATH when running the tests. If 260 None, do not set PYTHONPATH. Setting this to '.' will 261 cause the source files to be used in-place. 262 263 @type python: string (without spaces) or list 264 @param python: which python executable to use. Will form the start of 265 the argv array that will launch trial. If you use this, 266 you should set 'trial' to an explicit path (like 267 /usr/bin/trial or ./bin/trial). Defaults to None, which 268 leaves it out entirely (running 'trial args' instead of 269 'python ./bin/trial args'). Likely values are 'python', 270 ['python2.2'], ['python', '-Wall'], etc. 271 272 @type trial: string 273 @param trial: which 'trial' executable to run. 274 Defaults to 'trial', which will cause $PATH to be 275 searched and probably find /usr/bin/trial . If you set 276 'python', this should be set to an explicit path (because 277 'python2.3 trial' will not work). 278 279 @type trialMode: list of strings 280 @param trialMode: a list of arguments to pass to trial, specifically 281 to set the reporting mode. This defaults to ['-to'] 282 which means 'verbose colorless output' to the trial 283 that comes with Twisted-2.0.x and at least -2.1.0 . 284 Newer versions of Twisted may come with a trial 285 that prefers ['--reporter=bwverbose']. 286 287 @type trialArgs: list of strings 288 @param trialArgs: a list of arguments to pass to trial, available to 289 turn on any extra flags you like. Defaults to []. 290 291 @type tests: list of strings 292 @param tests: a list of test modules to run, like 293 ['twisted.test.test_defer', 'twisted.test.test_process']. 294 If this is a string, it will be converted into a one-item 295 list. 296 297 @type testChanges: boolean 298 @param testChanges: if True, ignore the 'tests' parameter and instead 299 ask the Build for all the files that make up the 300 Changes going into this build. Pass these filenames 301 to trial and ask it to look for test-case-name 302 tags, running just the tests necessary to cover the 303 changes. 304 305 @type recurse: boolean 306 @param recurse: If True, pass the --recurse option to trial, allowing 307 test cases to be found in deeper subdirectories of the 308 modules listed in 'tests'. This does not appear to be 309 necessary when using testChanges. 310 311 @type reactor: string 312 @param reactor: which reactor to use, like 'gtk' or 'java'. If not 313 provided, the Twisted's usual platform-dependent 314 default is used. 315 316 @type randomly: boolean 317 @param randomly: if True, add the --random=0 argument, which instructs 318 trial to run the unit tests in a random order each 319 time. This occasionally catches problems that might be 320 masked when one module always runs before another 321 (like failing to make registerAdapter calls before 322 lookups are done). 323 324 @type kwargs: dict 325 @param kwargs: parameters. The following parameters are inherited from 326 L{ShellCommand} and may be useful to set: workdir, 327 haltOnFailure, flunkOnWarnings, flunkOnFailure, 328 warnOnWarnings, warnOnFailure, want_stdout, want_stderr, 329 timeout. 330 """ 331 ShellCommand.__init__(self, **kwargs) 332 self.addFactoryArguments(reactor=reactor, 333 python=python, 334 trial=trial, 335 testpath=testpath, 336 tests=tests, 337 testChanges=testChanges, 338 recurse=recurse, 339 randomly=randomly, 340 trialMode=trialMode, 341 trialArgs=trialArgs, 342 ) 343 344 if python: 345 self.python = python 346 if self.python is not None: 347 if type(self.python) is str: 348 self.python = [self.python] 349 for s in self.python: 350 if " " in s: 351 # this is not strictly an error, but I suspect more 352 # people will accidentally try to use python="python2.3 353 # -Wall" than will use embedded spaces in a python flag 354 log.msg("python= component '%s' has spaces") 355 log.msg("To add -Wall, use python=['python', '-Wall']") 356 why = "python= value has spaces, probably an error" 357 raise ValueError(why) 358 359 if trial: 360 self.trial = trial 361 if " " in self.trial: 362 raise ValueError("trial= value has spaces") 363 if trialMode is not None: 364 self.trialMode = trialMode 365 if trialArgs is not None: 366 self.trialArgs = trialArgs 367 368 if testpath is not UNSPECIFIED: 369 self.testpath = testpath 370 if self.testpath is UNSPECIFIED: 371 raise ValueError("You must specify testpath= (it can be None)") 372 assert isinstance(self.testpath, str) or self.testpath is None 373 374 if reactor is not UNSPECIFIED: 375 self.reactor = reactor 376 377 if tests is not None: 378 self.tests = tests 379 if type(self.tests) is str: 380 self.tests = [self.tests] 381 if testChanges is not None: 382 self.testChanges = testChanges 383 #self.recurse = True # not sure this is necessary 384 385 if not self.testChanges and self.tests is None: 386 raise ValueError("Must either set testChanges= or provide tests=") 387 388 if recurse is not None: 389 self.recurse = recurse 390 if randomly is not None: 391 self.randomly = randomly 392 393 # build up most of the command, then stash it until start() 394 command = [] 395 if self.python: 396 command.extend(self.python) 397 command.append(self.trial) 398 command.extend(self.trialMode) 399 if self.recurse: 400 command.append("--recurse") 401 if self.reactor: 402 command.append("--reactor=%s" % reactor) 403 if self.randomly: 404 command.append("--random=0") 405 command.extend(self.trialArgs) 406 self.command = command 407 408 if self.reactor: 409 self.description = ["testing", "(%s)" % self.reactor] 410 self.descriptionDone = ["tests"] 411 # commandComplete adds (reactorname) to self.text 412 else: 413 self.description = ["testing"] 414 self.descriptionDone = ["tests"] 415 416 # this counter will feed Progress along the 'test cases' metric 417 self.addLogObserver('stdio', TrialTestCaseCounter()) 418 # this one just measures bytes of output in _trial_temp/test.log 419 self.addLogObserver('test.log', OutputProgressObserver('test.log'))
420
421 - def setupEnvironment(self, cmd):
422 ShellCommand.setupEnvironment(self, cmd) 423 if self.testpath != None: 424 e = cmd.args['env'] 425 if e is None: 426 cmd.args['env'] = {'PYTHONPATH': self.testpath} 427 else: 428 # TODO: somehow, each build causes another copy of 429 # self.testpath to get prepended 430 if e.get('PYTHONPATH', "") == "": 431 e['PYTHONPATH'] = self.testpath 432 else: 433 e['PYTHONPATH'] = self.testpath + ":" + e['PYTHONPATH'] 434 try: 435 p = cmd.args['env']['PYTHONPATH'] 436 if type(p) is not str: 437 log.msg("hey, not a string:", p) 438 assert False 439 except (KeyError, TypeError): 440 # KeyError if args doesn't have ['env'] 441 # KeyError if args['env'] doesn't have ['PYTHONPATH'] 442 # TypeError if args is None 443 pass
444
445 - def start(self):
446 # now that self.build.allFiles() is nailed down, finish building the 447 # command 448 if self.testChanges: 449 for f in self.build.allFiles(): 450 if f.endswith(".py"): 451 self.command.append("--testmodule=%s" % f) 452 else: 453 self.command.extend(self.tests) 454 log.msg("Trial.start: command is", self.command) 455 456 # if our slave is too old to understand logfiles=, fetch them 457 # manually. This is a fallback for the Twisted buildbot and some old 458 # buildslaves. 459 self._needToPullTestDotLog = False 460 if self.slaveVersionIsOlderThan("shell", "2.1"): 461 log.msg("Trial: buildslave %s is too old to accept logfiles=" % 462 self.getSlaveName()) 463 log.msg(" falling back to 'cat _trial_temp/test.log' instead") 464 self.logfiles = {} 465 self._needToPullTestDotLog = True 466 467 ShellCommand.start(self)
468 469
470 - def commandComplete(self, cmd):
471 if not self._needToPullTestDotLog: 472 return self._gotTestDotLog(cmd) 473 474 # if the buildslave was too old, pull test.log now 475 catcmd = ["cat", "_trial_temp/test.log"] 476 c2 = RemoteShellCommand(command=catcmd, workdir=self.workdir) 477 loog = self.addLog("test.log") 478 c2.useLog(loog, True, logfileName="stdio") 479 self.cmd = c2 # to allow interrupts 480 d = c2.run(self, self.remote) 481 d.addCallback(lambda res: self._gotTestDotLog(cmd)) 482 return d
483
484 - def rtext(self, fmt='%s'):
485 if self.reactor: 486 rtext = fmt % self.reactor 487 return rtext.replace("reactor", "") 488 return ""
489
490 - def _gotTestDotLog(self, cmd):
491 # figure out all status, then let the various hook functions return 492 # different pieces of it 493 494 # 'cmd' is the original trial command, so cmd.logs['stdio'] is the 495 # trial output. We don't have access to test.log from here. 496 output = cmd.logs['stdio'].getText() 497 counts = countFailedTests(output) 498 499 total = counts['total'] 500 failures, errors = counts['failures'], counts['errors'] 501 parsed = (total != None) 502 text = [] 503 text2 = "" 504 505 if cmd.rc == 0: 506 if parsed: 507 results = SUCCESS 508 if total: 509 text += ["%d %s" % \ 510 (total, 511 total == 1 and "test" or "tests"), 512 "passed"] 513 else: 514 text += ["no tests", "run"] 515 else: 516 results = FAILURE 517 text += ["testlog", "unparseable"] 518 text2 = "tests" 519 else: 520 # something failed 521 results = FAILURE 522 if parsed: 523 text.append("tests") 524 if failures: 525 text.append("%d %s" % \ 526 (failures, 527 failures == 1 and "failure" or "failures")) 528 if errors: 529 text.append("%d %s" % \ 530 (errors, 531 errors == 1 and "error" or "errors")) 532 count = failures + errors 533 text2 = "%d tes%s" % (count, (count == 1 and 't' or 'ts')) 534 else: 535 text += ["tests", "failed"] 536 text2 = "tests" 537 538 if counts['skips']: 539 text.append("%d %s" % \ 540 (counts['skips'], 541 counts['skips'] == 1 and "skip" or "skips")) 542 if counts['expectedFailures']: 543 text.append("%d %s" % \ 544 (counts['expectedFailures'], 545 counts['expectedFailures'] == 1 and "todo" 546 or "todos")) 547 if 0: # TODO 548 results = WARNINGS 549 if not text2: 550 text2 = "todo" 551 552 if 0: 553 # ignore unexpectedSuccesses for now, but it should really mark 554 # the build WARNING 555 if counts['unexpectedSuccesses']: 556 text.append("%d surprises" % counts['unexpectedSuccesses']) 557 results = WARNINGS 558 if not text2: 559 text2 = "tests" 560 561 if self.reactor: 562 text.append(self.rtext('(%s)')) 563 if text2: 564 text2 = "%s %s" % (text2, self.rtext('(%s)')) 565 566 self.results = results 567 self.text = text 568 self.text2 = [text2]
569
570 - def addTestResult(self, testname, results, text, tlog):
571 if self.reactor is not None: 572 testname = (self.reactor,) + testname 573 tr = builder.TestResult(testname, results, text, logs={'log': tlog}) 574 #self.step_status.build.addTestResult(tr) 575 self.build.build_status.addTestResult(tr)
576
577 - def createSummary(self, loog):
578 output = loog.getText() 579 problems = "" 580 sio = StringIO.StringIO(output) 581 warnings = {} 582 while 1: 583 line = sio.readline() 584 if line == "": 585 break 586 if line.find(" exceptions.DeprecationWarning: ") != -1: 587 # no source 588 warning = line # TODO: consider stripping basedir prefix here 589 warnings[warning] = warnings.get(warning, 0) + 1 590 elif (line.find(" DeprecationWarning: ") != -1 or 591 line.find(" UserWarning: ") != -1): 592 # next line is the source 593 warning = line + sio.readline() 594 warnings[warning] = warnings.get(warning, 0) + 1 595 elif line.find("Warning: ") != -1: 596 warning = line 597 warnings[warning] = warnings.get(warning, 0) + 1 598 599 if line.find("=" * 60) == 0 or line.find("-" * 60) == 0: 600 problems += line 601 problems += sio.read() 602 break 603 604 if problems: 605 self.addCompleteLog("problems", problems) 606 # now parse the problems for per-test results 607 pio = StringIO.StringIO(problems) 608 pio.readline() # eat the first separator line 609 testname = None 610 done = False 611 while not done: 612 while 1: 613 line = pio.readline() 614 if line == "": 615 done = True 616 break 617 if line.find("=" * 60) == 0: 618 break 619 if line.find("-" * 60) == 0: 620 # the last case has --- as a separator before the 621 # summary counts are printed 622 done = True 623 break 624 if testname is None: 625 # the first line after the === is like: 626 # EXPECTED FAILURE: testLackOfTB (twisted.test.test_failure.FailureTestCase) 627 # SKIPPED: testRETR (twisted.test.test_ftp.TestFTPServer) 628 # FAILURE: testBatchFile (twisted.conch.test.test_sftp.TestOurServerBatchFile) 629 r = re.search(r'^([^:]+): (\w+) \(([\w\.]+)\)', line) 630 if not r: 631 # TODO: cleanup, if there are no problems, 632 # we hit here 633 continue 634 result, name, case = r.groups() 635 testname = tuple(case.split(".") + [name]) 636 results = {'SKIPPED': SKIPPED, 637 'EXPECTED FAILURE': SUCCESS, 638 'UNEXPECTED SUCCESS': WARNINGS, 639 'FAILURE': FAILURE, 640 'ERROR': FAILURE, 641 'SUCCESS': SUCCESS, # not reported 642 }.get(result, WARNINGS) 643 text = result.lower().split() 644 loog = line 645 # the next line is all dashes 646 loog += pio.readline() 647 else: 648 # the rest goes into the log 649 loog += line 650 if testname: 651 self.addTestResult(testname, results, text, loog) 652 testname = None 653 654 if warnings: 655 lines = warnings.keys() 656 lines.sort() 657 self.addCompleteLog("warnings", "".join(lines))
658
659 - def evaluateCommand(self, cmd):
660 return self.results
661
662 - def getText(self, cmd, results):
663 return self.text
664 - def getText2(self, cmd, results):
665 return self.text2
666 667
668 -class ProcessDocs(ShellCommand):
669 """I build all docs. This requires some LaTeX packages to be installed. 670 It will result in the full documentation book (dvi, pdf, etc). 671 672 """ 673 674 name = "process-docs" 675 warnOnWarnings = 1 676 command = ["admin/process-docs"] 677 description = ["processing", "docs"] 678 descriptionDone = ["docs"] 679 # TODO: track output and time 680
681 - def __init__(self, **kwargs):
682 """ 683 @type workdir: string 684 @keyword workdir: the workdir to start from: must be the base of the 685 Twisted tree 686 """ 687 ShellCommand.__init__(self, **kwargs)
688
689 - def createSummary(self, log):
690 output = log.getText() 691 # hlint warnings are of the format: 'WARNING: file:line:col: stuff 692 # latex warnings start with "WARNING: LaTeX Warning: stuff", but 693 # sometimes wrap around to a second line. 694 lines = output.split("\n") 695 warningLines = [] 696 wantNext = False 697 for line in lines: 698 wantThis = wantNext 699 wantNext = False 700 if line.startswith("WARNING: "): 701 wantThis = True 702 wantNext = True 703 if wantThis: 704 warningLines.append(line) 705 706 if warningLines: 707 self.addCompleteLog("warnings", "\n".join(warningLines) + "\n") 708 self.warnings = len(warningLines)
709
710 - def evaluateCommand(self, cmd):
711 if cmd.rc != 0: 712 return FAILURE 713 if self.warnings: 714 return WARNINGS 715 return SUCCESS
716
717 - def getText(self, cmd, results):
718 if results == SUCCESS: 719 return ["docs", "successful"] 720 if results == WARNINGS: 721 return ["docs", 722 "%d warnin%s" % (self.warnings, 723 self.warnings == 1 and 'g' or 'gs')] 724 if results == FAILURE: 725 return ["docs", "failed"]
726
727 - def getText2(self, cmd, results):
728 if results == WARNINGS: 729 return ["%d do%s" % (self.warnings, 730 self.warnings == 1 and 'c' or 'cs')] 731 return ["docs"]
732 733 734
735 -class BuildDebs(ShellCommand):
736 """I build the .deb packages.""" 737 738 name = "debuild" 739 flunkOnFailure = 1 740 command = ["debuild", "-uc", "-us"] 741 description = ["building", "debs"] 742 descriptionDone = ["debs"] 743
744 - def __init__(self, **kwargs):
745 """ 746 @type workdir: string 747 @keyword workdir: the workdir to start from (must be the base of the 748 Twisted tree) 749 """ 750 ShellCommand.__init__(self, **kwargs)
751
752 - def commandComplete(self, cmd):
753 errors, warnings = 0, 0 754 output = cmd.logs['stdio'].getText() 755 summary = "" 756 sio = StringIO.StringIO(output) 757 for line in sio.readlines(): 758 if line.find("E: ") == 0: 759 summary += line 760 errors += 1 761 if line.find("W: ") == 0: 762 summary += line 763 warnings += 1 764 if summary: 765 self.addCompleteLog("problems", summary) 766 self.errors = errors 767 self.warnings = warnings
768
769 - def evaluateCommand(self, cmd):
770 if cmd.rc != 0: 771 return FAILURE 772 if self.errors: 773 return FAILURE 774 if self.warnings: 775 return WARNINGS 776 return SUCCESS
777
778 - def getText(self, cmd, results):
779 text = ["debuild"] 780 if cmd.rc != 0: 781 text.append("failed") 782 errors, warnings = self.errors, self.warnings 783 if warnings or errors: 784 text.append("lintian:") 785 if warnings: 786 text.append("%d warnin%s" % (warnings, 787 warnings == 1 and 'g' or 'gs')) 788 if errors: 789 text.append("%d erro%s" % (errors, 790 errors == 1 and 'r' or 'rs')) 791 return text
792
793 - def getText2(self, cmd, results):
794 if cmd.rc != 0: 795 return ["debuild"] 796 if self.errors or self.warnings: 797 return ["%d lintian" % (self.errors + self.warnings)] 798 return []
799
800 -class RemovePYCs(ShellCommand):
801 name = "remove-.pyc" 802 command = 'find . -name "*.pyc" | xargs rm' 803 description = ["removing", ".pyc", "files"] 804 descriptionDone = ["remove", ".pycs"]
805