ShellCommand: record RemoteShellCommand args to, to get 'workdir' and 'env' and ...
[buildbot.git] / buildbot / test / test_steps.py
blob68d5ad81213a1c36401b503ea4ba960d55f046d7
1 # -*- test-case-name: buildbot.test.test_steps -*-
3 # create the BuildStep with a fake .remote instance that logs the
4 # .callRemote invocations and compares them against the expected calls. Then
5 # the test harness should send statusUpdate() messages in with assorted
6 # data, eventually calling remote_complete(). Then we can verify that the
7 # Step's rc was correct, and that the status it was supposed to return
8 # matches.
10 # sometimes, .callRemote should raise an exception because of a stale
11 # reference. Sometimes it should errBack with an UnknownCommand failure.
12 # Or other failure.
14 # todo: test batched updates, by invoking remote_update(updates) instead of
15 # statusUpdate(update). Also involves interrupted builds.
17 import os
19 from twisted.trial import unittest
20 from twisted.internet import reactor, defer
22 from buildbot.sourcestamp import SourceStamp
23 from buildbot.process import buildstep, base, factory
24 from buildbot.buildslave import BuildSlave
25 from buildbot.steps import shell, source, python
26 from buildbot.status import builder
27 from buildbot.status.builder import SUCCESS, FAILURE
28 from buildbot.test.runutils import RunMixin, rmtree
29 from buildbot.test.runutils import makeBuildStep, StepTester
30 from buildbot.slave import commands, registry
33 class MyShellCommand(shell.ShellCommand):
34 started = False
35 def runCommand(self, c):
36 self.started = True
37 self.rc = c
38 return shell.ShellCommand.runCommand(self, c)
40 class FakeBuild:
41 pass
42 class FakeBuilder:
43 statusbag = None
44 name = "fakebuilder"
45 class FakeSlaveBuilder:
46 def getSlaveCommandVersion(self, command, oldversion=None):
47 return "1.10"
49 class FakeRemote:
50 def __init__(self):
51 self.events = []
52 self.remoteCalls = 0
53 #self.callRemoteNotifier = None
54 def callRemote(self, methname, *args):
55 event = ["callRemote", methname, args]
56 self.events.append(event)
57 ## if self.callRemoteNotifier:
58 ## reactor.callLater(0, self.callRemoteNotifier, event)
59 self.remoteCalls += 1
60 self.deferred = defer.Deferred()
61 return self.deferred
62 def notifyOnDisconnect(self, callback):
63 pass
64 def dontNotifyOnDisconnect(self, callback):
65 pass
68 class BuildStep(unittest.TestCase):
70 def setUp(self):
71 rmtree("test_steps")
72 self.builder = FakeBuilder()
73 self.builder_status = builder.BuilderStatus("fakebuilder")
74 self.builder_status.basedir = "test_steps"
75 self.builder_status.nextBuildNumber = 0
76 os.mkdir(self.builder_status.basedir)
77 self.build_status = self.builder_status.newBuild()
78 req = base.BuildRequest("reason", SourceStamp())
79 self.build = base.Build([req])
80 self.build.build_status = self.build_status # fake it
81 self.build.builder = self.builder
82 self.build.slavebuilder = FakeSlaveBuilder()
83 self.remote = FakeRemote()
84 self.finished = 0
86 def callback(self, results):
87 self.failed = 0
88 self.failure = None
89 self.results = results
90 self.finished = 1
91 def errback(self, failure):
92 self.failed = 1
93 self.failure = failure
94 self.results = None
95 self.finished = 1
97 def testShellCommand1(self):
98 cmd = "argle bargle"
99 dir = "murkle"
100 self.expectedEvents = []
101 buildstep.RemoteCommand.commandCounter[0] = 3
102 c = MyShellCommand(workdir=dir, command=cmd, timeout=10)
103 c.setBuild(self.build)
104 c.setBuildSlave(BuildSlave("name", "password"))
105 self.assertEqual(self.remote.events, self.expectedEvents)
106 c.step_status = self.build_status.addStepWithName("myshellcommand")
107 d = c.startStep(self.remote)
108 self.failUnless(c.started)
109 d.addCallbacks(self.callback, self.errback)
110 d2 = self.poll()
111 d2.addCallback(self._testShellCommand1_2, c)
112 return d2
113 testShellCommand1.timeout = 10
115 def poll(self, ignored=None):
116 # TODO: This is gross, but at least it's no longer using
117 # reactor.iterate() . Still, get rid of this some day soon.
118 if self.remote.remoteCalls == 0:
119 d = defer.Deferred()
120 d.addCallback(self.poll)
121 reactor.callLater(0.1, d.callback, None)
122 return d
123 return defer.succeed(None)
125 def _testShellCommand1_2(self, res, c):
126 rc = c.rc
127 self.expectedEvents.append(["callRemote", "startCommand",
128 (rc, "3",
129 "shell",
130 {'command': "argle bargle",
131 'workdir': "murkle",
132 'want_stdout': 1,
133 'want_stderr': 1,
134 'logfiles': {},
135 'timeout': 10,
136 'env': None}) ] )
137 self.assertEqual(self.remote.events, self.expectedEvents)
139 # we could do self.remote.deferred.errback(UnknownCommand) here. We
140 # could also do .callback(), but generally the master end silently
141 # ignores the slave's ack
143 logs = c.step_status.getLogs()
144 for log in logs:
145 if log.getName() == "log":
146 break
148 rc.remoteUpdate({'header':
149 "command 'argle bargle' in dir 'murkle'\n\n"})
150 rc.remoteUpdate({'stdout': "foo\n"})
151 self.assertEqual(log.getText(), "foo\n")
152 self.assertEqual(log.getTextWithHeaders(),
153 "command 'argle bargle' in dir 'murkle'\n\n"
154 "foo\n")
155 rc.remoteUpdate({'stderr': "bar\n"})
156 self.assertEqual(log.getText(), "foo\nbar\n")
157 self.assertEqual(log.getTextWithHeaders(),
158 "command 'argle bargle' in dir 'murkle'\n\n"
159 "foo\nbar\n")
160 rc.remoteUpdate({'rc': 0})
161 self.assertEqual(rc.rc, 0)
163 rc.remote_complete()
164 # that should fire the Deferred
165 d = self.poll2()
166 d.addCallback(self._testShellCommand1_3)
167 return d
169 def poll2(self, ignored=None):
170 if not self.finished:
171 d = defer.Deferred()
172 d.addCallback(self.poll2)
173 reactor.callLater(0.1, d.callback, None)
174 return d
175 return defer.succeed(None)
177 def _testShellCommand1_3(self, res):
178 self.assertEqual(self.failed, 0)
179 self.assertEqual(self.results, 0)
182 class MyObserver(buildstep.LogObserver):
183 out = ""
184 def outReceived(self, data):
185 self.out = self.out + data
187 class Steps(unittest.TestCase):
188 def testMultipleStepInstances(self):
189 steps = [
190 (source.CVS, {'cvsroot': "root", 'cvsmodule': "module"}),
191 (shell.Configure, {'command': "./configure"}),
192 (shell.Compile, {'command': "make"}),
193 (shell.Compile, {'command': "make more"}),
194 (shell.Compile, {'command': "make evenmore"}),
195 (shell.Test, {'command': "make test"}),
196 (shell.Test, {'command': "make testharder"}),
198 f = factory.ConfigurableBuildFactory(steps)
199 req = base.BuildRequest("reason", SourceStamp())
200 b = f.newBuild([req])
201 #for s in b.steps: print s.name
203 def failUnlessClones(self, s1, attrnames):
204 f1 = s1.getStepFactory()
205 f,args = f1
206 s2 = f(**args)
207 for name in attrnames:
208 self.failUnlessEqual(getattr(s1, name), getattr(s2, name))
210 def clone(self, s1):
211 f1 = s1.getStepFactory()
212 f,args = f1
213 s2 = f(**args)
214 return s2
216 def testClone(self):
217 s1 = shell.ShellCommand(command=["make", "test"],
218 timeout=1234,
219 workdir="here",
220 description="yo",
221 descriptionDone="yoyo",
222 env={'key': 'value'},
223 want_stdout=False,
224 want_stderr=False,
225 logfiles={"name": "filename"},
227 shellparms = (buildstep.BuildStep.parms +
228 ("remote_kwargs description descriptionDone "
229 "command logfiles").split() )
230 self.failUnlessClones(s1, shellparms)
233 # test the various methods available to buildsteps
235 def test_getProperty(self):
236 s = makeBuildStep("test_steps.Steps.test_getProperty")
237 bs = s.step_status.getBuild()
239 s.setProperty("prop1", "value1")
240 s.setProperty("prop2", "value2")
241 self.failUnlessEqual(s.getProperty("prop1"), "value1")
242 self.failUnlessEqual(bs.getProperty("prop1"), "value1")
243 self.failUnlessEqual(s.getProperty("prop2"), "value2")
244 self.failUnlessEqual(bs.getProperty("prop2"), "value2")
245 s.setProperty("prop1", "value1a")
246 self.failUnlessEqual(s.getProperty("prop1"), "value1a")
247 self.failUnlessEqual(bs.getProperty("prop1"), "value1a")
250 def test_addURL(self):
251 s = makeBuildStep("test_steps.Steps.test_addURL")
252 s.addURL("coverage", "http://coverage.example.org/target")
253 s.addURL("icon", "http://coverage.example.org/icon.png")
254 bs = s.step_status
255 links = bs.getURLs()
256 expected = {"coverage": "http://coverage.example.org/target",
257 "icon": "http://coverage.example.org/icon.png",
259 self.failUnlessEqual(links, expected)
261 def test_addLog(self):
262 s = makeBuildStep("test_steps.Steps.test_addLog")
263 l = s.addLog("newlog")
264 l.addStdout("some stdout here")
265 l.finish()
266 bs = s.step_status
267 logs = bs.getLogs()
268 self.failUnlessEqual(len(logs), 1)
269 l1 = logs[0]
270 self.failUnlessEqual(l1.getText(), "some stdout here")
271 l1a = s.getLog("newlog")
272 self.failUnlessEqual(l1a.getText(), "some stdout here")
274 def test_addHTMLLog(self):
275 s = makeBuildStep("test_steps.Steps.test_addHTMLLog")
276 l = s.addHTMLLog("newlog", "some html here")
277 bs = s.step_status
278 logs = bs.getLogs()
279 self.failUnlessEqual(len(logs), 1)
280 l1 = logs[0]
281 self.failUnless(isinstance(l1, builder.HTMLLogFile))
282 self.failUnlessEqual(l1.getText(), "some html here")
284 def test_addCompleteLog(self):
285 s = makeBuildStep("test_steps.Steps.test_addCompleteLog")
286 l = s.addCompleteLog("newlog", "some stdout here")
287 bs = s.step_status
288 logs = bs.getLogs()
289 self.failUnlessEqual(len(logs), 1)
290 l1 = logs[0]
291 self.failUnlessEqual(l1.getText(), "some stdout here")
292 l1a = s.getLog("newlog")
293 self.failUnlessEqual(l1a.getText(), "some stdout here")
295 def test_addLogObserver(self):
296 s = makeBuildStep("test_steps.Steps.test_addLogObserver")
297 bss = s.step_status
298 o1,o2,o3 = MyObserver(), MyObserver(), MyObserver()
300 # add the log before the observer
301 l1 = s.addLog("one")
302 l1.addStdout("onestuff")
303 s.addLogObserver("one", o1)
304 self.failUnlessEqual(o1.out, "onestuff")
305 l1.addStdout(" morestuff")
306 self.failUnlessEqual(o1.out, "onestuff morestuff")
308 # add the observer before the log
309 s.addLogObserver("two", o2)
310 l2 = s.addLog("two")
311 l2.addStdout("twostuff")
312 self.failUnlessEqual(o2.out, "twostuff")
314 # test more stuff about ShellCommands
316 def test_description(self):
317 s = makeBuildStep("test_steps.Steps.test_description.1",
318 step_class=shell.ShellCommand,
319 workdir="dummy",
320 description=["list", "of", "strings"],
321 descriptionDone=["another", "list"])
322 self.failUnlessEqual(s.description, ["list", "of", "strings"])
323 self.failUnlessEqual(s.descriptionDone, ["another", "list"])
325 s = makeBuildStep("test_steps.Steps.test_description.2",
326 step_class=shell.ShellCommand,
327 workdir="dummy",
328 description="single string",
329 descriptionDone="another string")
330 self.failUnlessEqual(s.description, ["single string"])
331 self.failUnlessEqual(s.descriptionDone, ["another string"])
333 class VersionCheckingStep(buildstep.BuildStep):
334 def start(self):
335 # give our test a chance to run. It is non-trivial for a buildstep to
336 # claw its way back out to the test case which is currently running.
337 master = self.build.builder.botmaster.parent
338 checker = master._checker
339 checker(self)
340 # then complete
341 self.finished(buildstep.SUCCESS)
343 version_config = """
344 from buildbot.process import factory
345 from buildbot.test.test_steps import VersionCheckingStep
346 from buildbot.buildslave import BuildSlave
347 BuildmasterConfig = c = {}
348 f1 = factory.BuildFactory([
349 factory.s(VersionCheckingStep),
351 c['slaves'] = [BuildSlave('bot1', 'sekrit')]
352 c['schedulers'] = []
353 c['builders'] = [{'name':'quick', 'slavename':'bot1',
354 'builddir': 'quickdir', 'factory': f1}]
355 c['slavePortnum'] = 0
358 class SlaveVersion(RunMixin, unittest.TestCase):
359 def setUp(self):
360 RunMixin.setUp(self)
361 self.master.loadConfig(version_config)
362 self.master.startService()
363 d = self.connectSlave(["quick"])
364 return d
366 def doBuild(self, buildername):
367 br = base.BuildRequest("forced", SourceStamp())
368 d = br.waitUntilFinished()
369 self.control.getBuilder(buildername).requestBuild(br)
370 return d
373 def checkCompare(self, s):
374 cver = commands.command_version
375 v = s.slaveVersion("svn", None)
376 # this insures that we are getting the version correctly
377 self.failUnlessEqual(s.slaveVersion("svn", None), cver)
378 # and that non-existent commands do not provide a version
379 self.failUnlessEqual(s.slaveVersion("NOSUCHCOMMAND"), None)
380 # TODO: verify that a <=0.5.0 buildslave (which does not implement
381 # remote_getCommands) handles oldversion= properly. This requires a
382 # mutant slave which does not offer that method.
383 #self.failUnlessEqual(s.slaveVersion("NOSUCHCOMMAND", "old"), "old")
385 # now check the comparison functions
386 self.failIf(s.slaveVersionIsOlderThan("svn", cver))
387 self.failIf(s.slaveVersionIsOlderThan("svn", "1.1"))
388 self.failUnless(s.slaveVersionIsOlderThan("svn", cver + ".1"))
390 self.failUnlessEqual(s.getSlaveName(), "bot1")
392 def testCompare(self):
393 self.master._checker = self.checkCompare
394 d = self.doBuild("quick")
395 return d
398 class _SimpleBuildStep(buildstep.BuildStep):
399 def start(self):
400 args = {"arg1": "value"}
401 cmd = buildstep.RemoteCommand("simple", args)
402 d = self.runCommand(cmd)
403 d.addCallback(lambda res: self.finished(SUCCESS))
405 class _SimpleCommand(commands.Command):
406 def start(self):
407 self.builder.flag = True
408 self.builder.flag_args = self.args
409 return defer.succeed(None)
411 class CheckStepTester(StepTester, unittest.TestCase):
412 def testSimple(self):
413 self.slavebase = "testSimple.slave"
414 self.masterbase = "testSimple.master"
415 sb = self.makeSlaveBuilder()
416 sb.flag = False
417 registry.registerSlaveCommand("simple", _SimpleCommand, "1")
418 step = self.makeStep(_SimpleBuildStep)
419 d = self.runStep(step)
420 def _checkSimple(results):
421 self.failUnless(sb.flag)
422 self.failUnlessEqual(sb.flag_args, {"arg1": "value"})
423 d.addCallback(_checkSimple)
424 return d
426 class Python(StepTester, unittest.TestCase):
427 def testPyFlakes1(self):
428 self.masterbase = "Python.testPyFlakes1"
429 step = self.makeStep(python.PyFlakes)
430 output = \
431 """pyflakes buildbot
432 buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
433 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
434 buildbot/clients/debug.py:9: 'gnome' imported but unused
435 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
436 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
437 buildbot/scripts/imaginary.py:12: undefined name 'size'
438 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
440 log = step.addLog("stdio")
441 log.addStdout(output)
442 log.finish()
443 step.createSummary(log)
444 desc = step.descriptionDone
445 self.failUnless("unused=2" in desc)
446 self.failUnless("undefined=1" in desc)
447 self.failUnless("redefs=3" in desc)
448 self.failUnless("import*=1" in desc)
449 self.failIf("misc=" in desc)
451 self.failUnlessEqual(step.getProperty("pyflakes-unused"), 2)
452 self.failUnlessEqual(step.getProperty("pyflakes-undefined"), 1)
453 self.failUnlessEqual(step.getProperty("pyflakes-redefs"), 3)
454 self.failUnlessEqual(step.getProperty("pyflakes-import*"), 1)
455 self.failUnlessEqual(step.getProperty("pyflakes-misc"), 0)
456 self.failUnlessEqual(step.getProperty("pyflakes-total"), 7)
458 logs = {}
459 for log in step.step_status.getLogs():
460 logs[log.getName()] = log
462 for name in ["unused", "undefined", "redefs", "import*"]:
463 self.failUnless(name in logs)
464 self.failIf("misc" in logs)
465 lines = logs["unused"].readlines()
466 self.failUnlessEqual(len(lines), 2)
467 self.failUnlessEqual(lines[0], "buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused\n")
469 cmd = buildstep.RemoteCommand(None, {})
470 cmd.rc = 0
471 results = step.evaluateCommand(cmd)
472 self.failUnlessEqual(results, FAILURE) # because of the 'undefined'
474 def testPyFlakes2(self):
475 self.masterbase = "Python.testPyFlakes2"
476 step = self.makeStep(python.PyFlakes)
477 output = \
478 """pyflakes buildbot
479 some more text here that should be ignored
480 buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
481 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
482 buildbot/clients/debug.py:9: 'gnome' imported but unused
483 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
484 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
485 buildbot/scripts/imaginary.py:12: undefined name 'size'
486 could not compile 'blah/blah.py':3:
487 pretend there was an invalid line here
488 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
490 log = step.addLog("stdio")
491 log.addStdout(output)
492 log.finish()
493 step.createSummary(log)
494 desc = step.descriptionDone
495 self.failUnless("unused=2" in desc)
496 self.failUnless("undefined=1" in desc)
497 self.failUnless("redefs=3" in desc)
498 self.failUnless("import*=1" in desc)
499 self.failUnless("misc=2" in desc)
502 def testPyFlakes3(self):
503 self.masterbase = "Python.testPyFlakes3"
504 step = self.makeStep(python.PyFlakes)
505 output = \
506 """buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
507 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
508 buildbot/clients/debug.py:9: 'gnome' imported but unused
509 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
510 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
511 buildbot/scripts/imaginary.py:12: undefined name 'size'
512 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
514 log = step.addLog("stdio")
515 log.addStdout(output)
516 log.finish()
517 step.createSummary(log)
518 desc = step.descriptionDone
519 self.failUnless("unused=2" in desc)
520 self.failUnless("undefined=1" in desc)
521 self.failUnless("redefs=3" in desc)
522 self.failUnless("import*=1" in desc)
523 self.failIf("misc" in desc)