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

Source Code for Module buildbot.steps.shell

  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  import re 
 18  from twisted.python import log 
 19  from twisted.spread import pb 
 20  from buildbot.process.buildstep import LoggingBuildStep, RemoteShellCommand 
 21  from buildbot.process.buildstep import RemoteCommand 
 22  from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, STDOUT, STDERR 
 23  from buildbot.interfaces import BuildSlaveTooOldError 
 24   
 25  # for existing configurations that import WithProperties from here.  We like 
 26  # to move this class around just to keep our readers guessing. 
 27  from buildbot.process.properties import WithProperties 
 28  _hush_pyflakes = [WithProperties] 
 29  del _hush_pyflakes 
 30   
31 -class ShellCommand(LoggingBuildStep):
32 """I run a single shell command on the buildslave. I return FAILURE if 33 the exit code of that command is non-zero, SUCCESS otherwise. To change 34 this behavior, override my .evaluateCommand method. 35 36 By default, a failure of this step will mark the whole build as FAILURE. 37 To override this, give me an argument of flunkOnFailure=False . 38 39 I create a single Log named 'log' which contains the output of the 40 command. To create additional summary Logs, override my .createSummary 41 method. 42 43 The shell command I run (a list of argv strings) can be provided in 44 several ways: 45 - a class-level .command attribute 46 - a command= parameter to my constructor (overrides .command) 47 - set explicitly with my .setCommand() method (overrides both) 48 49 @ivar command: a list of renderable objects (typically strings or 50 WithProperties instances). This will be used by start() 51 to create a RemoteShellCommand instance. 52 53 @ivar logfiles: a dict mapping log NAMEs to workdir-relative FILENAMEs 54 of their corresponding logfiles. The contents of the file 55 named FILENAME will be put into a LogFile named NAME, ina 56 something approximating real-time. (note that logfiles= 57 is actually handled by our parent class LoggingBuildStep) 58 59 @ivar lazylogfiles: Defaults to False. If True, logfiles will be tracked 60 `lazily', meaning they will only be added when and if 61 they are written to. Empty or nonexistent logfiles 62 will be omitted. (Also handled by class 63 LoggingBuildStep.) 64 65 """ 66 67 name = "shell" 68 description = None # set this to a list of short strings to override 69 descriptionDone = None # alternate description when the step is complete 70 command = None # set this to a command, or set in kwargs 71 # logfiles={} # you can also set 'logfiles' to a dictionary, and it 72 # will be merged with any logfiles= argument passed in 73 # to __init__ 74 75 # override this on a specific ShellCommand if you want to let it fail 76 # without dooming the entire build to a status of FAILURE 77 flunkOnFailure = True 78
79 - def __init__(self, workdir=None, 80 description=None, descriptionDone=None, 81 command=None, 82 usePTY="slave-config", 83 **kwargs):
84 # most of our arguments get passed through to the RemoteShellCommand 85 # that we create, but first strip out the ones that we pass to 86 # BuildStep (like haltOnFailure and friends), and a couple that we 87 # consume ourselves. 88 89 if description: 90 self.description = description 91 if isinstance(self.description, str): 92 self.description = [self.description] 93 if descriptionDone: 94 self.descriptionDone = descriptionDone 95 if isinstance(self.descriptionDone, str): 96 self.descriptionDone = [self.descriptionDone] 97 if command: 98 self.setCommand(command) 99 100 # pull out the ones that LoggingBuildStep wants, then upcall 101 buildstep_kwargs = {} 102 for k in kwargs.keys()[:]: 103 if k in self.__class__.parms: 104 buildstep_kwargs[k] = kwargs[k] 105 del kwargs[k] 106 LoggingBuildStep.__init__(self, **buildstep_kwargs) 107 self.addFactoryArguments(workdir=workdir, 108 description=description, 109 descriptionDone=descriptionDone, 110 command=command) 111 112 # everything left over goes to the RemoteShellCommand 113 kwargs['workdir'] = workdir # including a copy of 'workdir' 114 kwargs['usePTY'] = usePTY 115 self.remote_kwargs = kwargs 116 # we need to stash the RemoteShellCommand's args too 117 self.addFactoryArguments(**kwargs)
118
119 - def setStepStatus(self, step_status):
121
122 - def setDefaultWorkdir(self, workdir):
123 rkw = self.remote_kwargs 124 rkw['workdir'] = rkw['workdir'] or workdir
125
126 - def setCommand(self, command):
127 self.command = command
128
129 - def describe(self, done=False):
130 """Return a list of short strings to describe this step, for the 131 status display. This uses the first few words of the shell command. 132 You can replace this by setting .description in your subclass, or by 133 overriding this method to describe the step better. 134 135 @type done: boolean 136 @param done: whether the command is complete or not, to improve the 137 way the command is described. C{done=False} is used 138 while the command is still running, so a single 139 imperfect-tense verb is appropriate ('compiling', 140 'testing', ...) C{done=True} is used when the command 141 has finished, and the default getText() method adds some 142 text, so a simple noun is appropriate ('compile', 143 'tests' ...) 144 """ 145 146 try: 147 properties = self.build.getProperties() 148 149 if done and self.descriptionDone is not None: 150 return properties.render(self.descriptionDone) 151 if self.description is not None: 152 return properties.render(self.description) 153 154 # we may have no command if this is a step that sets its command 155 # name late in the game (e.g., in start()) 156 if not self.command: 157 return ["???"] 158 159 words = self.command 160 if isinstance(words, (str, unicode)): 161 words = words.split() 162 # render() each word to handle WithProperties objects 163 words = properties.render(words) 164 if len(words) < 1: 165 return ["???"] 166 if len(words) == 1: 167 return ["'%s'" % words[0]] 168 if len(words) == 2: 169 return ["'%s" % words[0], "%s'" % words[1]] 170 return ["'%s" % words[0], "%s" % words[1], "...'"] 171 except: 172 log.msg("Error describing step") 173 log.err() 174 return ["???"]
175
176 - def setupEnvironment(self, cmd):
177 # merge in anything from Build.slaveEnvironment 178 # This can be set from a Builder-level environment, or from earlier 179 # BuildSteps. The latter method is deprecated and superceded by 180 # BuildProperties. 181 # Environment variables passed in by a BuildStep override 182 # those passed in at the Builder level. 183 properties = self.build.getProperties() 184 slaveEnv = self.build.slaveEnvironment 185 if slaveEnv: 186 if cmd.args['env'] is None: 187 cmd.args['env'] = {} 188 fullSlaveEnv = slaveEnv.copy() 189 fullSlaveEnv.update(cmd.args['env']) 190 cmd.args['env'] = properties.render(fullSlaveEnv)
191 # note that each RemoteShellCommand gets its own copy of the 192 # dictionary, so we shouldn't be affecting anyone but ourselves. 193
195 if not self.logfiles: 196 return # doesn't matter 197 if not self.slaveVersionIsOlderThan("shell", "2.1"): 198 return # slave is new enough 199 # this buildslave is too old and will ignore the 'logfiles' 200 # argument. You'll either have to pull the logfiles manually 201 # (say, by using 'cat' in a separate RemoteShellCommand) or 202 # upgrade the buildslave. 203 msg1 = ("Warning: buildslave %s is too old " 204 "to understand logfiles=, ignoring it." 205 % self.getSlaveName()) 206 msg2 = "You will have to pull this logfile (%s) manually." 207 log.msg(msg1) 208 for logname,remotefilevalue in self.logfiles.items(): 209 remotefilename = remotefilevalue 210 # check for a dictionary of options 211 if type(remotefilevalue) == dict: 212 remotefilename = remotefilevalue['filename'] 213 214 newlog = self.addLog(logname) 215 newlog.addHeader(msg1 + "\n") 216 newlog.addHeader(msg2 % remotefilename + "\n") 217 newlog.finish() 218 # now prevent setupLogfiles() from adding them 219 self.logfiles = {}
220
221 - def start(self):
222 # this block is specific to ShellCommands. subclasses that don't need 223 # to set up an argv array, an environment, or extra logfiles= (like 224 # the Source subclasses) can just skip straight to startCommand() 225 properties = self.build.getProperties() 226 227 warnings = [] 228 229 # create the actual RemoteShellCommand instance now 230 kwargs = properties.render(self.remote_kwargs) 231 command = properties.render(self.command) 232 kwargs['command'] = command 233 kwargs['logfiles'] = self.logfiles 234 235 # check for the usePTY flag 236 if kwargs.has_key('usePTY') and kwargs['usePTY'] != 'slave-config': 237 if self.slaveVersionIsOlderThan("svn", "2.7"): 238 warnings.append("NOTE: slave does not allow master to override usePTY\n") 239 240 cmd = RemoteShellCommand(**kwargs) 241 self.setupEnvironment(cmd) 242 self.checkForOldSlaveAndLogfiles() 243 244 self.startCommand(cmd, warnings)
245 246 247
248 -class TreeSize(ShellCommand):
249 name = "treesize" 250 command = ["du", "-s", "-k", "."] 251 description = "measuring tree size" 252 descriptionDone = "tree size measured" 253 kib = None 254
255 - def __init__(self, *args, **kwargs):
256 ShellCommand.__init__(self, *args, **kwargs)
257
258 - def commandComplete(self, cmd):
259 out = cmd.logs['stdio'].getText() 260 m = re.search(r'^(\d+)', out) 261 if m: 262 self.kib = int(m.group(1)) 263 self.setProperty("tree-size-KiB", self.kib, "treesize")
264
265 - def evaluateCommand(self, cmd):
266 if cmd.rc != 0: 267 return FAILURE 268 if self.kib is None: 269 return WARNINGS # not sure how 'du' could fail, but whatever 270 return SUCCESS
271
272 - def getText(self, cmd, results):
273 if self.kib is not None: 274 return ["treesize", "%d KiB" % self.kib] 275 return ["treesize", "unknown"]
276
277 -class SetProperty(ShellCommand):
278 name = "setproperty" 279
280 - def __init__(self, property=None, extract_fn=None, strip=True, **kwargs):
281 self.property = property 282 self.extract_fn = extract_fn 283 self.strip = strip 284 285 assert (property is not None) ^ (extract_fn is not None), \ 286 "Exactly one of property and extract_fn must be set" 287 288 ShellCommand.__init__(self, **kwargs) 289 290 self.addFactoryArguments(property=self.property) 291 self.addFactoryArguments(extract_fn=self.extract_fn) 292 self.addFactoryArguments(strip=self.strip) 293 294 self.property_changes = {}
295
296 - def commandComplete(self, cmd):
297 if self.property: 298 result = cmd.logs['stdio'].getText() 299 if self.strip: result = result.strip() 300 propname = self.build.getProperties().render(self.property) 301 self.setProperty(propname, result, "SetProperty Step") 302 self.property_changes[propname] = result 303 else: 304 log = cmd.logs['stdio'] 305 new_props = self.extract_fn(cmd.rc, 306 ''.join(log.getChunks([STDOUT], onlyText=True)), 307 ''.join(log.getChunks([STDERR], onlyText=True))) 308 for k,v in new_props.items(): 309 self.setProperty(k, v, "SetProperty Step") 310 self.property_changes = new_props
311
312 - def createSummary(self, log):
313 props_set = [ "%s: %r" % (k,v) for k,v in self.property_changes.items() ] 314 self.addCompleteLog('property changes', "\n".join(props_set))
315
316 - def getText(self, cmd, results):
317 if self.property_changes: 318 return [ "set props:" ] + self.property_changes.keys() 319 else: 320 return [ "no change" ]
321
322 -class Configure(ShellCommand):
323 324 name = "configure" 325 haltOnFailure = 1 326 flunkOnFailure = 1 327 description = ["configuring"] 328 descriptionDone = ["configure"] 329 command = ["./configure"]
330
331 -class StringFileWriter(pb.Referenceable):
332 """ 333 FileWriter class that just puts received data into a buffer. 334 335 Used to upload a file from slave for inline processing rather than 336 writing into a file on master. 337 """
338 - def __init__(self):
339 self.buffer = ""
340
341 - def remote_write(self, data):
342 self.buffer += data
343
344 - def remote_close(self):
345 pass
346
347 -class SilentRemoteCommand(RemoteCommand):
348 """ 349 Remote command subclass used to run an internal file upload command on the 350 slave. We do not need any progress updates from such command, so override 351 remoteUpdate() with an empty method. 352 """
353 - def remoteUpdate(self, update):
354 pass
355
356 -class WarningCountingShellCommand(ShellCommand):
357 warnCount = 0 358 warningPattern = '.*warning[: ].*' 359 # The defaults work for GNU Make. 360 directoryEnterPattern = "make.*: Entering directory [\"`'](.*)['`\"]" 361 directoryLeavePattern = "make.*: Leaving directory" 362 suppressionFile = None 363 364 commentEmptyLineRe = re.compile(r"^\s*(\#.*)?$") 365 suppressionLineRe = re.compile(r"^\s*(.+?)\s*:\s*(.+?)\s*(?:[:]\s*([0-9]+)(?:-([0-9]+))?\s*)?$") 366
367 - def __init__(self, workdir=None, 368 warningPattern=None, warningExtractor=None, 369 directoryEnterPattern=None, directoryLeavePattern=None, 370 suppressionFile=None, **kwargs):
371 self.workdir = workdir 372 # See if we've been given a regular expression to use to match 373 # warnings. If not, use a default that assumes any line with "warning" 374 # present is a warning. This may lead to false positives in some cases. 375 if warningPattern: 376 self.warningPattern = warningPattern 377 if directoryEnterPattern: 378 self.directoryEnterPattern = directoryEnterPattern 379 if directoryLeavePattern: 380 self.directoryLeavePattern = directoryLeavePattern 381 if suppressionFile: 382 self.suppressionFile = suppressionFile 383 if warningExtractor: 384 self.warningExtractor = warningExtractor 385 else: 386 self.warningExtractor = WarningCountingShellCommand.warnExtractWholeLine 387 388 # And upcall to let the base class do its work 389 ShellCommand.__init__(self, workdir=workdir, **kwargs) 390 391 self.addFactoryArguments(warningPattern=warningPattern, 392 directoryEnterPattern=directoryEnterPattern, 393 directoryLeavePattern=directoryLeavePattern, 394 warningExtractor=warningExtractor, 395 suppressionFile=suppressionFile) 396 self.suppressions = [] 397 self.directoryStack = []
398
399 - def setDefaultWorkdir(self, workdir):
400 if self.workdir is None: 401 self.workdir = workdir 402 ShellCommand.setDefaultWorkdir(self, workdir)
403
404 - def addSuppression(self, suppressionList):
405 """ 406 This method can be used to add patters of warnings that should 407 not be counted. 408 409 It takes a single argument, a list of patterns. 410 411 Each pattern is a 4-tuple (FILE-RE, WARN-RE, START, END). 412 413 FILE-RE is a regular expression (string or compiled regexp), or None. 414 If None, the pattern matches all files, else only files matching the 415 regexp. If directoryEnterPattern is specified in the class constructor, 416 matching is against the full path name, eg. src/main.c. 417 418 WARN-RE is similarly a regular expression matched against the 419 text of the warning, or None to match all warnings. 420 421 START and END form an inclusive line number range to match against. If 422 START is None, there is no lower bound, similarly if END is none there 423 is no upper bound.""" 424 425 for fileRe, warnRe, start, end in suppressionList: 426 if fileRe != None and isinstance(fileRe, str): 427 fileRe = re.compile(fileRe) 428 if warnRe != None and isinstance(warnRe, str): 429 warnRe = re.compile(warnRe) 430 self.suppressions.append((fileRe, warnRe, start, end))
431
432 - def warnExtractWholeLine(self, line, match):
433 """ 434 Extract warning text as the whole line. 435 No file names or line numbers.""" 436 return (None, None, line)
437
438 - def warnExtractFromRegexpGroups(self, line, match):
439 """ 440 Extract file name, line number, and warning text as groups (1,2,3) 441 of warningPattern match.""" 442 file = match.group(1) 443 lineNo = match.group(2) 444 if lineNo != None: 445 lineNo = int(lineNo) 446 text = match.group(3) 447 return (file, lineNo, text)
448
449 - def maybeAddWarning(self, warnings, line, match):
450 if self.suppressions: 451 (file, lineNo, text) = self.warningExtractor(self, line, match) 452 453 if file != None and file != "" and self.directoryStack: 454 currentDirectory = self.directoryStack[-1] 455 if currentDirectory != None and currentDirectory != "": 456 file = "%s/%s" % (currentDirectory, file) 457 458 # Skip adding the warning if any suppression matches. 459 for fileRe, warnRe, start, end in self.suppressions: 460 if ( (file == None or fileRe == None or fileRe.search(file)) and 461 (warnRe == None or warnRe.search(text)) and 462 ((start == None and end == None) or 463 (lineNo != None and start <= lineNo and end >= lineNo)) ): 464 return 465 466 warnings.append(line) 467 self.warnCount += 1
468
469 - def start(self):
470 if self.suppressionFile == None: 471 return ShellCommand.start(self) 472 473 version = self.slaveVersion("uploadFile") 474 if not version: 475 m = "Slave is too old, does not know about uploadFile" 476 raise BuildSlaveTooOldError(m) 477 478 self.myFileWriter = StringFileWriter() 479 480 properties = self.build.getProperties() 481 482 args = { 483 'slavesrc': properties.render(self.suppressionFile), 484 'workdir': self.workdir, 485 'writer': self.myFileWriter, 486 'maxsize': None, 487 'blocksize': 32*1024, 488 } 489 cmd = SilentRemoteCommand('uploadFile', args) 490 d = self.runCommand(cmd) 491 d.addCallback(self.uploadDone) 492 d.addErrback(self.failed)
493
494 - def uploadDone(self, dummy):
495 lines = self.myFileWriter.buffer.split("\n") 496 del(self.myFileWriter) 497 498 list = [] 499 for line in lines: 500 if self.commentEmptyLineRe.match(line): 501 continue 502 match = self.suppressionLineRe.match(line) 503 if (match): 504 file, test, start, end = match.groups() 505 if (end != None): 506 end = int(end) 507 if (start != None): 508 start = int(start) 509 if end == None: 510 end = start 511 list.append((file, test, start, end)) 512 513 self.addSuppression(list) 514 return ShellCommand.start(self)
515
516 - def createSummary(self, log):
517 """ 518 Match log lines against warningPattern. 519 520 Warnings are collected into another log for this step, and the 521 build-wide 'warnings-count' is updated.""" 522 523 self.warnCount = 0 524 525 # Now compile a regular expression from whichever warning pattern we're 526 # using 527 if not self.warningPattern: 528 return 529 530 wre = self.warningPattern 531 if isinstance(wre, str): 532 wre = re.compile(wre) 533 534 directoryEnterRe = self.directoryEnterPattern 535 if directoryEnterRe != None and isinstance(directoryEnterRe, str): 536 directoryEnterRe = re.compile(directoryEnterRe) 537 538 directoryLeaveRe = self.directoryLeavePattern 539 if directoryLeaveRe != None and isinstance(directoryLeaveRe, str): 540 directoryLeaveRe = re.compile(directoryLeaveRe) 541 542 # Check if each line in the output from this command matched our 543 # warnings regular expressions. If did, bump the warnings count and 544 # add the line to the collection of lines with warnings 545 warnings = [] 546 # TODO: use log.readlines(), except we need to decide about stdout vs 547 # stderr 548 for line in log.getText().split("\n"): 549 if directoryEnterRe: 550 match = directoryEnterRe.search(line) 551 if match: 552 self.directoryStack.append(match.group(1)) 553 if (directoryLeaveRe and 554 self.directoryStack and 555 directoryLeaveRe.search(line)): 556 self.directoryStack.pop() 557 558 match = wre.match(line) 559 if match: 560 self.maybeAddWarning(warnings, line, match) 561 562 # If there were any warnings, make the log if lines with warnings 563 # available 564 if self.warnCount: 565 self.addCompleteLog("warnings (%d)" % self.warnCount, 566 "\n".join(warnings) + "\n") 567 568 warnings_stat = self.step_status.getStatistic('warnings', 0) 569 self.step_status.setStatistic('warnings', warnings_stat + self.warnCount) 570 571 try: 572 old_count = self.getProperty("warnings-count") 573 except KeyError: 574 old_count = 0 575 self.setProperty("warnings-count", old_count + self.warnCount, "WarningCountingShellCommand")
576 577
578 - def evaluateCommand(self, cmd):
579 if cmd.rc != 0: 580 return FAILURE 581 if self.warnCount: 582 return WARNINGS 583 return SUCCESS
584 585
586 -class Compile(WarningCountingShellCommand):
587 588 name = "compile" 589 haltOnFailure = 1 590 flunkOnFailure = 1 591 description = ["compiling"] 592 descriptionDone = ["compile"] 593 command = ["make", "all"] 594 595 OFFprogressMetrics = ('output',) 596 # things to track: number of files compiled, number of directories 597 # traversed (assuming 'make' is being used) 598
599 - def createSummary(self, log):
600 # TODO: grep for the characteristic GCC error lines and 601 # assemble them into a pair of buffers 602 WarningCountingShellCommand.createSummary(self, log)
603
604 -class Test(WarningCountingShellCommand):
605 606 name = "test" 607 warnOnFailure = 1 608 description = ["testing"] 609 descriptionDone = ["test"] 610 command = ["make", "test"] 611
612 - def setTestResults(self, total=0, failed=0, passed=0, warnings=0):
613 """ 614 Called by subclasses to set the relevant statistics; this actually 615 adds to any statistics already present 616 """ 617 total += self.step_status.getStatistic('tests-total', 0) 618 self.step_status.setStatistic('tests-total', total) 619 failed += self.step_status.getStatistic('tests-failed', 0) 620 self.step_status.setStatistic('tests-failed', failed) 621 warnings += self.step_status.getStatistic('tests-warnings', 0) 622 self.step_status.setStatistic('tests-warnings', warnings) 623 passed += self.step_status.getStatistic('tests-passed', 0) 624 self.step_status.setStatistic('tests-passed', passed)
625
626 - def describe(self, done=False):
627 description = WarningCountingShellCommand.describe(self, done) 628 if done: 629 if self.step_status.hasStatistic('tests-total'): 630 total = self.step_status.getStatistic("tests-total", 0) 631 failed = self.step_status.getStatistic("tests-failed", 0) 632 passed = self.step_status.getStatistic("tests-passed", 0) 633 warnings = self.step_status.getStatistic("tests-warnings", 0) 634 if not total: 635 total = failed + passed + warnings 636 637 if total: 638 description.append('%d tests' % total) 639 if passed: 640 description.append('%d passed' % passed) 641 if warnings: 642 description.append('%d warnings' % warnings) 643 if failed: 644 description.append('%d failed' % failed) 645 return description
646
647 -class PerlModuleTest(Test):
648 command=["prove", "--lib", "lib", "-r", "t"] 649 total = 0 650
651 - def evaluateCommand(self, cmd):
652 # Get stdio, stripping pesky newlines etc. 653 lines = map( 654 lambda line : line.replace('\r\n','').replace('\r','').replace('\n',''), 655 self.getLog('stdio').readlines() 656 ) 657 658 total = 0 659 passed = 0 660 failed = 0 661 rc = SUCCESS 662 if cmd.rc > 0: 663 rc = FAILURE 664 665 # New version of Test::Harness? 666 if "Test Summary Report" in lines: 667 test_summary_report_index = lines.index("Test Summary Report") 668 del lines[0:test_summary_report_index + 2] 669 670 re_test_result = re.compile("^Result: (PASS|FAIL)$|Tests: \d+ Failed: (\d+)\)|Files=\d+, Tests=(\d+)") 671 672 mos = map(lambda line: re_test_result.search(line), lines) 673 test_result_lines = [mo.groups() for mo in mos if mo] 674 675 for line in test_result_lines: 676 if line[0] == 'FAIL': 677 rc = FAILURE 678 679 if line[1]: 680 failed += int(line[1]) 681 if line[2]: 682 total = int(line[2]) 683 684 else: # Nope, it's the old version 685 re_test_result = re.compile("^(All tests successful)|(\d+)/(\d+) subtests failed|Files=\d+, Tests=(\d+),") 686 687 mos = map(lambda line: re_test_result.search(line), lines) 688 test_result_lines = [mo.groups() for mo in mos if mo] 689 690 if test_result_lines: 691 test_result_line = test_result_lines[0] 692 693 success = test_result_line[0] 694 695 if success: 696 failed = 0 697 698 test_totals_line = test_result_lines[1] 699 total_str = test_totals_line[3] 700 else: 701 failed_str = test_result_line[1] 702 failed = int(failed_str) 703 704 total_str = test_result_line[2] 705 706 rc = FAILURE 707 708 total = int(total_str) 709 710 warnings = 0 711 if self.warningPattern: 712 wre = self.warningPattern 713 if isinstance(wre, str): 714 wre = re.compile(wre) 715 716 warnings = len([l for l in lines if wre.search(l)]) 717 718 # Because there are two paths that are used to determine 719 # the success/fail result, I have to modify it here if 720 # there were warnings. 721 if rc == SUCCESS and warnings: 722 rc = WARNINGS 723 724 if total: 725 passed = total - failed 726 727 self.setTestResults(total=total, failed=failed, passed=passed, 728 warnings=warnings) 729 730 return rc
731