add max_builds= to BuildSlave, thanks to Dustin Mitchell. Closes #48.
[buildbot.git] / buildbot / test / test_slavecommand.py
blob64eb33cd257164e7426acd4c8e2e6b33bed7b937
1 # -*- test-case-name: buildbot.test.test_slavecommand -*-
3 from twisted.trial import unittest
4 from twisted.internet import reactor, interfaces
5 from twisted.python import runtime, failure, util
7 import os, sys
9 from buildbot.slave import commands
10 SlaveShellCommand = commands.SlaveShellCommand
12 from buildbot.test.runutils import SignalMixin, FakeSlaveBuilder
14 # test slavecommand.py by running the various commands with a fake
15 # SlaveBuilder object that logs the calls to sendUpdate()
17 class Utilities(unittest.TestCase):
18 def mkdir(self, basedir, path, mode=None):
19 fn = os.path.join(basedir, path)
20 os.makedirs(fn)
21 if mode is not None:
22 os.chmod(fn, mode)
24 def touch(self, basedir, path, mode=None):
25 fn = os.path.join(basedir, path)
26 f = open(fn, "w")
27 f.write("touch\n")
28 f.close()
29 if mode is not None:
30 os.chmod(fn, mode)
32 def test_rmdirRecursive(self):
33 basedir = "slavecommand/Utilities/test_rmdirRecursive"
34 os.makedirs(basedir)
35 d = os.path.join(basedir, "doomed")
36 self.mkdir(d, "a/b")
37 self.touch(d, "a/b/1.txt")
38 self.touch(d, "a/b/2.txt", 0444)
39 self.touch(d, "a/b/3.txt", 0)
40 self.mkdir(d, "a/c")
41 self.touch(d, "a/c/1.txt")
42 self.touch(d, "a/c/2.txt", 0444)
43 self.touch(d, "a/c/3.txt", 0)
44 os.chmod(os.path.join(d, "a/c"), 0444)
45 self.mkdir(d, "a/d")
46 self.touch(d, "a/d/1.txt")
47 self.touch(d, "a/d/2.txt", 0444)
48 self.touch(d, "a/d/3.txt", 0)
49 os.chmod(os.path.join(d, "a/d"), 0)
51 commands.rmdirRecursive(d)
52 self.failIf(os.path.exists(d))
55 class ShellBase(SignalMixin):
57 def setUp(self):
58 self.basedir = "test_slavecommand"
59 if not os.path.isdir(self.basedir):
60 os.mkdir(self.basedir)
61 self.subdir = os.path.join(self.basedir, "subdir")
62 if not os.path.isdir(self.subdir):
63 os.mkdir(self.subdir)
64 self.builder = FakeSlaveBuilder(self.usePTY, self.basedir)
65 self.emitcmd = util.sibpath(__file__, "emit.py")
66 self.subemitcmd = os.path.join(util.sibpath(__file__, "subdir"),
67 "emit.py")
68 self.sleepcmd = util.sibpath(__file__, "sleep.py")
70 def failUnlessIn(self, substring, string):
71 self.failUnless(string.find(substring) != -1,
72 "'%s' not in '%s'" % (substring, string))
74 def getfile(self, which):
75 got = ""
76 for r in self.builder.updates:
77 if r.has_key(which):
78 got += r[which]
79 return got
81 def checkOutput(self, expected):
82 """
83 @type expected: list of (streamname, contents) tuples
84 @param expected: the expected output
85 """
86 expected_linesep = os.linesep
87 if self.usePTY:
88 # PTYs change the line ending. I'm not sure why.
89 expected_linesep = "\r\n"
90 expected = [(stream, contents.replace("\n", expected_linesep, 1000))
91 for (stream, contents) in expected]
92 if self.usePTY:
93 # PTYs merge stdout+stderr into a single stream
94 expected = [('stdout', contents)
95 for (stream, contents) in expected]
96 # now merge everything into one string per stream
97 streams = {}
98 for (stream, contents) in expected:
99 streams[stream] = streams.get(stream, "") + contents
100 for (stream, contents) in streams.items():
101 got = self.getfile(stream)
102 self.assertEquals(got, contents)
104 def getrc(self):
105 self.failUnless(self.builder.updates[-1].has_key('rc'))
106 got = self.builder.updates[-1]['rc']
107 return got
108 def checkrc(self, expected):
109 got = self.getrc()
110 self.assertEquals(got, expected)
112 def testShell1(self):
113 targetfile = os.path.join(self.basedir, "log1.out")
114 if os.path.exists(targetfile):
115 os.unlink(targetfile)
116 cmd = "%s %s 0" % (sys.executable, self.emitcmd)
117 args = {'command': cmd, 'workdir': '.', 'timeout': 60}
118 c = SlaveShellCommand(self.builder, None, args)
119 d = c.start()
120 expected = [('stdout', "this is stdout\n"),
121 ('stderr', "this is stderr\n")]
122 d.addCallback(self._checkPass, expected, 0)
123 def _check_targetfile(res):
124 self.failUnless(os.path.exists(targetfile))
125 d.addCallback(_check_targetfile)
126 return d
128 def _checkPass(self, res, expected, rc):
129 self.checkOutput(expected)
130 self.checkrc(rc)
132 def testShell2(self):
133 cmd = [sys.executable, self.emitcmd, "0"]
134 args = {'command': cmd, 'workdir': '.', 'timeout': 60}
135 c = SlaveShellCommand(self.builder, None, args)
136 d = c.start()
137 expected = [('stdout', "this is stdout\n"),
138 ('stderr', "this is stderr\n")]
139 d.addCallback(self._checkPass, expected, 0)
140 return d
142 def testShellRC(self):
143 cmd = [sys.executable, self.emitcmd, "1"]
144 args = {'command': cmd, 'workdir': '.', 'timeout': 60}
145 c = SlaveShellCommand(self.builder, None, args)
146 d = c.start()
147 expected = [('stdout', "this is stdout\n"),
148 ('stderr', "this is stderr\n")]
149 d.addCallback(self._checkPass, expected, 1)
150 return d
152 def testShellEnv(self):
153 cmd = "%s %s 0" % (sys.executable, self.emitcmd)
154 args = {'command': cmd, 'workdir': '.',
155 'env': {'EMIT_TEST': "envtest"}, 'timeout': 60}
156 c = SlaveShellCommand(self.builder, None, args)
157 d = c.start()
158 expected = [('stdout', "this is stdout\n"),
159 ('stderr', "this is stderr\n"),
160 ('stdout', "EMIT_TEST: envtest\n"),
162 d.addCallback(self._checkPass, expected, 0)
163 return d
165 def testShellSubdir(self):
166 targetfile = os.path.join(self.basedir, "subdir", "log1.out")
167 if os.path.exists(targetfile):
168 os.unlink(targetfile)
169 cmd = "%s %s 0" % (sys.executable, self.subemitcmd)
170 args = {'command': cmd, 'workdir': "subdir", 'timeout': 60}
171 c = SlaveShellCommand(self.builder, None, args)
172 d = c.start()
173 expected = [('stdout', "this is stdout in subdir\n"),
174 ('stderr', "this is stderr\n")]
175 d.addCallback(self._checkPass, expected, 0)
176 def _check_targetfile(res):
177 self.failUnless(os.path.exists(targetfile))
178 d.addCallback(_check_targetfile)
179 return d
181 def testShellMissingCommand(self):
182 args = {'command': "/bin/EndWorldHungerAndMakePigsFly",
183 'workdir': '.', 'timeout': 10,
184 'env': {"LC_ALL": "C"},
186 c = SlaveShellCommand(self.builder, None, args)
187 d = c.start()
188 d.addCallback(self._testShellMissingCommand_1)
189 return d
190 def _testShellMissingCommand_1(self, res):
191 self.failIfEqual(self.getrc(), 0)
192 # we used to check the error message to make sure it said something
193 # about a missing command, but there are a variety of shells out
194 # there, and they emit message sin a variety of languages, so we
195 # stopped trying.
197 def testTimeout(self):
198 args = {'command': [sys.executable, self.sleepcmd, "10"],
199 'workdir': '.', 'timeout': 2}
200 c = SlaveShellCommand(self.builder, None, args)
201 d = c.start()
202 d.addCallback(self._testTimeout_1)
203 return d
204 def _testTimeout_1(self, res):
205 self.failIfEqual(self.getrc(), 0)
206 got = self.getfile('header')
207 self.failUnlessIn("command timed out: 2 seconds without output", got)
208 if runtime.platformType == "posix":
209 # the "killing pid" message is not present in windows
210 self.failUnlessIn("killing pid", got)
211 # but the process *ought* to be killed somehow
212 self.failUnlessIn("process killed by signal", got)
213 #print got
214 if runtime.platformType != 'posix':
215 testTimeout.todo = "timeout doesn't appear to work under windows"
217 def testInterrupt1(self):
218 args = {'command': [sys.executable, self.sleepcmd, "10"],
219 'workdir': '.', 'timeout': 20}
220 c = SlaveShellCommand(self.builder, None, args)
221 d = c.start()
222 reactor.callLater(1, c.interrupt)
223 d.addCallback(self._testInterrupt1_1)
224 return d
225 def _testInterrupt1_1(self, res):
226 self.failIfEqual(self.getrc(), 0)
227 got = self.getfile('header')
228 self.failUnlessIn("command interrupted", got)
229 if runtime.platformType == "posix":
230 self.failUnlessIn("process killed by signal", got)
231 if runtime.platformType != 'posix':
232 testInterrupt1.todo = "interrupt doesn't appear to work under windows"
235 # todo: twisted-specific command tests
237 class Shell(ShellBase, unittest.TestCase):
238 usePTY = False
240 def testInterrupt2(self):
241 # test the backup timeout. This doesn't work under a PTY, because the
242 # transport.loseConnection we do in the timeout handler actually
243 # *does* kill the process.
244 args = {'command': [sys.executable, self.sleepcmd, "5"],
245 'workdir': '.', 'timeout': 20}
246 c = SlaveShellCommand(self.builder, None, args)
247 d = c.start()
248 c.command.BACKUP_TIMEOUT = 1
249 # make it unable to kill the child, by changing the signal it uses
250 # from SIGKILL to the do-nothing signal 0.
251 c.command.KILL = None
252 reactor.callLater(1, c.interrupt)
253 d.addBoth(self._testInterrupt2_1)
254 return d
255 def _testInterrupt2_1(self, res):
256 # the slave should raise a TimeoutError exception. In a normal build
257 # process (i.e. one that uses step.RemoteShellCommand), this
258 # exception will be handed to the Step, which will acquire an ERROR
259 # status. In our test environment, it isn't such a big deal.
260 self.failUnless(isinstance(res, failure.Failure),
261 "res is not a Failure: %s" % (res,))
262 self.failUnless(res.check(commands.TimeoutError))
263 self.checkrc(-1)
264 return
265 # the command is still actually running. Start another command, to
266 # make sure that a) the old command's output doesn't interfere with
267 # the new one, and b) the old command's actual termination doesn't
268 # break anything
269 args = {'command': [sys.executable, self.sleepcmd, "5"],
270 'workdir': '.', 'timeout': 20}
271 c = SlaveShellCommand(self.builder, None, args)
272 d = c.start()
273 d.addCallback(self._testInterrupt2_2)
274 return d
275 def _testInterrupt2_2(self, res):
276 self.checkrc(0)
277 # N.B.: under windows, the trial process hangs out for another few
278 # seconds. I assume that the win32eventreactor is waiting for one of
279 # the lingering child processes to really finish.
281 haveProcess = interfaces.IReactorProcess(reactor, None)
282 if runtime.platformType == 'posix':
283 # test with PTYs also
284 class ShellPTY(ShellBase, unittest.TestCase):
285 usePTY = True
286 if not haveProcess:
287 ShellPTY.skip = "this reactor doesn't support IReactorProcess"
288 if not haveProcess:
289 Shell.skip = "this reactor doesn't support IReactorProcess"