move BuildSlave to new class, merge with BotPerspective, make it long-lived
[buildbot.git] / buildbot / test / test_steps.py
blobf3a8b25a7f031ddcd12c43c485a83aa96c398c6b
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.buildslave import BuildSlave
315 BuildmasterConfig = c = {}
316 f1 = factory.BuildFactory([
317 factory.s(VersionCheckingStep),
319 c['slaves'] = [BuildSlave('bot1', 'sekrit')]
320 c['schedulers'] = []
321 c['builders'] = [{'name':'quick', 'slavename':'bot1',
322 'builddir': 'quickdir', 'factory': f1}]
323 c['slavePortnum'] = 0
326 class SlaveVersion(RunMixin, unittest.TestCase):
327 def setUp(self):
328 RunMixin.setUp(self)
329 self.master.loadConfig(version_config)
330 self.master.startService()
331 d = self.connectSlave(["quick"])
332 return d
334 def doBuild(self, buildername):
335 br = base.BuildRequest("forced", SourceStamp())
336 d = br.waitUntilFinished()
337 self.control.getBuilder(buildername).requestBuild(br)
338 return d
341 def checkCompare(self, s):
342 cver = commands.command_version
343 v = s.slaveVersion("svn", None)
344 # this insures that we are getting the version correctly
345 self.failUnlessEqual(s.slaveVersion("svn", None), cver)
346 # and that non-existent commands do not provide a version
347 self.failUnlessEqual(s.slaveVersion("NOSUCHCOMMAND"), None)
348 # TODO: verify that a <=0.5.0 buildslave (which does not implement
349 # remote_getCommands) handles oldversion= properly. This requires a
350 # mutant slave which does not offer that method.
351 #self.failUnlessEqual(s.slaveVersion("NOSUCHCOMMAND", "old"), "old")
353 # now check the comparison functions
354 self.failIf(s.slaveVersionIsOlderThan("svn", cver))
355 self.failIf(s.slaveVersionIsOlderThan("svn", "1.1"))
356 self.failUnless(s.slaveVersionIsOlderThan("svn", cver + ".1"))
358 self.failUnlessEqual(s.getSlaveName(), "bot1")
360 def testCompare(self):
361 self.master._checker = self.checkCompare
362 d = self.doBuild("quick")
363 return d
366 class _SimpleBuildStep(buildstep.BuildStep):
367 def start(self):
368 args = {"arg1": "value"}
369 cmd = buildstep.RemoteCommand("simple", args)
370 d = self.runCommand(cmd)
371 d.addCallback(lambda res: self.finished(SUCCESS))
373 class _SimpleCommand(commands.Command):
374 def start(self):
375 self.builder.flag = True
376 self.builder.flag_args = self.args
377 return defer.succeed(None)
379 class CheckStepTester(StepTester, unittest.TestCase):
380 def testSimple(self):
381 self.slavebase = "testSimple.slave"
382 self.masterbase = "testSimple.master"
383 sb = self.makeSlaveBuilder()
384 sb.flag = False
385 registry.registerSlaveCommand("simple", _SimpleCommand, "1")
386 step = self.makeStep(_SimpleBuildStep)
387 d = self.runStep(step)
388 def _checkSimple(results):
389 self.failUnless(sb.flag)
390 self.failUnlessEqual(sb.flag_args, {"arg1": "value"})
391 d.addCallback(_checkSimple)
392 return d
394 class Python(StepTester, unittest.TestCase):
395 def testPyFlakes1(self):
396 self.masterbase = "Python.testPyFlakes1"
397 step = self.makeStep(python.PyFlakes)
398 output = \
399 """pyflakes buildbot
400 buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
401 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
402 buildbot/clients/debug.py:9: 'gnome' imported but unused
403 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
404 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
405 buildbot/scripts/imaginary.py:12: undefined name 'size'
406 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
408 log = step.addLog("stdio")
409 log.addStdout(output)
410 log.finish()
411 step.createSummary(log)
412 desc = step.descriptionDone
413 self.failUnless("unused=2" in desc)
414 self.failUnless("undefined=1" in desc)
415 self.failUnless("redefs=3" in desc)
416 self.failUnless("import*=1" in desc)
417 self.failIf("misc=" in desc)
419 self.failUnlessEqual(step.getProperty("pyflakes-unused"), 2)
420 self.failUnlessEqual(step.getProperty("pyflakes-undefined"), 1)
421 self.failUnlessEqual(step.getProperty("pyflakes-redefs"), 3)
422 self.failUnlessEqual(step.getProperty("pyflakes-import*"), 1)
423 self.failUnlessEqual(step.getProperty("pyflakes-misc"), 0)
424 self.failUnlessEqual(step.getProperty("pyflakes-total"), 7)
426 logs = {}
427 for log in step.step_status.getLogs():
428 logs[log.getName()] = log
430 for name in ["unused", "undefined", "redefs", "import*"]:
431 self.failUnless(name in logs)
432 self.failIf("misc" in logs)
433 lines = logs["unused"].readlines()
434 self.failUnlessEqual(len(lines), 2)
435 self.failUnlessEqual(lines[0], "buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused\n")
437 cmd = buildstep.RemoteCommand(None, {})
438 cmd.rc = 0
439 results = step.evaluateCommand(cmd)
440 self.failUnlessEqual(results, FAILURE) # because of the 'undefined'
442 def testPyFlakes2(self):
443 self.masterbase = "Python.testPyFlakes2"
444 step = self.makeStep(python.PyFlakes)
445 output = \
446 """pyflakes buildbot
447 some more text here that should be ignored
448 buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
449 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
450 buildbot/clients/debug.py:9: 'gnome' imported but unused
451 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
452 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
453 buildbot/scripts/imaginary.py:12: undefined name 'size'
454 could not compile 'blah/blah.py':3:
455 pretend there was an invalid line here
456 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
458 log = step.addLog("stdio")
459 log.addStdout(output)
460 log.finish()
461 step.createSummary(log)
462 desc = step.descriptionDone
463 self.failUnless("unused=2" in desc)
464 self.failUnless("undefined=1" in desc)
465 self.failUnless("redefs=3" in desc)
466 self.failUnless("import*=1" in desc)
467 self.failUnless("misc=2" in desc)
470 def testPyFlakes3(self):
471 self.masterbase = "Python.testPyFlakes3"
472 step = self.makeStep(python.PyFlakes)
473 output = \
474 """buildbot/changes/freshcvsmail.py:5: 'FCMaildirSource' imported but unused
475 buildbot/clients/debug.py:9: redefinition of unused 'gtk' from line 9
476 buildbot/clients/debug.py:9: 'gnome' imported but unused
477 buildbot/scripts/runner.py:323: redefinition of unused 'run' from line 321
478 buildbot/scripts/runner.py:325: redefinition of unused 'run' from line 323
479 buildbot/scripts/imaginary.py:12: undefined name 'size'
480 buildbot/scripts/imaginary.py:18: 'from buildbot import *' used; unable to detect undefined names
482 log = step.addLog("stdio")
483 log.addStdout(output)
484 log.finish()
485 step.createSummary(log)
486 desc = step.descriptionDone
487 self.failUnless("unused=2" in desc)
488 self.failUnless("undefined=1" in desc)
489 self.failUnless("redefs=3" in desc)
490 self.failUnless("import*=1" in desc)
491 self.failIf("misc" in desc)