rename c['bots'] to c['slaves'], and use buildbot.slave.BuildSlave instances instead...
[buildbot.git] / buildbot / test / test_steps.py
blob79eba5971a70996664aca09ef0ed8edeb0a39f5f
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.steps import shell, source, python
25 from buildbot.status import builder
26 from buildbot.status.builder import SUCCESS, FAILURE
27 from buildbot.test.runutils import RunMixin, rmtree
28 from buildbot.test.runutils import makeBuildStep, StepTester
29 from buildbot.slave import commands, registry
32 class MyShellCommand(shell.ShellCommand):
33 started = False
34 def runCommand(self, c):
35 self.started = True
36 self.rc = c
37 return shell.ShellCommand.runCommand(self, c)
39 class FakeBuild:
40 pass
41 class FakeBuilder:
42 statusbag = None
43 name = "fakebuilder"
44 class FakeSlaveBuilder:
45 def getSlaveCommandVersion(self, command, oldversion=None):
46 return "1.10"
48 class FakeRemote:
49 def __init__(self):
50 self.events = []
51 self.remoteCalls = 0
52 #self.callRemoteNotifier = None
53 def callRemote(self, methname, *args):
54 event = ["callRemote", methname, args]
55 self.events.append(event)
56 ## if self.callRemoteNotifier:
57 ## reactor.callLater(0, self.callRemoteNotifier, event)
58 self.remoteCalls += 1
59 self.deferred = defer.Deferred()
60 return self.deferred
61 def notifyOnDisconnect(self, callback):
62 pass
63 def dontNotifyOnDisconnect(self, callback):
64 pass
67 class BuildStep(unittest.TestCase):
69 def setUp(self):
70 rmtree("test_steps")
71 self.builder = FakeBuilder()
72 self.builder_status = builder.BuilderStatus("fakebuilder")
73 self.builder_status.basedir = "test_steps"
74 self.builder_status.nextBuildNumber = 0
75 os.mkdir(self.builder_status.basedir)
76 self.build_status = self.builder_status.newBuild()
77 req = base.BuildRequest("reason", SourceStamp())
78 self.build = base.Build([req])
79 self.build.build_status = self.build_status # fake it
80 self.build.builder = self.builder
81 self.build.slavebuilder = FakeSlaveBuilder()
82 self.remote = FakeRemote()
83 self.finished = 0
85 def callback(self, results):
86 self.failed = 0
87 self.failure = None
88 self.results = results
89 self.finished = 1
90 def errback(self, failure):
91 self.failed = 1
92 self.failure = failure
93 self.results = None
94 self.finished = 1
96 def testShellCommand1(self):
97 cmd = "argle bargle"
98 dir = "murkle"
99 self.expectedEvents = []
100 buildstep.RemoteCommand.commandCounter[0] = 3
101 c = MyShellCommand(workdir=dir, command=cmd, timeout=10)
102 c.setBuild(self.build)
103 self.assertEqual(self.remote.events, self.expectedEvents)
104 c.step_status = self.build_status.addStepWithName("myshellcommand")
105 d = c.startStep(self.remote)
106 self.failUnless(c.started)
107 d.addCallbacks(self.callback, self.errback)
108 d2 = self.poll()
109 d2.addCallback(self._testShellCommand1_2, c)
110 return d2
111 testShellCommand1.timeout = 10
113 def poll(self, ignored=None):
114 # TODO: This is gross, but at least it's no longer using
115 # reactor.iterate() . Still, get rid of this some day soon.
116 if self.remote.remoteCalls == 0:
117 d = defer.Deferred()
118 d.addCallback(self.poll)
119 reactor.callLater(0.1, d.callback, None)
120 return d
121 return defer.succeed(None)
123 def _testShellCommand1_2(self, res, c):
124 rc = c.rc
125 self.expectedEvents.append(["callRemote", "startCommand",
126 (rc, "3",
127 "shell",
128 {'command': "argle bargle",
129 'workdir': "murkle",
130 'want_stdout': 1,
131 'want_stderr': 1,
132 'logfiles': {},
133 'timeout': 10,
134 'env': None}) ] )
135 self.assertEqual(self.remote.events, self.expectedEvents)
137 # we could do self.remote.deferred.errback(UnknownCommand) here. We
138 # could also do .callback(), but generally the master end silently
139 # ignores the slave's ack
141 logs = c.step_status.getLogs()
142 for log in logs:
143 if log.getName() == "log":
144 break
146 rc.remoteUpdate({'header':
147 "command 'argle bargle' in dir 'murkle'\n\n"})
148 rc.remoteUpdate({'stdout': "foo\n"})
149 self.assertEqual(log.getText(), "foo\n")
150 self.assertEqual(log.getTextWithHeaders(),
151 "command 'argle bargle' in dir 'murkle'\n\n"
152 "foo\n")
153 rc.remoteUpdate({'stderr': "bar\n"})
154 self.assertEqual(log.getText(), "foo\nbar\n")
155 self.assertEqual(log.getTextWithHeaders(),
156 "command 'argle bargle' in dir 'murkle'\n\n"
157 "foo\nbar\n")
158 rc.remoteUpdate({'rc': 0})
159 self.assertEqual(rc.rc, 0)
161 rc.remote_complete()
162 # that should fire the Deferred
163 d = self.poll2()
164 d.addCallback(self._testShellCommand1_3)
165 return d
167 def poll2(self, ignored=None):
168 if not self.finished:
169 d = defer.Deferred()
170 d.addCallback(self.poll2)
171 reactor.callLater(0.1, d.callback, None)
172 return d
173 return defer.succeed(None)
175 def _testShellCommand1_3(self, res):
176 self.assertEqual(self.failed, 0)
177 self.assertEqual(self.results, 0)
180 class MyObserver(buildstep.LogObserver):
181 out = ""
182 def outReceived(self, data):
183 self.out = self.out + data
185 class Steps(unittest.TestCase):
186 def testMultipleStepInstances(self):
187 steps = [
188 (source.CVS, {'cvsroot': "root", 'cvsmodule': "module"}),
189 (shell.Configure, {'command': "./configure"}),
190 (shell.Compile, {'command': "make"}),
191 (shell.Compile, {'command': "make more"}),
192 (shell.Compile, {'command': "make evenmore"}),
193 (shell.Test, {'command': "make test"}),
194 (shell.Test, {'command': "make testharder"}),
196 f = factory.ConfigurableBuildFactory(steps)
197 req = base.BuildRequest("reason", SourceStamp())
198 b = f.newBuild([req])
199 #for s in b.steps: print s.name
201 # test the various methods available to buildsteps
203 def test_getProperty(self):
204 s = makeBuildStep("test_steps.Steps.test_getProperty")
205 bs = s.step_status.getBuild()
207 s.setProperty("prop1", "value1")
208 s.setProperty("prop2", "value2")
209 self.failUnlessEqual(s.getProperty("prop1"), "value1")
210 self.failUnlessEqual(bs.getProperty("prop1"), "value1")
211 self.failUnlessEqual(s.getProperty("prop2"), "value2")
212 self.failUnlessEqual(bs.getProperty("prop2"), "value2")
213 s.setProperty("prop1", "value1a")
214 self.failUnlessEqual(s.getProperty("prop1"), "value1a")
215 self.failUnlessEqual(bs.getProperty("prop1"), "value1a")
218 def test_addURL(self):
219 s = makeBuildStep("test_steps.Steps.test_addURL")
220 s.addURL("coverage", "http://coverage.example.org/target")
221 s.addURL("icon", "http://coverage.example.org/icon.png")
222 bs = s.step_status
223 links = bs.getURLs()
224 expected = {"coverage": "http://coverage.example.org/target",
225 "icon": "http://coverage.example.org/icon.png",
227 self.failUnlessEqual(links, expected)
229 def test_addLog(self):
230 s = makeBuildStep("test_steps.Steps.test_addLog")
231 l = s.addLog("newlog")
232 l.addStdout("some stdout here")
233 l.finish()
234 bs = s.step_status
235 logs = bs.getLogs()
236 self.failUnlessEqual(len(logs), 1)
237 l1 = logs[0]
238 self.failUnlessEqual(l1.getText(), "some stdout here")
239 l1a = s.getLog("newlog")
240 self.failUnlessEqual(l1a.getText(), "some stdout here")
242 def test_addHTMLLog(self):
243 s = makeBuildStep("test_steps.Steps.test_addHTMLLog")
244 l = s.addHTMLLog("newlog", "some html here")
245 bs = s.step_status
246 logs = bs.getLogs()
247 self.failUnlessEqual(len(logs), 1)
248 l1 = logs[0]
249 self.failUnless(isinstance(l1, builder.HTMLLogFile))
250 self.failUnlessEqual(l1.getText(), "some html here")
252 def test_addCompleteLog(self):
253 s = makeBuildStep("test_steps.Steps.test_addCompleteLog")
254 l = s.addCompleteLog("newlog", "some stdout here")
255 bs = s.step_status
256 logs = bs.getLogs()
257 self.failUnlessEqual(len(logs), 1)
258 l1 = logs[0]
259 self.failUnlessEqual(l1.getText(), "some stdout here")
260 l1a = s.getLog("newlog")
261 self.failUnlessEqual(l1a.getText(), "some stdout here")
263 def test_addLogObserver(self):
264 s = makeBuildStep("test_steps.Steps.test_addLogObserver")
265 bss = s.step_status
266 o1,o2,o3 = MyObserver(), MyObserver(), MyObserver()
268 # add the log before the observer
269 l1 = s.addLog("one")
270 l1.addStdout("onestuff")
271 s.addLogObserver("one", o1)
272 self.failUnlessEqual(o1.out, "onestuff")
273 l1.addStdout(" morestuff")
274 self.failUnlessEqual(o1.out, "onestuff morestuff")
276 # add the observer before the log
277 s.addLogObserver("two", o2)
278 l2 = s.addLog("two")
279 l2.addStdout("twostuff")
280 self.failUnlessEqual(o2.out, "twostuff")
282 # test more stuff about ShellCommands
284 def test_description(self):
285 s = makeBuildStep("test_steps.Steps.test_description.1",
286 step_class=shell.ShellCommand,
287 workdir="dummy",
288 description=["list", "of", "strings"],
289 descriptionDone=["another", "list"])
290 self.failUnlessEqual(s.description, ["list", "of", "strings"])
291 self.failUnlessEqual(s.descriptionDone, ["another", "list"])
293 s = makeBuildStep("test_steps.Steps.test_description.2",
294 step_class=shell.ShellCommand,
295 workdir="dummy",
296 description="single string",
297 descriptionDone="another string")
298 self.failUnlessEqual(s.description, ["single string"])
299 self.failUnlessEqual(s.descriptionDone, ["another string"])
301 class VersionCheckingStep(buildstep.BuildStep):
302 def start(self):
303 # give our test a chance to run. It is non-trivial for a buildstep to
304 # claw its way back out to the test case which is currently running.
305 master = self.build.builder.botmaster.parent
306 checker = master._checker
307 checker(self)
308 # then complete
309 self.finished(buildstep.SUCCESS)
311 version_config = """
312 from buildbot.process import factory
313 from buildbot.test.test_steps import VersionCheckingStep
314 from buildbot.slave import BuildSlave
315 BuildmasterConfig = c = {}
316 f1 = factory.BuildFactory([
317 factory.s(VersionCheckingStep),
319 c['slaves'] = [BuildSlave('bot1', 'sekrit')]
320 c['sources'] = []
321 c['schedulers'] = []
322 c['builders'] = [{'name':'quick', 'slavename':'bot1',
323 'builddir': 'quickdir', 'factory': f1}]
324 c['slavePortnum'] = 0
327 class SlaveVersion(RunMixin, unittest.TestCase):
328 def setUp(self):
329 RunMixin.setUp(self)
330 self.master.loadConfig(version_config)
331 self.master.startService()
332 d = self.connectSlave(["quick"])
333 return d
335 def doBuild(self, buildername):
336 br = base.BuildRequest("forced", SourceStamp())
337 d = br.waitUntilFinished()
338 self.control.getBuilder(buildername).requestBuild(br)
339 return d
342 def checkCompare(self, s):
343 cver = commands.command_version
344 v = s.slaveVersion("svn", None)
345 # this insures that we are getting the version correctly
346 self.failUnlessEqual(s.slaveVersion("svn", None), cver)
347 # and that non-existent commands do not provide a version
348 self.failUnlessEqual(s.slaveVersion("NOSUCHCOMMAND"), None)
349 # TODO: verify that a <=0.5.0 buildslave (which does not implement
350 # remote_getCommands) handles oldversion= properly. This requires a
351 # mutant slave which does not offer that method.
352 #self.failUnlessEqual(s.slaveVersion("NOSUCHCOMMAND", "old"), "old")
354 # now check the comparison functions
355 self.failIf(s.slaveVersionIsOlderThan("svn", cver))
356 self.failIf(s.slaveVersionIsOlderThan("svn", "1.1"))
357 self.failUnless(s.slaveVersionIsOlderThan("svn", cver + ".1"))
359 self.failUnlessEqual(s.getSlaveName(), "bot1")
361 def testCompare(self):
362 self.master._checker = self.checkCompare
363 d = self.doBuild("quick")
364 return d
367 class _SimpleBuildStep(buildstep.BuildStep):
368 def start(self):
369 args = {"arg1": "value"}
370 cmd = buildstep.RemoteCommand("simple", args)
371 d = self.runCommand(cmd)
372 d.addCallback(lambda res: self.finished(SUCCESS))
374 class _SimpleCommand(commands.Command):
375 def start(self):
376 self.builder.flag = True
377 self.builder.flag_args = self.args
378 return defer.succeed(None)
380 class CheckStepTester(StepTester, unittest.TestCase):
381 def testSimple(self):
382 self.slavebase = "testSimple.slave"
383 self.masterbase = "testSimple.master"
384 sb = self.makeSlaveBuilder()
385 sb.flag = False
386 registry.registerSlaveCommand("simple", _SimpleCommand, "1")
387 step = self.makeStep(_SimpleBuildStep)
388 d = self.runStep(step)
389 def _checkSimple(results):
390 self.failUnless(sb.flag)
391 self.failUnlessEqual(sb.flag_args, {"arg1": "value"})
392 d.addCallback(_checkSimple)
393 return d
395 class Python(StepTester, unittest.TestCase):
396 def testPyFlakes1(self):
397 self.masterbase = "Python.testPyFlakes1"
398 step = self.makeStep(python.PyFlakes)
399 output = \
400 """pyflakes buildbot
401 buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
402 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
403 buildbot/clients/debug.py:9: 'gnome' imported but unused
404 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
405 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
406 buildbot/scripts/imaginary.py:12: undefined name 'size'
407 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
409 log = step.addLog("stdio")
410 log.addStdout(output)
411 log.finish()
412 step.createSummary(log)
413 desc = step.descriptionDone
414 self.failUnless("unused=2" in desc)
415 self.failUnless("undefined=1" in desc)
416 self.failUnless("redefs=3" in desc)
417 self.failUnless("import*=1" in desc)
418 self.failIf("misc=" in desc)
420 self.failUnlessEqual(step.getProperty("pyflakes-unused"), 2)
421 self.failUnlessEqual(step.getProperty("pyflakes-undefined"), 1)
422 self.failUnlessEqual(step.getProperty("pyflakes-redefs"), 3)
423 self.failUnlessEqual(step.getProperty("pyflakes-import*"), 1)
424 self.failUnlessEqual(step.getProperty("pyflakes-misc"), 0)
425 self.failUnlessEqual(step.getProperty("pyflakes-total"), 7)
427 logs = {}
428 for log in step.step_status.getLogs():
429 logs[log.getName()] = log
431 for name in ["unused", "undefined", "redefs", "import*"]:
432 self.failUnless(name in logs)
433 self.failIf("misc" in logs)
434 lines = logs["unused"].readlines()
435 self.failUnlessEqual(len(lines), 2)
436 self.failUnlessEqual(lines[0], "buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused\n")
438 cmd = buildstep.RemoteCommand(None, {})
439 cmd.rc = 0
440 results = step.evaluateCommand(cmd)
441 self.failUnlessEqual(results, FAILURE) # because of the 'undefined'
443 def testPyFlakes2(self):
444 self.masterbase = "Python.testPyFlakes2"
445 step = self.makeStep(python.PyFlakes)
446 output = \
447 """pyflakes buildbot
448 some more text here that should be ignored
449 buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
450 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
451 buildbot/clients/debug.py:9: 'gnome' imported but unused
452 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
453 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
454 buildbot/scripts/imaginary.py:12: undefined name 'size'
455 could not compile 'blah/blah.py':3:
456 pretend there was an invalid line here
457 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
459 log = step.addLog("stdio")
460 log.addStdout(output)
461 log.finish()
462 step.createSummary(log)
463 desc = step.descriptionDone
464 self.failUnless("unused=2" in desc)
465 self.failUnless("undefined=1" in desc)
466 self.failUnless("redefs=3" in desc)
467 self.failUnless("import*=1" in desc)
468 self.failUnless("misc=2" in desc)
471 def testPyFlakes3(self):
472 self.masterbase = "Python.testPyFlakes3"
473 step = self.makeStep(python.PyFlakes)
474 output = \
475 """buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
476 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
477 buildbot/clients/debug.py:9: 'gnome' imported but unused
478 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
479 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
480 buildbot/scripts/imaginary.py:12: undefined name 'size'
481 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
483 log = step.addLog("stdio")
484 log.addStdout(output)
485 log.finish()
486 step.createSummary(log)
487 desc = step.descriptionDone
488 self.failUnless("unused=2" in desc)
489 self.failUnless("undefined=1" in desc)
490 self.failUnless("redefs=3" in desc)
491 self.failUnless("import*=1" in desc)
492 self.failIf("misc" in desc)