(refs #557, #559) use tarfile to upload multiple files from the master
[buildbot.git] / buildbot / test / runutils.py
blob7c8cf6aaddfd226b8856e6203dd0ae7b497ce844
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(), 'test_builder')
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, 'test_builder')
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 setUpSignalHandler(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 tearDownSignalHandler(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 setUp(self):
343 self.setUpSignalHandler()
345 def tearDown(self):
346 self.tearDownSignalHandler()
348 def setUpBuilder(self, basedir):
349 if not os.path.exists(basedir):
350 os.mkdir(basedir)
351 self.builder = FakeSlaveBuilder(self.usePTY, basedir)
353 def startCommand(self, cmdclass, args):
354 stepId = 0
355 self.cmd = c = cmdclass(self.builder, stepId, args)
356 c.running = True
357 d = c.doStart()
358 return d
360 def collectUpdates(self, res=None):
361 logs = {}
362 for u in self.builder.updates:
363 for k in u.keys():
364 if k == "log":
365 logname,data = u[k]
366 oldlog = logs.get(("log",logname), "")
367 logs[("log",logname)] = oldlog + data
368 elif k == "rc":
369 pass
370 else:
371 logs[k] = logs.get(k, "") + u[k]
372 return logs
374 def findRC(self):
375 for u in self.builder.updates:
376 if "rc" in u:
377 return u["rc"]
378 return None
380 def printStderr(self):
381 for u in self.builder.updates:
382 if "stderr" in u:
383 print u["stderr"]
385 # ----------------------------------------
387 class LocalWrapper:
388 # r = pb.Referenceable()
389 # w = LocalWrapper(r)
390 # now you can do things like w.callRemote()
391 def __init__(self, target):
392 self.target = target
394 def callRemote(self, name, *args, **kwargs):
395 # callRemote is not allowed to fire its Deferred in the same turn
396 d = defer.Deferred()
397 d.addCallback(self._callRemote, *args, **kwargs)
398 reactor.callLater(0, d.callback, name)
399 return d
401 def _callRemote(self, name, *args, **kwargs):
402 method = getattr(self.target, "remote_"+name)
403 return method(*args, **kwargs)
405 def notifyOnDisconnect(self, observer):
406 pass
407 def dontNotifyOnDisconnect(self, observer):
408 pass
411 class LocalSlaveBuilder(bot.SlaveBuilder):
412 """I am object that behaves like a pb.RemoteReference, but in fact I
413 invoke methods locally."""
414 _arg_filter = None
416 def setArgFilter(self, filter):
417 self._arg_filter = filter
419 def remote_startCommand(self, stepref, stepId, command, args):
420 if self._arg_filter:
421 args = self._arg_filter(args)
422 # stepref should be a RemoteReference to the RemoteCommand
423 return bot.SlaveBuilder.remote_startCommand(self,
424 LocalWrapper(stepref),
425 stepId, command, args)
427 class StepTester:
428 """Utility class to exercise BuildSteps and RemoteCommands, without
429 really using a Build or a Bot. No networks are used.
431 Use this as follows::
433 class MyTest(StepTester, unittest.TestCase):
434 def testOne(self):
435 self.slavebase = 'testOne.slave'
436 self.masterbase = 'testOne.master'
437 sb = self.makeSlaveBuilder()
438 step = self.makeStep(stepclass, **kwargs)
439 d = self.runStep(step)
440 d.addCallback(_checkResults)
441 return d
444 #slavebase = "slavebase"
445 slavebuilderbase = "slavebuilderbase"
446 #masterbase = "masterbase"
448 def makeSlaveBuilder(self):
449 os.mkdir(self.slavebase)
450 os.mkdir(os.path.join(self.slavebase, self.slavebuilderbase))
451 b = bot.Bot(self.slavebase, False)
452 b.startService()
453 sb = LocalSlaveBuilder("slavebuildername", False)
454 sb.setArgFilter(self.filterArgs)
455 sb.usePTY = False
456 sb.setServiceParent(b)
457 sb.setBuilddir(self.slavebuilderbase)
458 self.remote = LocalWrapper(sb)
459 return sb
461 workdir = "build"
462 def makeStep(self, factory, **kwargs):
463 step = makeBuildStep(self.masterbase, factory, **kwargs)
464 step.setBuildSlave(BuildSlave("name", "password"))
465 step.setDefaultWorkdir(self.workdir)
466 return step
468 def runStep(self, step):
469 d = defer.maybeDeferred(step.startStep, self.remote)
470 return d
472 def wrap(self, target):
473 return LocalWrapper(target)
475 def filterArgs(self, args):
476 # this can be overridden
477 return args
479 # ----------------------------------------
481 _flags = {}
483 def setTestFlag(flagname, value):
484 _flags[flagname] = value
486 class SetTestFlagStep(BuildStep):
488 A special BuildStep to set a named flag; this can be used with the
489 TestFlagMixin to monitor what has and has not run in a particular
490 configuration.
492 def __init__(self, flagname='flag', value=1, **kwargs):
493 BuildStep.__init__(self, **kwargs)
494 self.addFactoryArguments(flagname=flagname, value=value)
496 self.flagname = flagname
497 self.value = value
499 def start(self):
500 properties = self.build.getProperties()
501 _flags[self.flagname] = properties.render(self.value)
502 self.finished(builder.SUCCESS)
504 class TestFlagMixin:
505 def clearFlags(self):
507 Set up for a test by clearing all flags; call this from your test
508 function.
510 _flags.clear()
512 def failIfFlagSet(self, flagname, msg=None):
513 if not msg: msg = "flag '%s' is set" % flagname
514 self.failIf(_flags.has_key(flagname), msg=msg)
516 def failIfFlagNotSet(self, flagname, msg=None):
517 if not msg: msg = "flag '%s' is not set" % flagname
518 self.failUnless(_flags.has_key(flagname), msg=msg)
520 def getFlag(self, flagname):
521 self.failIfFlagNotSet(flagname, "flag '%s' not set" % flagname)
522 return _flags.get(flagname)