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
19 from twisted.spread import pb
20 from buildbot.process.buildstep import LoggingBuildStep, RemoteShellCommand
21 from buildbot.process.buildstep import RemoteCommand
22 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, STDOUT, STDERR
23 from buildbot.interfaces import BuildSlaveTooOldError
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.
35
36 By default, a failure of this step will mark the whole build as FAILURE.
37 To override this, give me an argument of flunkOnFailure=False .
38
39 I create a single Log named 'log' which contains the output of the
40 command. To create additional summary Logs, override my .createSummary
41 method.
42
43 The shell command I run (a list of argv strings) can be provided in
44 several ways:
45 - a class-level .command attribute
46 - a command= parameter to my constructor (overrides .command)
47 - set explicitly with my .setCommand() method (overrides both)
48
49 @ivar command: a list of renderable objects (typically strings or
50 WithProperties instances). This will be used by start()
51 to create a RemoteShellCommand instance.
52
53 @ivar logfiles: a dict mapping log NAMEs to workdir-relative FILENAMEs
54 of their corresponding logfiles. The contents of the file
55 named FILENAME will be put into a LogFile named NAME, ina
56 something approximating real-time. (note that logfiles=
57 is actually handled by our parent class LoggingBuildStep)
58
59 @ivar lazylogfiles: Defaults to False. If True, logfiles will be tracked
60 `lazily', meaning they will only be added when and if
61 they are written to. Empty or nonexistent logfiles
62 will be omitted. (Also handled by class
63 LoggingBuildStep.)
64
65 """
66
67 name = "shell"
68 description = None
69 descriptionDone = None
70 command = None
71
72
73
74
75
76
77 flunkOnFailure = True
78
79 - def __init__(self, workdir=None,
80 description=None, descriptionDone=None,
81 command=None,
82 usePTY="slave-config",
83 **kwargs):
118
121
123 rkw = self.remote_kwargs
124 rkw['workdir'] = rkw['workdir'] or workdir
125
128
130 """Return a list of short strings to describe this step, for the
131 status display. This uses the first few words of the shell command.
132 You can replace this by setting .description in your subclass, or by
133 overriding this method to describe the step better.
134
135 @type done: boolean
136 @param done: whether the command is complete or not, to improve the
137 way the command is described. C{done=False} is used
138 while the command is still running, so a single
139 imperfect-tense verb is appropriate ('compiling',
140 'testing', ...) C{done=True} is used when the command
141 has finished, and the default getText() method adds some
142 text, so a simple noun is appropriate ('compile',
143 'tests' ...)
144 """
145
146 try:
147 properties = self.build.getProperties()
148
149 if done and self.descriptionDone is not None:
150 return properties.render(self.descriptionDone)
151 if self.description is not None:
152 return properties.render(self.description)
153
154
155
156 if not self.command:
157 return ["???"]
158
159 words = self.command
160 if isinstance(words, (str, unicode)):
161 words = words.split()
162
163 words = properties.render(words)
164 if len(words) < 1:
165 return ["???"]
166 if len(words) == 1:
167 return ["'%s'" % words[0]]
168 if len(words) == 2:
169 return ["'%s" % words[0], "%s'" % words[1]]
170 return ["'%s" % words[0], "%s" % words[1], "...'"]
171 except:
172 log.msg("Error describing step")
173 log.err()
174 return ["???"]
175
191
192
193
195 if not self.logfiles:
196 return
197 if not self.slaveVersionIsOlderThan("shell", "2.1"):
198 return
199
200
201
202
203 msg1 = ("Warning: buildslave %s is too old "
204 "to understand logfiles=, ignoring it."
205 % self.getSlaveName())
206 msg2 = "You will have to pull this logfile (%s) manually."
207 log.msg(msg1)
208 for logname,remotefilevalue in self.logfiles.items():
209 remotefilename = remotefilevalue
210
211 if type(remotefilevalue) == dict:
212 remotefilename = remotefilevalue['filename']
213
214 newlog = self.addLog(logname)
215 newlog.addHeader(msg1 + "\n")
216 newlog.addHeader(msg2 % remotefilename + "\n")
217 newlog.finish()
218
219 self.logfiles = {}
220
245
246
247
249 name = "treesize"
250 command = ["du", "-s", "-k", "."]
251 description = "measuring tree size"
252 descriptionDone = "tree size measured"
253 kib = None
254
257
264
271
272 - def getText(self, cmd, results):
273 if self.kib is not None:
274 return ["treesize", "%d KiB" % self.kib]
275 return ["treesize", "unknown"]
276
278 name = "setproperty"
279
280 - def __init__(self, property=None, extract_fn=None, strip=True, **kwargs):
295
311
313 props_set = [ "%s: %r" % (k,v) for k,v in self.property_changes.items() ]
314 self.addCompleteLog('property changes', "\n".join(props_set))
315
316 - def getText(self, cmd, results):
317 if self.property_changes:
318 return [ "set props:" ] + self.property_changes.keys()
319 else:
320 return [ "no change" ]
321
330
332 """
333 FileWriter class that just puts received data into a buffer.
334
335 Used to upload a file from slave for inline processing rather than
336 writing into a file on master.
337 """
340
343
346
348 """
349 Remote command subclass used to run an internal file upload command on the
350 slave. We do not need any progress updates from such command, so override
351 remoteUpdate() with an empty method.
352 """
355
357 warnCount = 0
358 warningPattern = '.*warning[: ].*'
359
360 directoryEnterPattern = "make.*: Entering directory [\"`'](.*)['`\"]"
361 directoryLeavePattern = "make.*: Leaving directory"
362 suppressionFile = None
363
364 commentEmptyLineRe = re.compile(r"^\s*(\#.*)?$")
365 suppressionLineRe = re.compile(r"^\s*(.+?)\s*:\s*(.+?)\s*(?:[:]\s*([0-9]+)(?:-([0-9]+))?\s*)?$")
366
367 - def __init__(self, workdir=None,
368 warningPattern=None, warningExtractor=None,
369 directoryEnterPattern=None, directoryLeavePattern=None,
370 suppressionFile=None, **kwargs):
398
403
405 """
406 This method can be used to add patters of warnings that should
407 not be counted.
408
409 It takes a single argument, a list of patterns.
410
411 Each pattern is a 4-tuple (FILE-RE, WARN-RE, START, END).
412
413 FILE-RE is a regular expression (string or compiled regexp), or None.
414 If None, the pattern matches all files, else only files matching the
415 regexp. If directoryEnterPattern is specified in the class constructor,
416 matching is against the full path name, eg. src/main.c.
417
418 WARN-RE is similarly a regular expression matched against the
419 text of the warning, or None to match all warnings.
420
421 START and END form an inclusive line number range to match against. If
422 START is None, there is no lower bound, similarly if END is none there
423 is no upper bound."""
424
425 for fileRe, warnRe, start, end in suppressionList:
426 if fileRe != None and isinstance(fileRe, str):
427 fileRe = re.compile(fileRe)
428 if warnRe != None and isinstance(warnRe, str):
429 warnRe = re.compile(warnRe)
430 self.suppressions.append((fileRe, warnRe, start, end))
431
433 """
434 Extract warning text as the whole line.
435 No file names or line numbers."""
436 return (None, None, line)
437
439 """
440 Extract file name, line number, and warning text as groups (1,2,3)
441 of warningPattern match."""
442 file = match.group(1)
443 lineNo = match.group(2)
444 if lineNo != None:
445 lineNo = int(lineNo)
446 text = match.group(3)
447 return (file, lineNo, text)
448
450 if self.suppressions:
451 (file, lineNo, text) = self.warningExtractor(self, line, match)
452
453 if file != None and file != "" and self.directoryStack:
454 currentDirectory = self.directoryStack[-1]
455 if currentDirectory != None and currentDirectory != "":
456 file = "%s/%s" % (currentDirectory, file)
457
458
459 for fileRe, warnRe, start, end in self.suppressions:
460 if ( (file == None or fileRe == None or fileRe.search(file)) and
461 (warnRe == None or warnRe.search(text)) and
462 ((start == None and end == None) or
463 (lineNo != None and start <= lineNo and end >= lineNo)) ):
464 return
465
466 warnings.append(line)
467 self.warnCount += 1
468
493
515
517 """
518 Match log lines against warningPattern.
519
520 Warnings are collected into another log for this step, and the
521 build-wide 'warnings-count' is updated."""
522
523 self.warnCount = 0
524
525
526
527 if not self.warningPattern:
528 return
529
530 wre = self.warningPattern
531 if isinstance(wre, str):
532 wre = re.compile(wre)
533
534 directoryEnterRe = self.directoryEnterPattern
535 if directoryEnterRe != None and isinstance(directoryEnterRe, str):
536 directoryEnterRe = re.compile(directoryEnterRe)
537
538 directoryLeaveRe = self.directoryLeavePattern
539 if directoryLeaveRe != None and isinstance(directoryLeaveRe, str):
540 directoryLeaveRe = re.compile(directoryLeaveRe)
541
542
543
544
545 warnings = []
546
547
548 for line in log.getText().split("\n"):
549 if directoryEnterRe:
550 match = directoryEnterRe.search(line)
551 if match:
552 self.directoryStack.append(match.group(1))
553 if (directoryLeaveRe and
554 self.directoryStack and
555 directoryLeaveRe.search(line)):
556 self.directoryStack.pop()
557
558 match = wre.match(line)
559 if match:
560 self.maybeAddWarning(warnings, line, match)
561
562
563
564 if self.warnCount:
565 self.addCompleteLog("warnings (%d)" % self.warnCount,
566 "\n".join(warnings) + "\n")
567
568 warnings_stat = self.step_status.getStatistic('warnings', 0)
569 self.step_status.setStatistic('warnings', warnings_stat + self.warnCount)
570
571 try:
572 old_count = self.getProperty("warnings-count")
573 except KeyError:
574 old_count = 0
575 self.setProperty("warnings-count", old_count + self.warnCount, "WarningCountingShellCommand")
576
577
584
585
586 -class Compile(WarningCountingShellCommand):
603
604 -class Test(WarningCountingShellCommand):
646
648 command=["prove", "--lib", "lib", "-r", "t"]
649 total = 0
650
652
653 lines = map(
654 lambda line : line.replace('\r\n','').replace('\r','').replace('\n',''),
655 self.getLog('stdio').readlines()
656 )
657
658 total = 0
659 passed = 0
660 failed = 0
661 rc = SUCCESS
662 if cmd.rc > 0:
663 rc = FAILURE
664
665
666 if "Test Summary Report" in lines:
667 test_summary_report_index = lines.index("Test Summary Report")
668 del lines[0:test_summary_report_index + 2]
669
670 re_test_result = re.compile("^Result: (PASS|FAIL)$|Tests: \d+ Failed: (\d+)\)|Files=\d+, Tests=(\d+)")
671
672 mos = map(lambda line: re_test_result.search(line), lines)
673 test_result_lines = [mo.groups() for mo in mos if mo]
674
675 for line in test_result_lines:
676 if line[0] == 'FAIL':
677 rc = FAILURE
678
679 if line[1]:
680 failed += int(line[1])
681 if line[2]:
682 total = int(line[2])
683
684 else:
685 re_test_result = re.compile("^(All tests successful)|(\d+)/(\d+) subtests failed|Files=\d+, Tests=(\d+),")
686
687 mos = map(lambda line: re_test_result.search(line), lines)
688 test_result_lines = [mo.groups() for mo in mos if mo]
689
690 if test_result_lines:
691 test_result_line = test_result_lines[0]
692
693 success = test_result_line[0]
694
695 if success:
696 failed = 0
697
698 test_totals_line = test_result_lines[1]
699 total_str = test_totals_line[3]
700 else:
701 failed_str = test_result_line[1]
702 failed = int(failed_str)
703
704 total_str = test_result_line[2]
705
706 rc = FAILURE
707
708 total = int(total_str)
709
710 warnings = 0
711 if self.warningPattern:
712 wre = self.warningPattern
713 if isinstance(wre, str):
714 wre = re.compile(wre)
715
716 warnings = len([l for l in lines if wre.search(l)])
717
718
719
720
721 if rc == SUCCESS and warnings:
722 rc = WARNINGS
723
724 if total:
725 passed = total - failed
726
727 self.setTestResults(total=total, failed=failed, passed=passed,
728 warnings=warnings)
729
730 return rc
731