Allow users to specify the remote git branch.
[buildbot.git] / buildbot / test / runutils.py
blob377537df56d221436cbbf73c467c221667a4cd38
2 import signal
3 import shutil, os, errno
4 from cStringIO import StringIO
5 from twisted.internet import defer, reactor, protocol
6 from twisted.python import log, util
8 from buildbot import master, interfaces
9 from buildbot.slave import bot
10 from buildbot.buildslave import BuildSlave
11 from buildbot.process.builder import Builder
12 from buildbot.process.base import BuildRequest, Build
13 from buildbot.process.buildstep import BuildStep
14 from buildbot.sourcestamp import SourceStamp
15 from buildbot.status import builder
16 from buildbot.process.properties import Properties
20 class _PutEverythingGetter(protocol.ProcessProtocol):
21 def __init__(self, deferred, stdin):
22 self.deferred = deferred
23 self.outBuf = StringIO()
24 self.errBuf = StringIO()
25 self.outReceived = self.outBuf.write
26 self.errReceived = self.errBuf.write
27 self.stdin = stdin
29 def connectionMade(self):
30 if self.stdin is not None:
31 self.transport.write(self.stdin)
32 self.transport.closeStdin()
34 def processEnded(self, reason):
35 out = self.outBuf.getvalue()
36 err = self.errBuf.getvalue()
37 e = reason.value
38 code = e.exitCode
39 if e.signal:
40 self.deferred.errback((out, err, e.signal))
41 else:
42 self.deferred.callback((out, err, code))
44 def myGetProcessOutputAndValue(executable, args=(), env={}, path='.',
45 _reactor_ignored=None, stdin=None):
46 """Like twisted.internet.utils.getProcessOutputAndValue but takes
47 stdin, too."""
48 d = defer.Deferred()
49 p = _PutEverythingGetter(d, stdin)
50 reactor.spawnProcess(p, executable, (executable,)+tuple(args), env, path)
51 return d
54 class MyBot(bot.Bot):
55 def remote_getSlaveInfo(self):
56 return self.parent.info
58 class MyBuildSlave(bot.BuildSlave):
59 botClass = MyBot
61 def rmtree(d):
62 try:
63 shutil.rmtree(d, ignore_errors=1)
64 except OSError, e:
65 # stupid 2.2 appears to ignore ignore_errors
66 if e.errno != errno.ENOENT:
67 raise
69 class RunMixin:
70 master = None
72 def rmtree(self, d):
73 rmtree(d)
75 def setUp(self):
76 self.slaves = {}
77 self.rmtree("basedir")
78 os.mkdir("basedir")
79 self.master = master.BuildMaster("basedir")
80 self.status = self.master.getStatus()
81 self.control = interfaces.IControl(self.master)
83 def connectOneSlave(self, slavename, opts={}):
84 port = self.master.slavePort._port.getHost().port
85 self.rmtree("slavebase-%s" % slavename)
86 os.mkdir("slavebase-%s" % slavename)
87 slave = MyBuildSlave("localhost", port, slavename, "sekrit",
88 "slavebase-%s" % slavename,
89 keepalive=0, usePTY=False, debugOpts=opts)
90 slave.info = {"admin": "one"}
91 self.slaves[slavename] = slave
92 slave.startService()
94 def connectSlave(self, builders=["dummy"], slavename="bot1",
95 opts={}):
96 # connect buildslave 'slavename' and wait for it to connect to all of
97 # the given builders
98 dl = []
99 # initiate call for all of them, before waiting on result,
100 # otherwise we might miss some
101 for b in builders:
102 dl.append(self.master.botmaster.waitUntilBuilderAttached(b))
103 d = defer.DeferredList(dl)
104 self.connectOneSlave(slavename, opts)
105 return d
107 def connectSlaves(self, slavenames, builders):
108 dl = []
109 # initiate call for all of them, before waiting on result,
110 # otherwise we might miss some
111 for b in builders:
112 dl.append(self.master.botmaster.waitUntilBuilderAttached(b))
113 d = defer.DeferredList(dl)
114 for name in slavenames:
115 self.connectOneSlave(name)
116 return d
118 def connectSlave2(self):
119 # this takes over for bot1, so it has to share the slavename
120 port = self.master.slavePort._port.getHost().port
121 self.rmtree("slavebase-bot2")
122 os.mkdir("slavebase-bot2")
123 # this uses bot1, really
124 slave = MyBuildSlave("localhost", port, "bot1", "sekrit",
125 "slavebase-bot2", keepalive=0, usePTY=False)
126 slave.info = {"admin": "two"}
127 self.slaves['bot2'] = slave
128 slave.startService()
130 def connectSlaveFastTimeout(self):
131 # this slave has a very fast keepalive timeout
132 port = self.master.slavePort._port.getHost().port
133 self.rmtree("slavebase-bot1")
134 os.mkdir("slavebase-bot1")
135 slave = MyBuildSlave("localhost", port, "bot1", "sekrit",
136 "slavebase-bot1", keepalive=2, usePTY=False,
137 keepaliveTimeout=1)
138 slave.info = {"admin": "one"}
139 self.slaves['bot1'] = slave
140 slave.startService()
141 d = self.master.botmaster.waitUntilBuilderAttached("dummy")
142 return d
144 # things to start builds
145 def requestBuild(self, builder):
146 # returns a Deferred that fires with an IBuildStatus object when the
147 # build is finished
148 req = BuildRequest("forced build", SourceStamp())
149 self.control.getBuilder(builder).requestBuild(req)
150 return req.waitUntilFinished()
152 def failUnlessBuildSucceeded(self, bs):
153 if bs.getResults() != builder.SUCCESS:
154 log.msg("failUnlessBuildSucceeded noticed that the build failed")
155 self.logBuildResults(bs)
156 self.failUnlessEqual(bs.getResults(), builder.SUCCESS)
157 return bs # useful for chaining
159 def logBuildResults(self, bs):
160 # emit the build status and the contents of all logs to test.log
161 log.msg("logBuildResults starting")
162 log.msg(" bs.getResults() == %s" % builder.Results[bs.getResults()])
163 log.msg(" bs.isFinished() == %s" % bs.isFinished())
164 for s in bs.getSteps():
165 for l in s.getLogs():
166 log.msg("--- START step %s / log %s ---" % (s.getName(),
167 l.getName()))
168 if not l.getName().endswith(".html"):
169 log.msg(l.getTextWithHeaders())
170 log.msg("--- STOP ---")
171 log.msg("logBuildResults finished")
173 def tearDown(self):
174 log.msg("doing tearDown")
175 d = self.shutdownAllSlaves()
176 d.addCallback(self._tearDown_1)
177 d.addCallback(self._tearDown_2)
178 return d
179 def _tearDown_1(self, res):
180 if self.master:
181 return defer.maybeDeferred(self.master.stopService)
182 def _tearDown_2(self, res):
183 self.master = None
184 log.msg("tearDown done")
187 # various forms of slave death
189 def shutdownAllSlaves(self):
190 # the slave has disconnected normally: they SIGINT'ed it, or it shut
191 # down willingly. This will kill child processes and give them a
192 # chance to finish up. We return a Deferred that will fire when
193 # everything is finished shutting down.
195 log.msg("doing shutdownAllSlaves")
196 dl = []
197 for slave in self.slaves.values():
198 dl.append(slave.waitUntilDisconnected())
199 dl.append(defer.maybeDeferred(slave.stopService))
200 d = defer.DeferredList(dl)
201 d.addCallback(self._shutdownAllSlavesDone)
202 return d
203 def _shutdownAllSlavesDone(self, res):
204 for name in self.slaves.keys():
205 del self.slaves[name]
206 return self.master.botmaster.waitUntilBuilderFullyDetached("dummy")
208 def shutdownSlave(self, slavename, buildername):
209 # this slave has disconnected normally: they SIGINT'ed it, or it shut
210 # down willingly. This will kill child processes and give them a
211 # chance to finish up. We return a Deferred that will fire when
212 # everything is finished shutting down, and the given Builder knows
213 # that the slave has gone away.
215 s = self.slaves[slavename]
216 dl = [self.master.botmaster.waitUntilBuilderDetached(buildername),
217 s.waitUntilDisconnected()]
218 d = defer.DeferredList(dl)
219 d.addCallback(self._shutdownSlave_done, slavename)
220 s.stopService()
221 return d
222 def _shutdownSlave_done(self, res, slavename):
223 del self.slaves[slavename]
225 def killSlave(self):
226 # the slave has died, its host sent a FIN. The .notifyOnDisconnect
227 # callbacks will terminate the current step, so the build should be
228 # flunked (no further steps should be started).
229 self.slaves['bot1'].bf.continueTrying = 0
230 bot = self.slaves['bot1'].getServiceNamed("bot")
231 broker = bot.builders["dummy"].remote.broker
232 broker.transport.loseConnection()
233 del self.slaves['bot1']
235 def disappearSlave(self, slavename="bot1", buildername="dummy",
236 allowReconnect=False):
237 # the slave's host has vanished off the net, leaving the connection
238 # dangling. This will be detected quickly by app-level keepalives or
239 # a ping, or slowly by TCP timeouts.
241 # simulate this by replacing the slave Broker's .dataReceived method
242 # with one that just throws away all data.
243 def discard(data):
244 pass
245 bot = self.slaves[slavename].getServiceNamed("bot")
246 broker = bot.builders[buildername].remote.broker
247 broker.dataReceived = discard # seal its ears
248 broker.transport.write = discard # and take away its voice
249 if not allowReconnect:
250 # also discourage it from reconnecting once the connection goes away
251 assert self.slaves[slavename].bf.continueTrying
252 self.slaves[slavename].bf.continueTrying = False
254 def ghostSlave(self):
255 # the slave thinks it has lost the connection, and initiated a
256 # reconnect. The master doesn't yet realize it has lost the previous
257 # connection, and sees two connections at once.
258 raise NotImplementedError
261 def setupBuildStepStatus(basedir):
262 """Return a BuildStep with a suitable BuildStepStatus object, ready to
263 use."""
264 os.mkdir(basedir)
265 botmaster = None
266 s0 = builder.Status(botmaster, basedir)
267 s1 = s0.builderAdded("buildername", "buildername")
268 s2 = builder.BuildStatus(s1, 1)
269 s3 = builder.BuildStepStatus(s2)
270 s3.setName("foostep")
271 s3.started = True
272 s3.stepStarted()
273 return s3
275 def fake_slaveVersion(command, oldversion=None):
276 from buildbot.slave.registry import commandRegistry
277 return commandRegistry[command]
279 class FakeBuildMaster:
280 properties = Properties(masterprop="master")
282 class FakeBotMaster:
283 parent = FakeBuildMaster()
285 def makeBuildStep(basedir, step_class=BuildStep, **kwargs):
286 bss = setupBuildStepStatus(basedir)
288 ss = SourceStamp()
289 setup = {'name': "builder1", "slavename": "bot1",
290 'builddir': "builddir", 'factory': None}
291 b0 = Builder(setup, bss.getBuild().getBuilder())
292 b0.botmaster = FakeBotMaster()
293 br = BuildRequest("reason", ss)
294 b = Build([br])
295 b.setBuilder(b0)
296 s = step_class(**kwargs)
297 s.setBuild(b)
298 s.setStepStatus(bss)
299 b.build_status = bss.getBuild()
300 b.setupProperties()
301 s.slaveVersion = fake_slaveVersion
302 return s
305 def findDir():
306 # the same directory that holds this script
307 return util.sibpath(__file__, ".")
309 class SignalMixin:
310 sigchldHandler = None
312 def setUpClass(self):
313 # make sure SIGCHLD handler is installed, as it should be on
314 # reactor.run(). problem is reactor may not have been run when this
315 # test runs.
316 if hasattr(reactor, "_handleSigchld") and hasattr(signal, "SIGCHLD"):
317 self.sigchldHandler = signal.signal(signal.SIGCHLD,
318 reactor._handleSigchld)
320 def tearDownClass(self):
321 if self.sigchldHandler:
322 signal.signal(signal.SIGCHLD, self.sigchldHandler)
324 # these classes are used to test SlaveCommands in isolation
326 class FakeSlaveBuilder:
327 debug = False
328 def __init__(self, usePTY, basedir):
329 self.updates = []
330 self.basedir = basedir
331 self.usePTY = usePTY
333 def sendUpdate(self, data):
334 if self.debug:
335 print "FakeSlaveBuilder.sendUpdate", data
336 self.updates.append(data)
339 class SlaveCommandTestBase(SignalMixin):
340 usePTY = False
342 def setUpBuilder(self, basedir):
343 if not os.path.exists(basedir):
344 os.mkdir(basedir)
345 self.builder = FakeSlaveBuilder(self.usePTY, basedir)
347 def startCommand(self, cmdclass, args):
348 stepId = 0
349 self.cmd = c = cmdclass(self.builder, stepId, args)
350 c.running = True
351 d = c.doStart()
352 return d
354 def collectUpdates(self, res=None):
355 logs = {}
356 for u in self.builder.updates:
357 for k in u.keys():
358 if k == "log":
359 logname,data = u[k]
360 oldlog = logs.get(("log",logname), "")
361 logs[("log",logname)] = oldlog + data
362 elif k == "rc":
363 pass
364 else:
365 logs[k] = logs.get(k, "") + u[k]
366 return logs
368 def findRC(self):
369 for u in self.builder.updates:
370 if "rc" in u:
371 return u["rc"]
372 return None
374 def printStderr(self):
375 for u in self.builder.updates:
376 if "stderr" in u:
377 print u["stderr"]
379 # ----------------------------------------
381 class LocalWrapper:
382 # r = pb.Referenceable()
383 # w = LocalWrapper(r)
384 # now you can do things like w.callRemote()
385 def __init__(self, target):
386 self.target = target
388 def callRemote(self, name, *args, **kwargs):
389 # callRemote is not allowed to fire its Deferred in the same turn
390 d = defer.Deferred()
391 d.addCallback(self._callRemote, *args, **kwargs)
392 reactor.callLater(0, d.callback, name)
393 return d
395 def _callRemote(self, name, *args, **kwargs):
396 method = getattr(self.target, "remote_"+name)
397 return method(*args, **kwargs)
399 def notifyOnDisconnect(self, observer):
400 pass
401 def dontNotifyOnDisconnect(self, observer):
402 pass
405 class LocalSlaveBuilder(bot.SlaveBuilder):
406 """I am object that behaves like a pb.RemoteReference, but in fact I
407 invoke methods locally."""
408 _arg_filter = None
410 def setArgFilter(self, filter):
411 self._arg_filter = filter
413 def remote_startCommand(self, stepref, stepId, command, args):
414 if self._arg_filter:
415 args = self._arg_filter(args)
416 # stepref should be a RemoteReference to the RemoteCommand
417 return bot.SlaveBuilder.remote_startCommand(self,
418 LocalWrapper(stepref),
419 stepId, command, args)
421 class StepTester:
422 """Utility class to exercise BuildSteps and RemoteCommands, without
423 really using a Build or a Bot. No networks are used.
425 Use this as follows::
427 class MyTest(StepTester, unittest.TestCase):
428 def testOne(self):
429 self.slavebase = 'testOne.slave'
430 self.masterbase = 'testOne.master'
431 sb = self.makeSlaveBuilder()
432 step = self.makeStep(stepclass, **kwargs)
433 d = self.runStep(step)
434 d.addCallback(_checkResults)
435 return d
438 #slavebase = "slavebase"
439 slavebuilderbase = "slavebuilderbase"
440 #masterbase = "masterbase"
442 def makeSlaveBuilder(self):
443 os.mkdir(self.slavebase)
444 os.mkdir(os.path.join(self.slavebase, self.slavebuilderbase))
445 b = bot.Bot(self.slavebase, False)
446 b.startService()
447 sb = LocalSlaveBuilder("slavebuildername", False)
448 sb.setArgFilter(self.filterArgs)
449 sb.usePTY = False
450 sb.setServiceParent(b)
451 sb.setBuilddir(self.slavebuilderbase)
452 self.remote = LocalWrapper(sb)
453 return sb
455 workdir = "build"
456 def makeStep(self, factory, **kwargs):
457 step = makeBuildStep(self.masterbase, factory, **kwargs)
458 step.setBuildSlave(BuildSlave("name", "password"))
459 step.setDefaultWorkdir(self.workdir)
460 return step
462 def runStep(self, step):
463 d = defer.maybeDeferred(step.startStep, self.remote)
464 return d
466 def wrap(self, target):
467 return LocalWrapper(target)
469 def filterArgs(self, args):
470 # this can be overridden
471 return args
473 # ----------------------------------------
475 _flags = {}
477 def setTestFlag(flagname, value):
478 _flags[flagname] = value
480 class SetTestFlagStep(BuildStep):
482 A special BuildStep to set a named flag; this can be used with the
483 TestFlagMixin to monitor what has and has not run in a particular
484 configuration.
486 def __init__(self, flagname='flag', value=1, **kwargs):
487 BuildStep.__init__(self, **kwargs)
488 self.addFactoryArguments(flagname=flagname, value=value)
490 self.flagname = flagname
491 self.value = value
493 def start(self):
494 properties = self.build.getProperties()
495 _flags[self.flagname] = properties.render(self.value)
496 self.finished(builder.SUCCESS)
498 class TestFlagMixin:
499 def clearFlags(self):
501 Set up for a test by clearing all flags; call this from your test
502 function.
504 _flags.clear()
506 def failIfFlagSet(self, flagname, msg=None):
507 if not msg: msg = "flag '%s' is set" % flagname
508 self.failIf(_flags.has_key(flagname), msg=msg)
510 def failIfFlagNotSet(self, flagname, msg=None):
511 if not msg: msg = "flag '%s' is not set" % flagname
512 self.failUnless(_flags.has_key(flagname), msg=msg)
514 def getFlag(self, flagname):
515 self.failIfFlagNotSet(flagname, "flag '%s' not set" % flagname)
516 return _flags.get(flagname)