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