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 |