1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import re
18 from twisted.python import log, failure
19 from twisted.spread import pb
20 from buildbot.process import buildstep
21 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE
22 from buildbot.status.logfile import STDOUT, STDERR
23 from buildbot import config
24
25
26
27 from buildbot.process.properties import WithProperties
28 _hush_pyflakes = [WithProperties]
29 del _hush_pyflakes
30
32 """I run a single shell command on the buildslave. I return FAILURE if
33 the exit code of that command is non-zero, SUCCESS otherwise. To change
34 this behavior, override my .evaluateCommand method, or customize
35 decodeRC argument
36
37 By default, a failure of this step will mark the whole build as FAILURE.
38 To override this, give me an argument of flunkOnFailure=False .
39
40 I create a single Log named 'log' which contains the output of the
41 command. To create additional summary Logs, override my .createSummary
42 method.
43
44 The shell command I run (a list of argv strings) can be provided in
45 several ways:
46 - a class-level .command attribute
47 - a command= parameter to my constructor (overrides .command)
48 - set explicitly with my .setCommand() method (overrides both)
49
50 @ivar command: a list of renderable objects (typically strings or
51 WithProperties instances). This will be used by start()
52 to create a RemoteShellCommand instance.
53
54 @ivar logfiles: a dict mapping log NAMEs to workdir-relative FILENAMEs
55 of their corresponding logfiles. The contents of the file
56 named FILENAME will be put into a LogFile named NAME, ina
57 something approximating real-time. (note that logfiles=
58 is actually handled by our parent class LoggingBuildStep)
59
60 @ivar lazylogfiles: Defaults to False. If True, logfiles will be tracked
61 `lazily', meaning they will only be added when and if
62 they are written to. Empty or nonexistent logfiles
63 will be omitted. (Also handled by class
64 LoggingBuildStep.)
65 """
66
67 name = "shell"
68 renderables = buildstep.LoggingBuildStep.renderables + [
69 'slaveEnvironment', 'remote_kwargs', 'command',
70 'description', 'descriptionDone', 'descriptionSuffix']
71
72 description = None
73 descriptionDone = None
74 descriptionSuffix = None
75
76 command = None
77
78
79
80
81
82
83 flunkOnFailure = True
84
85 - def __init__(self, workdir=None,
86 description=None, descriptionDone=None, descriptionSuffix=None,
87 command=None,
88 usePTY="slave-config",
89 **kwargs):
124
129
132
134 rkw = self.remote_kwargs
135 rkw['workdir'] = rkw['workdir'] or workdir
136
138 """
139 Get the current notion of the workdir. Note that this may change
140 between instantiation of the step and C{start}, as it is based on the
141 build's default workdir, and may even be C{None} before that point.
142 """
143 return self.remote_kwargs['workdir']
144
147
149 for x in commands:
150 if isinstance(x, (list, tuple)):
151 if x != []:
152 self._flattenList(mainlist, x)
153 else:
154 mainlist.append(x)
155
162
164 """Return a list of short strings to describe this step, for the
165 status display. This uses the first few words of the shell command.
166 You can replace this by setting .description in your subclass, or by
167 overriding this method to describe the step better.
168
169 @type done: boolean
170 @param done: whether the command is complete or not, to improve the
171 way the command is described. C{done=False} is used
172 while the command is still running, so a single
173 imperfect-tense verb is appropriate ('compiling',
174 'testing', ...) C{done=True} is used when the command
175 has finished, and the default getText() method adds some
176 text, so a simple noun is appropriate ('compile',
177 'tests' ...)
178 """
179
180 try:
181 if done and self.descriptionDone is not None:
182 return self.descriptionDone
183 if self.description is not None:
184 return self.description
185
186
187
188 if not self.command:
189 return ["???"]
190
191 words = self.command
192 if isinstance(words, (str, unicode)):
193 words = words.split()
194
195 try:
196 len(words)
197 except AttributeError:
198
199 return ["???"]
200
201
202 tmp = []
203 self._flattenList(tmp, words)
204 words = tmp
205
206
207
208 words = [ w for w in words if isinstance(w, (str, unicode)) ]
209
210 if len(words) < 1:
211 return ["???"]
212 if len(words) == 1:
213 return ["'%s'" % words[0]]
214 if len(words) == 2:
215 return ["'%s" % words[0], "%s'" % words[1]]
216 return ["'%s" % words[0], "%s" % words[1], "...'"]
217
218 except:
219 log.err(failure.Failure(), "Error describing step")
220 return ["???"]
221
223
224
225
226
227
228
229 slaveEnv = self.slaveEnvironment
230 if slaveEnv:
231 if cmd.args['env'] is None:
232 cmd.args['env'] = {}
233 fullSlaveEnv = slaveEnv.copy()
234 fullSlaveEnv.update(cmd.args['env'])
235 cmd.args['env'] = fullSlaveEnv
236
237
238
262
276
277
278
280 name = "treesize"
281 command = ["du", "-s", "-k", "."]
282 description = "measuring tree size"
283 descriptionDone = "tree size measured"
284 kib = None
285
292
299
300 - def getText(self, cmd, results):
301 if self.kib is not None:
302 return ["treesize", "%d KiB" % self.kib]
303 return ["treesize", "unknown"]
304
305
307 name = "setproperty"
308 renderables = [ 'property' ]
309
310 - def __init__(self, property=None, extract_fn=None, strip=True, **kwargs):
311 self.property = property
312 self.extract_fn = extract_fn
313 self.strip = strip
314
315 if not ((property is not None) ^ (extract_fn is not None)):
316 config.error(
317 "Exactly one of property and extract_fn must be set")
318
319 ShellCommand.__init__(self, **kwargs)
320
321 self.property_changes = {}
322
324 if self.property:
325 if cmd.didFail():
326 return
327 result = cmd.logs['stdio'].getText()
328 if self.strip: result = result.strip()
329 propname = self.property
330 self.setProperty(propname, result, "SetProperty Step")
331 self.property_changes[propname] = result
332 else:
333 log = cmd.logs['stdio']
334 new_props = self.extract_fn(cmd.rc,
335 ''.join(log.getChunks([STDOUT], onlyText=True)),
336 ''.join(log.getChunks([STDERR], onlyText=True)))
337 for k,v in new_props.items():
338 self.setProperty(k, v, "SetProperty Step")
339 self.property_changes = new_props
340
342 if self.property_changes:
343 props_set = [ "%s: %r" % (k,v)
344 for k,v in self.property_changes.items() ]
345 self.addCompleteLog('property changes', "\n".join(props_set))
346
347 - def getText(self, cmd, results):
348 if len(self.property_changes) > 1:
349 return [ "%d properties set" % len(self.property_changes) ]
350 elif len(self.property_changes) == 1:
351 return [ "property '%s' set" % self.property_changes.keys()[0] ]
352 else:
353
354 return ShellCommand.getText(self, cmd, results)
355
364
366 """
367 FileWriter class that just puts received data into a buffer.
368
369 Used to upload a file from slave for inline processing rather than
370 writing into a file on master.
371 """
374
377
380
382 renderables = [ 'suppressionFile' ]
383
384 warnCount = 0
385 warningPattern = '.*warning[: ].*'
386
387 directoryEnterPattern = (u"make.*: Entering directory "
388 u"[\u2019\"`'](.*)[\u2019'`\"]")
389 directoryLeavePattern = "make.*: Leaving directory"
390 suppressionFile = None
391
392 commentEmptyLineRe = re.compile(r"^\s*(\#.*)?$")
393 suppressionLineRe = re.compile(r"^\s*(.+?)\s*:\s*(.+?)\s*(?:[:]\s*([0-9]+)(?:-([0-9]+))?\s*)?$")
394
395 - def __init__(self,
396 warningPattern=None, warningExtractor=None, maxWarnCount=None,
397 directoryEnterPattern=None, directoryLeavePattern=None,
398 suppressionFile=None, **kwargs):
421
423 """
424 This method can be used to add patters of warnings that should
425 not be counted.
426
427 It takes a single argument, a list of patterns.
428
429 Each pattern is a 4-tuple (FILE-RE, WARN-RE, START, END).
430
431 FILE-RE is a regular expression (string or compiled regexp), or None.
432 If None, the pattern matches all files, else only files matching the
433 regexp. If directoryEnterPattern is specified in the class constructor,
434 matching is against the full path name, eg. src/main.c.
435
436 WARN-RE is similarly a regular expression matched against the
437 text of the warning, or None to match all warnings.
438
439 START and END form an inclusive line number range to match against. If
440 START is None, there is no lower bound, similarly if END is none there
441 is no upper bound."""
442
443 for fileRe, warnRe, start, end in suppressionList:
444 if fileRe != None and isinstance(fileRe, basestring):
445 fileRe = re.compile(fileRe)
446 if warnRe != None and isinstance(warnRe, basestring):
447 warnRe = re.compile(warnRe)
448 self.suppressions.append((fileRe, warnRe, start, end))
449
451 """
452 Extract warning text as the whole line.
453 No file names or line numbers."""
454 return (None, None, line)
455
457 """
458 Extract file name, line number, and warning text as groups (1,2,3)
459 of warningPattern match."""
460 file = match.group(1)
461 lineNo = match.group(2)
462 if lineNo != None:
463 lineNo = int(lineNo)
464 text = match.group(3)
465 return (file, lineNo, text)
466
468 if self.suppressions:
469 (file, lineNo, text) = self.warningExtractor(self, line, match)
470 lineNo = lineNo and int(lineNo)
471
472 if file != None and file != "" and self.directoryStack:
473 currentDirectory = '/'.join(self.directoryStack)
474 if currentDirectory != None and currentDirectory != "":
475 file = "%s/%s" % (currentDirectory, file)
476
477
478 for fileRe, warnRe, start, end in self.suppressions:
479 if not (file == None or fileRe == None or fileRe.match(file)):
480 continue
481 if not (warnRe == None or warnRe.search(text)):
482 continue
483 if not ((start == None and end == None) or
484 (lineNo != None and start <= lineNo and end >= lineNo)):
485 continue
486 return
487
488 warnings.append(line)
489 self.warnCount += 1
490
508
530
532 """
533 Match log lines against warningPattern.
534
535 Warnings are collected into another log for this step, and the
536 build-wide 'warnings-count' is updated."""
537
538 self.warnCount = 0
539
540
541
542 wre = self.warningPattern
543 if isinstance(wre, str):
544 wre = re.compile(wre)
545
546 directoryEnterRe = self.directoryEnterPattern
547 if (directoryEnterRe != None
548 and isinstance(directoryEnterRe, basestring)):
549 directoryEnterRe = re.compile(directoryEnterRe)
550
551 directoryLeaveRe = self.directoryLeavePattern
552 if (directoryLeaveRe != None
553 and isinstance(directoryLeaveRe, basestring)):
554 directoryLeaveRe = re.compile(directoryLeaveRe)
555
556
557
558
559 warnings = []
560
561
562 for line in log.getText().split("\n"):
563 if directoryEnterRe:
564 match = directoryEnterRe.search(line)
565 if match:
566 self.directoryStack.append(match.group(1))
567 continue
568 if (directoryLeaveRe and
569 self.directoryStack and
570 directoryLeaveRe.search(line)):
571 self.directoryStack.pop()
572 continue
573
574 match = wre.match(line)
575 if match:
576 self.maybeAddWarning(warnings, line, match)
577
578
579
580 if self.warnCount:
581 self.addCompleteLog("warnings (%d)" % self.warnCount,
582 "\n".join(warnings) + "\n")
583
584 warnings_stat = self.step_status.getStatistic('warnings', 0)
585 self.step_status.setStatistic('warnings', warnings_stat + self.warnCount)
586
587 old_count = self.getProperty("warnings-count", 0)
588 self.setProperty("warnings-count", old_count + self.warnCount, "WarningCountingShellCommand")
589
590
598
599
600 -class Compile(WarningCountingShellCommand):
608
609 -class Test(WarningCountingShellCommand):
652
654 command=["prove", "--lib", "lib", "-r", "t"]
655 total = 0
656
658
659 lines = map(
660 lambda line : line.replace('\r\n','').replace('\r','').replace('\n',''),
661 self.getLog('stdio').readlines()
662 )
663
664 total = 0
665 passed = 0
666 failed = 0
667 rc = SUCCESS
668 if cmd.didFail():
669 rc = FAILURE
670
671
672 if "Test Summary Report" in lines:
673 test_summary_report_index = lines.index("Test Summary Report")
674 del lines[0:test_summary_report_index + 2]
675
676 re_test_result = re.compile("^Result: (PASS|FAIL)$|Tests: \d+ Failed: (\d+)\)|Files=\d+, Tests=(\d+)")
677
678 mos = map(lambda line: re_test_result.search(line), lines)
679 test_result_lines = [mo.groups() for mo in mos if mo]
680
681 for line in test_result_lines:
682 if line[0] == 'FAIL':
683 rc = FAILURE
684
685 if line[1]:
686 failed += int(line[1])
687 if line[2]:
688 total = int(line[2])
689
690 else:
691 re_test_result = re.compile("^(All tests successful)|(\d+)/(\d+) subtests failed|Files=\d+, Tests=(\d+),")
692
693 mos = map(lambda line: re_test_result.search(line), lines)
694 test_result_lines = [mo.groups() for mo in mos if mo]
695
696 if test_result_lines:
697 test_result_line = test_result_lines[0]
698
699 success = test_result_line[0]
700
701 if success:
702 failed = 0
703
704 test_totals_line = test_result_lines[1]
705 total_str = test_totals_line[3]
706 else:
707 failed_str = test_result_line[1]
708 failed = int(failed_str)
709
710 total_str = test_result_line[2]
711
712 rc = FAILURE
713
714 total = int(total_str)
715
716 warnings = 0
717 if self.warningPattern:
718 wre = self.warningPattern
719 if isinstance(wre, str):
720 wre = re.compile(wre)
721
722 warnings = len([l for l in lines if wre.search(l)])
723
724
725
726
727 if rc == SUCCESS and warnings:
728 rc = WARNINGS
729
730 if total:
731 passed = total - failed
732
733 self.setTestResults(total=total, failed=failed, passed=passed,
734 warnings=warnings)
735
736 return rc
737