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 if done and self.descriptionDone is not None: 134 return list(self.descriptionDone) 135 if self.description is not None: 136 return list(self.description) 137 138 properties = self.build.getProperties() 139 words = self.command 140 if isinstance(words, (str, unicode)): 141 words = words.split() 142 # render() each word to handle WithProperties objects 143 words = properties.render(words) 144 if len(words) < 1: 145 return ["???"] 146 if len(words) == 1: 147 return ["'%s'" % words[0]] 148 if len(words) == 2: 149 return ["'%s" % words[0], "%s'" % words[1]] 150 return ["'%s" % words[0], "%s" % words[1], "...'"] 151 except: 152 log.msg("Error describing step") 153 log.err() 154 return ["???"]
155
156 - def setupEnvironment(self, cmd):
157 # merge in anything from Build.slaveEnvironment 158 # This can be set from a Builder-level environment, or from earlier 159 # BuildSteps. The latter method is deprecated and superceded by 160 # BuildProperties. 161 # Environment variables passed in by a BuildStep override 162 # those passed in at the Builder level. 163 properties = self.build.getProperties() 164 slaveEnv = self.build.slaveEnvironment 165 if slaveEnv: 166 if cmd.args['env'] is None: 167 cmd.args['env'] = {} 168 fullSlaveEnv = slaveEnv.copy() 169 fullSlaveEnv.update(cmd.args['env']) 170 cmd.args['env'] = properties.render(fullSlaveEnv)
171 # note that each RemoteShellCommand gets its own copy of the 172 # dictionary, so we shouldn't be affecting anyone but ourselves. 173
175 if not self.logfiles: 176 return # doesn't matter 177 if not self.slaveVersionIsOlderThan("shell", "2.1"): 178 return # slave is new enough 179 # this buildslave is too old and will ignore the 'logfiles' 180 # argument. You'll either have to pull the logfiles manually 181 # (say, by using 'cat' in a separate RemoteShellCommand) or 182 # upgrade the buildslave. 183 msg1 = ("Warning: buildslave %s is too old " 184 "to understand logfiles=, ignoring it." 185 % self.getSlaveName()) 186 msg2 = "You will have to pull this logfile (%s) manually." 187 log.msg(msg1) 188 for logname,remotefilevalue in self.logfiles.items(): 189 remotefilename = remotefilevalue 190 # check for a dictionary of options 191 if type(remotefilevalue) == dict: 192 remotefilename = remotefilevalue['filename'] 193 194 newlog = self.addLog(logname) 195 newlog.addHeader(msg1 + "\n") 196 newlog.addHeader(msg2 % remotefilename + "\n") 197 newlog.finish() 198 # now prevent setupLogfiles() from adding them 199 self.logfiles = {}
200
201 - def start(self):
202 # this block is specific to ShellCommands. subclasses that don't need 203 # to set up an argv array, an environment, or extra logfiles= (like 204 # the Source subclasses) can just skip straight to startCommand() 205 properties = self.build.getProperties() 206 207 warnings = [] 208 209 # create the actual RemoteShellCommand instance now 210 kwargs = properties.render(self.remote_kwargs) 211 command = properties.render(self.command) 212 kwargs['command'] = command 213 kwargs['logfiles'] = self.logfiles 214 215 # check for the usePTY flag 216 if kwargs.has_key('usePTY') and kwargs['usePTY'] != 'slave-config': 217 if self.slaveVersionIsOlderThan("svn", "2.7"): 218 warnings.append("NOTE: slave does not allow master to override usePTY\n") 219 220 cmd = RemoteShellCommand(**kwargs) 221 self.setupEnvironment(cmd) 222 self.checkForOldSlaveAndLogfiles() 223 224 self.startCommand(cmd, warnings)
225 226 227
228 -class TreeSize(ShellCommand):
229 name = "treesize" 230 command = ["du", "-s", "-k", "."] 231 description = "measuring tree size" 232 descriptionDone = "tree size measured" 233 kib = None 234
235 - def __init__(self, *args, **kwargs):
236 ShellCommand.__init__(self, *args, **kwargs)
237
238 - def commandComplete(self, cmd):
239 out = cmd.logs['stdio'].getText() 240 m = re.search(r'^(\d+)', out) 241 if m: 242 self.kib = int(m.group(1)) 243 self.setProperty("tree-size-KiB", self.kib, "treesize")
244
245 - def evaluateCommand(self, cmd):
246 if cmd.rc != 0: 247 return FAILURE 248 if self.kib is None: 249 return WARNINGS # not sure how 'du' could fail, but whatever 250 return SUCCESS
251
252 - def getText(self, cmd, results):
253 if self.kib is not None: 254 return ["treesize", "%d KiB" % self.kib] 255 return ["treesize", "unknown"]
256
257 -class SetProperty(ShellCommand):
258 name = "setproperty" 259
260 - def __init__(self, property=None, extract_fn=None, strip=True, **kwargs):
261 self.property = property 262 self.extract_fn = extract_fn 263 self.strip = strip 264 265 assert (property is not None) ^ (extract_fn is not None), \ 266 "Exactly one of property and extract_fn must be set" 267 268 ShellCommand.__init__(self, **kwargs) 269 270 self.addFactoryArguments(property=self.property) 271 self.addFactoryArguments(extract_fn=self.extract_fn) 272 self.addFactoryArguments(strip=self.strip) 273 274 self.property_changes = {}
275
276 - def commandComplete(self, cmd):
277 if self.property: 278 result = cmd.logs['stdio'].getText() 279 if self.strip: result = result.strip() 280 propname = self.build.getProperties().render(self.property) 281 self.setProperty(propname, result, "SetProperty Step") 282 self.property_changes[propname] = result 283 else: 284 log = cmd.logs['stdio'] 285 new_props = self.extract_fn(cmd.rc, 286 ''.join(log.getChunks([STDOUT], onlyText=True)), 287 ''.join(log.getChunks([STDERR], onlyText=True))) 288 for k,v in new_props.items(): 289 self.setProperty(k, v, "SetProperty Step") 290 self.property_changes = new_props
291
292 - def createSummary(self, log):
293 props_set = [ "%s: %r" % (k,v) for k,v in self.property_changes.items() ] 294 self.addCompleteLog('property changes', "\n".join(props_set))
295
296 - def getText(self, cmd, results):
297 if self.property_changes: 298 return [ "set props:" ] + self.property_changes.keys() 299 else: 300 return [ "no change" ]
301
302 -class Configure(ShellCommand):
303 304 name = "configure" 305 haltOnFailure = 1 306 flunkOnFailure = 1 307 description = ["configuring"] 308 descriptionDone = ["configure"] 309 command = ["./configure"]
310
311 -class StringFileWriter(pb.Referenceable):
312 """ 313 FileWriter class that just puts received data into a buffer. 314 315 Used to upload a file from slave for inline processing rather than 316 writing into a file on master. 317 """
318 - def __init__(self):
319 self.buffer = ""
320
321 - def remote_write(self, data):
322 self.buffer += data
323
324 - def remote_close(self):
325 pass
326
327 -class SilentRemoteCommand(RemoteCommand):
328 """ 329 Remote command subclass used to run an internal file upload command on the 330 slave. We do not need any progress updates from such command, so override 331 remoteUpdate() with an empty method. 332 """
333 - def remoteUpdate(self, update):
334 pass
335
336 -class WarningCountingShellCommand(ShellCommand):
337 warnCount = 0 338 warningPattern = '.*warning[: ].*' 339 # The defaults work for GNU Make. 340 directoryEnterPattern = "make.*: Entering directory [\"`'](.*)['`\"]" 341 directoryLeavePattern = "make.*: Leaving directory" 342 suppressionFile = None 343 344 commentEmptyLineRe = re.compile(r"^\s*(\#.*)?$") 345 suppressionLineRe = re.compile(r"^\s*(.+?)\s*:\s*(.+?)\s*(?:[:]\s*([0-9]+)(?:-([0-9]+))?\s*)?$") 346
347 - def __init__(self, workdir=None, 348 warningPattern=None, warningExtractor=None, 349 directoryEnterPattern=None, directoryLeavePattern=None, 350 suppressionFile=None, **kwargs):
351 self.workdir = workdir 352 # See if we've been given a regular expression to use to match 353 # warnings. If not, use a default that assumes any line with "warning" 354 # present is a warning. This may lead to false positives in some cases. 355 if warningPattern: 356 self.warningPattern = warningPattern 357 if directoryEnterPattern: 358 self.directoryEnterPattern = directoryEnterPattern 359 if directoryLeavePattern: 360 self.directoryLeavePattern = directoryLeavePattern 361 if suppressionFile: 362 self.suppressionFile = suppressionFile 363 if warningExtractor: 364 self.warningExtractor = warningExtractor 365 else: 366 self.warningExtractor = WarningCountingShellCommand.warnExtractWholeLine 367 368 # And upcall to let the base class do its work 369 ShellCommand.__init__(self, workdir=workdir, **kwargs) 370 371 self.addFactoryArguments(warningPattern=warningPattern, 372 directoryEnterPattern=directoryEnterPattern, 373 directoryLeavePattern=directoryLeavePattern, 374 warningExtractor=warningExtractor, 375 suppressionFile=suppressionFile) 376 self.suppressions = [] 377 self.directoryStack = []
378
379 - def setDefaultWorkdir(self, workdir):
380 if self.workdir is None: 381 self.workdir = workdir 382 ShellCommand.setDefaultWorkdir(self, workdir)
383
384 - def addSuppression(self, suppressionList):
385 """ 386 This method can be used to add patters of warnings that should 387 not be counted. 388 389 It takes a single argument, a list of patterns. 390 391 Each pattern is a 4-tuple (FILE-RE, WARN-RE, START, END). 392 393 FILE-RE is a regular expression (string or compiled regexp), or None. 394 If None, the pattern matches all files, else only files matching the 395 regexp. If directoryEnterPattern is specified in the class constructor, 396 matching is against the full path name, eg. src/main.c. 397 398 WARN-RE is similarly a regular expression matched against the 399 text of the warning, or None to match all warnings. 400 401 START and END form an inclusive line number range to match against. If 402 START is None, there is no lower bound, similarly if END is none there 403 is no upper bound.""" 404 405 for fileRe, warnRe, start, end in suppressionList: 406 if fileRe != None and isinstance(fileRe, str): 407 fileRe = re.compile(fileRe) 408 if warnRe != None and isinstance(warnRe, str): 409 warnRe = re.compile(warnRe) 410 self.suppressions.append((fileRe, warnRe, start, end))
411
412 - def warnExtractWholeLine(self, line, match):
413 """ 414 Extract warning text as the whole line. 415 No file names or line numbers.""" 416 return (None, None, line)
417
418 - def warnExtractFromRegexpGroups(self, line, match):
419 """ 420 Extract file name, line number, and warning text as groups (1,2,3) 421 of warningPattern match.""" 422 file = match.group(1) 423 lineNo = match.group(2) 424 if lineNo != None: 425 lineNo = int(lineNo) 426 text = match.group(3) 427 return (file, lineNo, text)
428
429 - def maybeAddWarning(self, warnings, line, match):
430 if self.suppressions: 431 (file, lineNo, text) = self.warningExtractor(self, line, match) 432 433 if file != None and file != "" and self.directoryStack: 434 currentDirectory = self.directoryStack[-1] 435 if currentDirectory != None and currentDirectory != "": 436 file = "%s/%s" % (currentDirectory, file) 437 438 # Skip adding the warning if any suppression matches. 439 for fileRe, warnRe, start, end in self.suppressions: 440 if ( (file == None or fileRe == None or fileRe.search(file)) and 441 (warnRe == None or warnRe.search(text)) and 442 lineNo != None and 443 (start == None or start <= lineNo) and 444 (end == None or 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", "\n".join(warnings) + "\n") 547 548 warnings_stat = self.step_status.getStatistic('warnings', 0) 549 self.step_status.setStatistic('warnings', warnings_stat + self.warnCount) 550 551 try: 552 old_count = self.getProperty("warnings-count") 553 except KeyError: 554 old_count = 0 555 self.setProperty("warnings-count", old_count + self.warnCount, "WarningCountingShellCommand")
556 557
558 - def evaluateCommand(self, cmd):
559 if cmd.rc != 0: 560 return FAILURE 561 if self.warnCount: 562 return WARNINGS 563 return SUCCESS
564 565
566 -class Compile(WarningCountingShellCommand):
567 568 name = "compile" 569 haltOnFailure = 1 570 flunkOnFailure = 1 571 description = ["compiling"] 572 descriptionDone = ["compile"] 573 command = ["make", "all"] 574 575 OFFprogressMetrics = ('output',) 576 # things to track: number of files compiled, number of directories 577 # traversed (assuming 'make' is being used) 578
579 - def createSummary(self, log):
580 # TODO: grep for the characteristic GCC error lines and 581 # assemble them into a pair of buffers 582 WarningCountingShellCommand.createSummary(self, log)
583
584 -class Test(WarningCountingShellCommand):
585 586 name = "test" 587 warnOnFailure = 1 588 description = ["testing"] 589 descriptionDone = ["test"] 590 command = ["make", "test"] 591
592 - def setTestResults(self, total=0, failed=0, passed=0, warnings=0):
593 """ 594 Called by subclasses to set the relevant statistics; this actually 595 adds to any statistics already present 596 """ 597 total += self.step_status.getStatistic('tests-total', 0) 598 self.step_status.setStatistic('tests-total', total) 599 failed += self.step_status.getStatistic('tests-failed', 0) 600 self.step_status.setStatistic('tests-failed', failed) 601 warnings += self.step_status.getStatistic('tests-warnings', 0) 602 self.step_status.setStatistic('tests-warnings', warnings) 603 passed += self.step_status.getStatistic('tests-passed', 0) 604 self.step_status.setStatistic('tests-passed', passed)
605
606 - def describe(self, done=False):
607 description = WarningCountingShellCommand.describe(self, done) 608 if done: 609 if self.step_status.hasStatistic('tests-total'): 610 total = self.step_status.getStatistic("tests-total", 0) 611 failed = self.step_status.getStatistic("tests-failed", 0) 612 passed = self.step_status.getStatistic("tests-passed", 0) 613 warnings = self.step_status.getStatistic("tests-warnings", 0) 614 if not total: 615 total = failed + passed + warnings 616 617 if total: 618 description.append('%d tests' % total) 619 if passed: 620 description.append('%d passed' % passed) 621 if warnings: 622 description.append('%d warnings' % warnings) 623 if failed: 624 description.append('%d failed' % failed) 625 return description
626
627 -class PerlModuleTest(Test):
628 command=["prove", "--lib", "lib", "-r", "t"] 629 total = 0 630
631 - def evaluateCommand(self, cmd):
632 # Get stdio, stripping pesky newlines etc. 633 lines = map( 634 lambda line : line.replace('\r\n','').replace('\r','').replace('\n',''), 635 self.getLog('stdio').readlines() 636 ) 637 638 total = 0 639 passed = 0 640 failed = 0 641 rc = cmd.rc 642 643 # New version of Test::Harness? 644 try: 645 test_summary_report_index = lines.index("Test Summary Report") 646 647 del lines[0:test_summary_report_index + 2] 648 649 re_test_result = re.compile("^Result: (PASS|FAIL)$|Tests: \d+ Failed: (\d+)\)|Files=\d+, Tests=(\d+)") 650 651 mos = map(lambda line: re_test_result.search(line), lines) 652 test_result_lines = [mo.groups() for mo in mos if mo] 653 654 for line in test_result_lines: 655 if line[0] == 'PASS': 656 rc = SUCCESS 657 elif line[0] == 'FAIL': 658 rc = FAILURE 659 elif line[1]: 660 failed += int(line[1]) 661 elif line[2]: 662 total = int(line[2]) 663 664 except ValueError: # Nope, it's the old version 665 re_test_result = re.compile("^(All tests successful)|(\d+)/(\d+) subtests failed|Files=\d+, Tests=(\d+),") 666 667 mos = map(lambda line: re_test_result.search(line), lines) 668 test_result_lines = [mo.groups() for mo in mos if mo] 669 670 if test_result_lines: 671 test_result_line = test_result_lines[0] 672 673 success = test_result_line[0] 674 675 if success: 676 failed = 0 677 678 test_totals_line = test_result_lines[1] 679 total_str = test_totals_line[3] 680 rc = SUCCESS 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