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

Source Code for Module buildbot.steps.shell

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