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