1
2
3 import re
4 from twisted.python import log
5 from buildbot.process.buildstep import LoggingBuildStep, RemoteShellCommand
6 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, STDOUT, STDERR
7
8
9
10 from buildbot.process.properties import WithProperties
11 _hush_pyflakes = [WithProperties]
12 del _hush_pyflakes
13
15 """I run a single shell command on the buildslave. I return FAILURE if
16 the exit code of that command is non-zero, SUCCESS otherwise. To change
17 this behavior, override my .evaluateCommand method.
18
19 By default, a failure of this step will mark the whole build as FAILURE.
20 To override this, give me an argument of flunkOnFailure=False .
21
22 I create a single Log named 'log' which contains the output of the
23 command. To create additional summary Logs, override my .createSummary
24 method.
25
26 The shell command I run (a list of argv strings) can be provided in
27 several ways:
28 - a class-level .command attribute
29 - a command= parameter to my constructor (overrides .command)
30 - set explicitly with my .setCommand() method (overrides both)
31
32 @ivar command: a list of renderable objects (typically strings or
33 WithProperties instances). This will be used by start()
34 to create a RemoteShellCommand instance.
35
36 @ivar logfiles: a dict mapping log NAMEs to workdir-relative FILENAMEs
37 of their corresponding logfiles. The contents of the file
38 named FILENAME will be put into a LogFile named NAME, ina
39 something approximating real-time. (note that logfiles=
40 is actually handled by our parent class LoggingBuildStep)
41
42 """
43
44 name = "shell"
45 description = None
46 descriptionDone = None
47 command = None
48
49
50
51
52
53
54 flunkOnFailure = True
55
56 - def __init__(self, workdir=None,
57 description=None, descriptionDone=None,
58 command=None,
59 usePTY="slave-config",
60 **kwargs):
95
97 rkw = self.remote_kwargs
98 rkw['workdir'] = rkw['workdir'] or workdir
99
102
104 """Return a list of short strings to describe this step, for the
105 status display. This uses the first few words of the shell command.
106 You can replace this by setting .description in your subclass, or by
107 overriding this method to describe the step better.
108
109 @type done: boolean
110 @param done: whether the command is complete or not, to improve the
111 way the command is described. C{done=False} is used
112 while the command is still running, so a single
113 imperfect-tense verb is appropriate ('compiling',
114 'testing', ...) C{done=True} is used when the command
115 has finished, and the default getText() method adds some
116 text, so a simple noun is appropriate ('compile',
117 'tests' ...)
118 """
119
120 if done and self.descriptionDone is not None:
121 return list(self.descriptionDone)
122 if self.description is not None:
123 return list(self.description)
124
125 properties = self.build.getProperties()
126 words = self.command
127 if isinstance(words, (str, unicode)):
128 words = words.split()
129
130 words = properties.render(words)
131 if len(words) < 1:
132 return ["???"]
133 if len(words) == 1:
134 return ["'%s'" % words[0]]
135 if len(words) == 2:
136 return ["'%s" % words[0], "%s'" % words[1]]
137 return ["'%s" % words[0], "%s" % words[1], "...'"]
138
140
141
142
143
144
145
146 properties = self.build.getProperties()
147 slaveEnv = self.build.slaveEnvironment
148 if slaveEnv:
149 if cmd.args['env'] is None:
150 cmd.args['env'] = {}
151 fullSlaveEnv = slaveEnv.copy()
152 fullSlaveEnv.update(cmd.args['env'])
153 cmd.args['env'] = properties.render(fullSlaveEnv)
154
155
156
158 if not self.logfiles:
159 return
160 if not self.slaveVersionIsOlderThan("shell", "2.1"):
161 return
162
163
164
165
166 msg1 = ("Warning: buildslave %s is too old "
167 "to understand logfiles=, ignoring it."
168 % self.getSlaveName())
169 msg2 = "You will have to pull this logfile (%s) manually."
170 log.msg(msg1)
171 for logname,remotefilevalue in self.logfiles.items():
172 remotefilename = remotefilevalue
173
174 if type(remotefilevalue) == dict:
175 remotefilename = remotefilevalue['filename']
176
177 newlog = self.addLog(logname)
178 newlog.addHeader(msg1 + "\n")
179 newlog.addHeader(msg2 % remotefilename + "\n")
180 newlog.finish()
181
182 self.logfiles = {}
183
208
209
210
212 name = "treesize"
213 command = ["du", "-s", "-k", "."]
214 kib = None
215
217 out = cmd.logs['stdio'].getText()
218 m = re.search(r'^(\d+)', out)
219 if m:
220 self.kib = int(m.group(1))
221 self.setProperty("tree-size-KiB", self.kib, "treesize")
222
229
230 - def getText(self, cmd, results):
231 if self.kib is not None:
232 return ["treesize", "%d KiB" % self.kib]
233 return ["treesize", "unknown"]
234
236 name = "setproperty"
237
239 self.property = None
240 self.extract_fn = None
241 self.strip = True
242
243 if kwargs.has_key('property'):
244 self.property = kwargs['property']
245 del kwargs['property']
246 if kwargs.has_key('extract_fn'):
247 self.extract_fn = kwargs['extract_fn']
248 del kwargs['extract_fn']
249 if kwargs.has_key('strip'):
250 self.strip = kwargs['strip']
251 del kwargs['strip']
252
253 ShellCommand.__init__(self, **kwargs)
254
255 self.addFactoryArguments(property=self.property)
256 self.addFactoryArguments(extract_fn=self.extract_fn)
257 self.addFactoryArguments(strip=self.strip)
258
259 assert self.property or self.extract_fn, \
260 "SetProperty step needs either property= or extract_fn="
261
262 self.property_changes = {}
263
265 if self.property:
266 result = cmd.logs['stdio'].getText()
267 if self.strip: result = result.strip()
268 propname = self.build.getProperties().render(self.property)
269 self.setProperty(propname, result, "SetProperty Step")
270 self.property_changes[propname] = result
271 else:
272 log = cmd.logs['stdio']
273 new_props = self.extract_fn(cmd.rc,
274 ''.join(log.getChunks([STDOUT], onlyText=True)),
275 ''.join(log.getChunks([STDERR], onlyText=True)))
276 for k,v in new_props.items():
277 self.setProperty(k, v, "SetProperty Step")
278 self.property_changes = new_props
279
281 props_set = [ "%s: %r" % (k,v) for k,v in self.property_changes.items() ]
282 self.addCompleteLog('property changes', "\n".join(props_set))
283
284 - def getText(self, cmd, results):
285 if self.property_changes:
286 return [ "set props:" ] + self.property_changes.keys()
287 else:
288 return [ "no change" ]
289
298
359
360
361 -class Compile(WarningCountingShellCommand):
378
379 -class Test(WarningCountingShellCommand):
421
423 command=["prove", "--lib", "lib", "-r", "t"]
424 total = 0
425
427
428 lines = map(
429 lambda line : line.replace('\r\n','').replace('\r','').replace('\n',''),
430 self.getLog('stdio').readlines()
431 )
432
433 total = 0
434 passed = 0
435 failed = 0
436 rc = cmd.rc
437
438
439 try:
440 test_summary_report_index = lines.index("Test Summary Report")
441
442 del lines[0:test_summary_report_index + 2]
443
444 re_test_result = re.compile("^Result: (PASS|FAIL)$|Tests: \d+ Failed: (\d+)\)|Files=\d+, Tests=(\d+)")
445
446 mos = map(lambda line: re_test_result.search(line), lines)
447 test_result_lines = [mo.groups() for mo in mos if mo]
448
449 for line in test_result_lines:
450 if line[0] == 'PASS':
451 rc = SUCCESS
452 elif line[0] == 'FAIL':
453 rc = FAILURE
454 elif line[1]:
455 failed += int(line[1])
456 elif line[2]:
457 total = int(line[2])
458
459 except ValueError:
460 re_test_result = re.compile("^(All tests successful)|(\d+)/(\d+) subtests failed|Files=\d+, Tests=(\d+),")
461
462 mos = map(lambda line: re_test_result.search(line), lines)
463 test_result_lines = [mo.groups() for mo in mos if mo]
464
465 if test_result_lines:
466 test_result_line = test_result_lines[0]
467
468 success = test_result_line[0]
469
470 if success:
471 failed = 0
472
473 test_totals_line = test_result_lines[1]
474 total_str = test_totals_line[3]
475
476 rc = SUCCESS
477 else:
478 failed_str = test_result_line[1]
479 failed = int(failed_str)
480
481 total_str = test_result_line[2]
482
483 rc = FAILURE
484
485 total = int(total_str)
486
487 if total:
488 passed = total - failed
489
490 self.setTestResults(total=total, failed=failed, passed=passed)
491
492 return rc
493