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