OLD | NEW |
| (Empty) |
1 | |
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 | |
7 | |
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 | |
17 | |
18 | |
19 | |
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 | |
28 | |
29 def connectionMade(self): | |
30 if self.stdin is not None: | |
31 self.transport.write(self.stdin) | |
32 self.transport.closeStdin() | |
33 | |
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)) | |
43 | |
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 | |
52 | |
53 | |
54 class MyBot(bot.Bot): | |
55 def remote_getSlaveInfo(self): | |
56 return self.parent.info | |
57 | |
58 class MyBuildSlave(bot.BuildSlave): | |
59 botClass = MyBot | |
60 | |
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 | |
68 | |
69 class RunMixin: | |
70 master = None | |
71 | |
72 def rmtree(self, d): | |
73 rmtree(d) | |
74 | |
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) | |
82 | |
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() | |
93 | |
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 | |
106 | |
107 def connectSlave2(self): | |
108 # this takes over for bot1, so it has to share the slavename | |
109 port = self.master.slavePort._port.getHost().port | |
110 self.rmtree("slavebase-bot2") | |
111 os.mkdir("slavebase-bot2") | |
112 # this uses bot1, really | |
113 slave = MyBuildSlave("localhost", port, "bot1", "sekrit", | |
114 "slavebase-bot2", keepalive=0, usePTY=False) | |
115 slave.info = {"admin": "two"} | |
116 self.slaves['bot2'] = slave | |
117 slave.startService() | |
118 | |
119 def connectSlaveFastTimeout(self): | |
120 # this slave has a very fast keepalive timeout | |
121 port = self.master.slavePort._port.getHost().port | |
122 self.rmtree("slavebase-bot1") | |
123 os.mkdir("slavebase-bot1") | |
124 slave = MyBuildSlave("localhost", port, "bot1", "sekrit", | |
125 "slavebase-bot1", keepalive=2, usePTY=False, | |
126 keepaliveTimeout=1) | |
127 slave.info = {"admin": "one"} | |
128 self.slaves['bot1'] = slave | |
129 slave.startService() | |
130 d = self.master.botmaster.waitUntilBuilderAttached("dummy") | |
131 return d | |
132 | |
133 # things to start builds | |
134 def requestBuild(self, builder): | |
135 # returns a Deferred that fires with an IBuildStatus object when the | |
136 # build is finished | |
137 req = BuildRequest("forced build", SourceStamp(), 'test_builder') | |
138 self.control.getBuilder(builder).requestBuild(req) | |
139 return req.waitUntilFinished() | |
140 | |
141 def failUnlessBuildSucceeded(self, bs): | |
142 if bs.getResults() != builder.SUCCESS: | |
143 log.msg("failUnlessBuildSucceeded noticed that the build failed") | |
144 self.logBuildResults(bs) | |
145 self.failUnlessEqual(bs.getResults(), builder.SUCCESS) | |
146 return bs # useful for chaining | |
147 | |
148 def logBuildResults(self, bs): | |
149 # emit the build status and the contents of all logs to test.log | |
150 log.msg("logBuildResults starting") | |
151 log.msg(" bs.getResults() == %s" % builder.Results[bs.getResults()]) | |
152 log.msg(" bs.isFinished() == %s" % bs.isFinished()) | |
153 for s in bs.getSteps(): | |
154 for l in s.getLogs(): | |
155 log.msg("--- START step %s / log %s ---" % (s.getName(), | |
156 l.getName())) | |
157 if not l.getName().endswith(".html"): | |
158 log.msg(l.getTextWithHeaders()) | |
159 log.msg("--- STOP ---") | |
160 log.msg("logBuildResults finished") | |
161 | |
162 def tearDown(self): | |
163 log.msg("doing tearDown") | |
164 d = self.shutdownAllSlaves() | |
165 d.addCallback(self._tearDown_1) | |
166 d.addCallback(self._tearDown_2) | |
167 return d | |
168 def _tearDown_1(self, res): | |
169 if self.master: | |
170 return defer.maybeDeferred(self.master.stopService) | |
171 def _tearDown_2(self, res): | |
172 self.master = None | |
173 log.msg("tearDown done") | |
174 | |
175 | |
176 # various forms of slave death | |
177 | |
178 def shutdownAllSlaves(self): | |
179 # the slave has disconnected normally: they SIGINT'ed it, or it shut | |
180 # down willingly. This will kill child processes and give them a | |
181 # chance to finish up. We return a Deferred that will fire when | |
182 # everything is finished shutting down. | |
183 | |
184 log.msg("doing shutdownAllSlaves") | |
185 dl = [] | |
186 for slave in self.slaves.values(): | |
187 slave.stopService() | |
188 dl.append(slave.waitUntilDisconnected()) | |
189 d = defer.DeferredList(dl) | |
190 d.addCallback(self._shutdownAllSlavesDone) | |
191 return d | |
192 def _shutdownAllSlavesDone(self, res): | |
193 for name in self.slaves.keys(): | |
194 del self.slaves[name] | |
195 return self.master.botmaster.waitUntilBuilderFullyDetached("dummy") | |
196 | |
197 def shutdownSlave(self, slavename, buildername): | |
198 # this slave has disconnected normally: they SIGINT'ed it, or it shut | |
199 # down willingly. This will kill child processes and give them a | |
200 # chance to finish up. We return a Deferred that will fire when | |
201 # everything is finished shutting down, and the given Builder knows | |
202 # that the slave has gone away. | |
203 | |
204 s = self.slaves[slavename] | |
205 dl = [self.master.botmaster.waitUntilBuilderDetached(buildername), | |
206 s.waitUntilDisconnected()] | |
207 d = defer.DeferredList(dl) | |
208 d.addCallback(self._shutdownSlave_done, slavename) | |
209 s.stopService() | |
210 return d | |
211 def _shutdownSlave_done(self, res, slavename): | |
212 del self.slaves[slavename] | |
213 | |
214 def killSlave(self, slavename="bot1", buildername="dummy"): | |
215 # the slave has died, its host sent a FIN. The .notifyOnDisconnect | |
216 # callbacks will terminate the current step, so the build should be | |
217 # flunked (no further steps should be started). | |
218 self.slaves[slavename].bf.continueTrying = 0 | |
219 bot = self.slaves[slavename].getServiceNamed("bot") | |
220 broker = bot.builders[buildername].remote.broker | |
221 broker.transport.loseConnection() | |
222 del self.slaves[slavename] | |
223 | |
224 def disappearSlave(self, slavename="bot1", buildername="dummy", | |
225 allowReconnect=False): | |
226 # the slave's host has vanished off the net, leaving the connection | |
227 # dangling. This will be detected quickly by app-level keepalives or | |
228 # a ping, or slowly by TCP timeouts. | |
229 | |
230 # simulate this by replacing the slave Broker's .dataReceived method | |
231 # with one that just throws away all data. | |
232 def discard(data): | |
233 pass | |
234 bot = self.slaves[slavename].getServiceNamed("bot") | |
235 broker = bot.builders[buildername].remote.broker | |
236 broker.dataReceived = discard # seal its ears | |
237 broker.transport.write = discard # and take away its voice | |
238 if not allowReconnect: | |
239 # also discourage it from reconnecting once the connection goes away | |
240 assert self.slaves[slavename].bf.continueTrying | |
241 self.slaves[slavename].bf.continueTrying = False | |
242 | |
243 def ghostSlave(self): | |
244 # the slave thinks it has lost the connection, and initiated a | |
245 # reconnect. The master doesn't yet realize it has lost the previous | |
246 # connection, and sees two connections at once. | |
247 raise NotImplementedError | |
248 | |
249 | |
250 def setupBuildStepStatus(basedir): | |
251 """Return a BuildStep with a suitable BuildStepStatus object, ready to | |
252 use.""" | |
253 os.mkdir(basedir) | |
254 botmaster = None | |
255 s0 = builder.Status(botmaster, basedir) | |
256 s1 = s0.builderAdded("buildername", "buildername") | |
257 s2 = builder.BuildStatus(s1, 1) | |
258 s3 = builder.BuildStepStatus(s2) | |
259 s3.setName("foostep") | |
260 s3.started = True | |
261 s3.stepStarted() | |
262 return s3 | |
263 | |
264 def fake_slaveVersion(command, oldversion=None): | |
265 from buildbot.slave.registry import commandRegistry | |
266 return commandRegistry[command] | |
267 | |
268 class FakeBuildMaster: | |
269 properties = Properties(masterprop="master") | |
270 | |
271 class FakeBotMaster: | |
272 parent = FakeBuildMaster() | |
273 | |
274 def makeBuildStep(basedir, step_class=BuildStep, **kwargs): | |
275 bss = setupBuildStepStatus(basedir) | |
276 | |
277 ss = SourceStamp() | |
278 setup = {'name': "builder1", "slavename": "bot1", | |
279 'builddir': "builddir", 'slavebuilddir': "slavebuilddir", 'factory'
: None} | |
280 b0 = Builder(setup, bss.getBuild().getBuilder()) | |
281 b0.botmaster = FakeBotMaster() | |
282 br = BuildRequest("reason", ss, 'test_builder') | |
283 b = Build([br]) | |
284 b.setBuilder(b0) | |
285 s = step_class(**kwargs) | |
286 s.setBuild(b) | |
287 s.setStepStatus(bss) | |
288 b.build_status = bss.getBuild() | |
289 b.setupProperties() | |
290 s.slaveVersion = fake_slaveVersion | |
291 return s | |
292 | |
293 | |
294 def findDir(): | |
295 # the same directory that holds this script | |
296 return util.sibpath(__file__, ".") | |
297 | |
298 class SignalMixin: | |
299 sigchldHandler = None | |
300 | |
301 def setUpSignalHandler(self): | |
302 # make sure SIGCHLD handler is installed, as it should be on | |
303 # reactor.run(). problem is reactor may not have been run when this | |
304 # test runs. | |
305 if hasattr(reactor, "_handleSigchld") and hasattr(signal, "SIGCHLD"): | |
306 self.sigchldHandler = signal.signal(signal.SIGCHLD, | |
307 reactor._handleSigchld) | |
308 | |
309 def tearDownSignalHandler(self): | |
310 if self.sigchldHandler: | |
311 signal.signal(signal.SIGCHLD, self.sigchldHandler) | |
312 | |
313 # these classes are used to test SlaveCommands in isolation | |
314 | |
315 class FakeSlaveBuilder: | |
316 debug = False | |
317 def __init__(self, usePTY, basedir): | |
318 self.updates = [] | |
319 self.basedir = basedir | |
320 self.usePTY = usePTY | |
321 | |
322 def sendUpdate(self, data): | |
323 if self.debug: | |
324 print "FakeSlaveBuilder.sendUpdate", data | |
325 self.updates.append(data) | |
326 | |
327 | |
328 class SlaveCommandTestBase(SignalMixin): | |
329 usePTY = False | |
330 | |
331 def setUp(self): | |
332 self.setUpSignalHandler() | |
333 | |
334 def tearDown(self): | |
335 self.tearDownSignalHandler() | |
336 | |
337 def setUpBuilder(self, basedir): | |
338 if not os.path.exists(basedir): | |
339 os.mkdir(basedir) | |
340 self.builder = FakeSlaveBuilder(self.usePTY, basedir) | |
341 | |
342 def startCommand(self, cmdclass, args): | |
343 stepId = 0 | |
344 self.cmd = c = cmdclass(self.builder, stepId, args) | |
345 c.running = True | |
346 d = c.doStart() | |
347 return d | |
348 | |
349 def collectUpdates(self, res=None): | |
350 logs = {} | |
351 for u in self.builder.updates: | |
352 for k in u.keys(): | |
353 if k == "log": | |
354 logname,data = u[k] | |
355 oldlog = logs.get(("log",logname), "") | |
356 logs[("log",logname)] = oldlog + data | |
357 elif k == "rc": | |
358 pass | |
359 else: | |
360 logs[k] = logs.get(k, "") + u[k] | |
361 return logs | |
362 | |
363 def findRC(self): | |
364 for u in self.builder.updates: | |
365 if "rc" in u: | |
366 return u["rc"] | |
367 return None | |
368 | |
369 def printStderr(self): | |
370 for u in self.builder.updates: | |
371 if "stderr" in u: | |
372 print u["stderr"] | |
373 | |
374 # ---------------------------------------- | |
375 | |
376 class LocalWrapper: | |
377 # r = pb.Referenceable() | |
378 # w = LocalWrapper(r) | |
379 # now you can do things like w.callRemote() | |
380 def __init__(self, target): | |
381 self.target = target | |
382 | |
383 def callRemote(self, name, *args, **kwargs): | |
384 # callRemote is not allowed to fire its Deferred in the same turn | |
385 d = defer.Deferred() | |
386 d.addCallback(self._callRemote, *args, **kwargs) | |
387 reactor.callLater(0, d.callback, name) | |
388 return d | |
389 | |
390 def _callRemote(self, name, *args, **kwargs): | |
391 method = getattr(self.target, "remote_"+name) | |
392 return method(*args, **kwargs) | |
393 | |
394 def notifyOnDisconnect(self, observer): | |
395 pass | |
396 def dontNotifyOnDisconnect(self, observer): | |
397 pass | |
398 | |
399 | |
400 class LocalSlaveBuilder(bot.SlaveBuilder): | |
401 """I am object that behaves like a pb.RemoteReference, but in fact I | |
402 invoke methods locally.""" | |
403 _arg_filter = None | |
404 | |
405 def setArgFilter(self, filter): | |
406 self._arg_filter = filter | |
407 | |
408 def remote_startCommand(self, stepref, stepId, command, args): | |
409 if self._arg_filter: | |
410 args = self._arg_filter(args) | |
411 # stepref should be a RemoteReference to the RemoteCommand | |
412 return bot.SlaveBuilder.remote_startCommand(self, | |
413 LocalWrapper(stepref), | |
414 stepId, command, args) | |
415 | |
416 class StepTester: | |
417 """Utility class to exercise BuildSteps and RemoteCommands, without | |
418 really using a Build or a Bot. No networks are used. | |
419 | |
420 Use this as follows:: | |
421 | |
422 class MyTest(StepTester, unittest.TestCase): | |
423 def testOne(self): | |
424 self.slavebase = 'testOne.slave' | |
425 self.masterbase = 'testOne.master' | |
426 sb = self.makeSlaveBuilder() | |
427 step = self.makeStep(stepclass, **kwargs) | |
428 d = self.runStep(step) | |
429 d.addCallback(_checkResults) | |
430 return d | |
431 """ | |
432 | |
433 #slavebase = "slavebase" | |
434 slavebuilderbase = "slavebuilderbase" | |
435 #masterbase = "masterbase" | |
436 | |
437 def makeSlaveBuilder(self): | |
438 os.mkdir(self.slavebase) | |
439 os.mkdir(os.path.join(self.slavebase, self.slavebuilderbase)) | |
440 b = bot.Bot(self.slavebase, False) | |
441 b.startService() | |
442 sb = LocalSlaveBuilder("slavebuildername", False) | |
443 sb.setArgFilter(self.filterArgs) | |
444 sb.usePTY = False | |
445 sb.setServiceParent(b) | |
446 sb.setBuilddir(self.slavebuilderbase) | |
447 self.remote = LocalWrapper(sb) | |
448 return sb | |
449 | |
450 workdir = "build" | |
451 def makeStep(self, factory, **kwargs): | |
452 step = makeBuildStep(self.masterbase, factory, **kwargs) | |
453 step.setBuildSlave(BuildSlave("name", "password")) | |
454 step.setDefaultWorkdir(self.workdir) | |
455 return step | |
456 | |
457 def runStep(self, step): | |
458 d = defer.maybeDeferred(step.startStep, self.remote) | |
459 return d | |
460 | |
461 def wrap(self, target): | |
462 return LocalWrapper(target) | |
463 | |
464 def filterArgs(self, args): | |
465 # this can be overridden | |
466 return args | |
467 | |
468 # ---------------------------------------- | |
469 | |
470 _flags = {} | |
471 | |
472 def setTestFlag(flagname, value): | |
473 _flags[flagname] = value | |
474 | |
475 class SetTestFlagStep(BuildStep): | |
476 """ | |
477 A special BuildStep to set a named flag; this can be used with the | |
478 TestFlagMixin to monitor what has and has not run in a particular | |
479 configuration. | |
480 """ | |
481 def __init__(self, flagname='flag', value=1, **kwargs): | |
482 BuildStep.__init__(self, **kwargs) | |
483 self.addFactoryArguments(flagname=flagname, value=value) | |
484 | |
485 self.flagname = flagname | |
486 self.value = value | |
487 | |
488 def start(self): | |
489 properties = self.build.getProperties() | |
490 _flags[self.flagname] = properties.render(self.value) | |
491 self.finished(builder.SUCCESS) | |
492 | |
493 class TestFlagMixin: | |
494 def clearFlags(self): | |
495 """ | |
496 Set up for a test by clearing all flags; call this from your test | |
497 function. | |
498 """ | |
499 _flags.clear() | |
500 | |
501 def failIfFlagSet(self, flagname, msg=None): | |
502 if not msg: msg = "flag '%s' is set" % flagname | |
503 self.failIf(_flags.has_key(flagname), msg=msg) | |
504 | |
505 def failIfFlagNotSet(self, flagname, msg=None): | |
506 if not msg: msg = "flag '%s' is not set" % flagname | |
507 self.failUnless(_flags.has_key(flagname), msg=msg) | |
508 | |
509 def getFlag(self, flagname): | |
510 self.failIfFlagNotSet(flagname, "flag '%s' not set" % flagname) | |
511 return _flags.get(flagname) | |
OLD | NEW |