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 util, runtime, failure
6
from buildbot.twcompat import maybeWait
10
from twisted.python.log import startLogging
12
startLogging(sys.stdout)
17
from buildbot.slave import commands
18
SlaveShellCommand = commands.SlaveShellCommand
20
# test slavecommand.py by running the various commands with a fake
21
# SlaveBuilder object that logs the calls to sendUpdate()
24
# the same directory that holds this script
25
return util.sibpath(__file__, ".")
27
class FakeSlaveBuilder:
28
def __init__(self, usePTY):
30
self.basedir = findDir()
33
def sendUpdate(self, data):
34
if noisy: print "FakeSlaveBuilder.sendUpdate", data
35
self.updates.append(data)
42
# make sure SIGCHLD handler is installed, as it should be on
43
# reactor.run(). problem is reactor may not have been run when this
45
if hasattr(reactor, "_handleSigchld") and hasattr(signal, "SIGCHLD"):
46
self.sigchldHandler = signal.signal(signal.SIGCHLD,
47
reactor._handleSigchld)
49
def tearDownClass(self):
50
if self.sigchldHandler:
51
signal.signal(signal.SIGCHLD, self.sigchldHandler)
54
class ShellBase(SignalMixin):
57
self.builder = FakeSlaveBuilder(self.usePTY)
59
def failUnlessIn(self, substring, string):
60
self.failUnless(string.find(substring) != -1)
62
def getfile(self, which):
64
for r in self.builder.updates:
69
def checkOutput(self, expected):
71
@type expected: list of (streamname, contents) tuples
72
@param expected: the expected output
74
expected_linesep = os.linesep
76
# PTYs change the line ending. I'm not sure why.
77
expected_linesep = "\r\n"
78
expected = [(stream, contents.replace("\n", expected_linesep, 1000))
79
for (stream, contents) in expected]
81
# PTYs merge stdout+stderr into a single stream
82
expected = [('stdout', contents)
83
for (stream, contents) in expected]
84
# now merge everything into one string per stream
86
for (stream, contents) in expected:
87
streams[stream] = streams.get(stream, "") + contents
88
for (stream, contents) in streams.items():
89
got = self.getfile(stream)
90
self.assertEquals(got, contents)
93
self.failUnless(self.builder.updates[-1].has_key('rc'))
94
got = self.builder.updates[-1]['rc']
96
def checkrc(self, expected):
98
self.assertEquals(got, expected)
100
def testShell1(self):
101
cmd = sys.executable + " emit.py 0"
102
args = {'command': cmd, 'workdir': '.', 'timeout': 60}
103
c = SlaveShellCommand(self.builder, None, args)
105
expected = [('stdout', "this is stdout\n"),
106
('stderr', "this is stderr\n")]
107
d.addCallback(self._checkPass, expected, 0)
110
def _checkPass(self, res, expected, rc):
111
self.checkOutput(expected)
114
def testShell2(self):
115
cmd = [sys.executable, "emit.py", "0"]
116
args = {'command': cmd, 'workdir': '.', 'timeout': 60}
117
c = SlaveShellCommand(self.builder, None, args)
119
expected = [('stdout', "this is stdout\n"),
120
('stderr', "this is stderr\n")]
121
d.addCallback(self._checkPass, expected, 0)
124
def testShellRC(self):
125
cmd = [sys.executable, "emit.py", "1"]
126
args = {'command': cmd, 'workdir': '.', 'timeout': 60}
127
c = SlaveShellCommand(self.builder, None, args)
129
expected = [('stdout', "this is stdout\n"),
130
('stderr', "this is stderr\n")]
131
d.addCallback(self._checkPass, expected, 1)
134
def testShellEnv(self):
135
cmd = sys.executable + " emit.py 0"
136
args = {'command': cmd, 'workdir': '.',
137
'env': {'EMIT_TEST': "envtest"}, 'timeout': 60}
138
c = SlaveShellCommand(self.builder, None, args)
140
expected = [('stdout', "this is stdout\n"),
141
('stderr', "this is stderr\n"),
142
('stdout', "EMIT_TEST: envtest\n"),
144
d.addCallback(self._checkPass, expected, 0)
147
def testShellSubdir(self):
148
cmd = sys.executable + " emit.py 0"
149
args = {'command': cmd, 'workdir': "subdir", 'timeout': 60}
150
c = SlaveShellCommand(self.builder, None, args)
152
expected = [('stdout', "this is stdout in subdir\n"),
153
('stderr', "this is stderr\n")]
154
d.addCallback(self._checkPass, expected, 0)
157
def testShellMissingCommand(self):
158
args = {'command': "/bin/EndWorldHungerAndMakePigsFly",
159
'workdir': '.', 'timeout': 10}
160
c = SlaveShellCommand(self.builder, None, args)
162
d.addCallback(self._testShellMissingCommand_1)
164
def _testShellMissingCommand_1(self, res):
165
self.failIfEqual(self.getrc(), 0)
166
got = self.getfile('stdout') + self.getfile('stderr')
167
self.failUnless(re.search(r'no such file', got, re.I) # unix
168
or re.search(r'cannot find the path specified',
170
or re.search(r'is not recognized as',
171
got, re.I), # other win32
172
"bogus command didn't create the expected error "
173
"message, got '%s'" % got
176
def testTimeout(self):
177
args = {'command': [sys.executable, "sleep.py", "10"],
178
'workdir': '.', 'timeout': 2}
179
c = SlaveShellCommand(self.builder, None, args)
181
d.addCallback(self._testTimeout_1)
183
def _testTimeout_1(self, res):
184
self.failIfEqual(self.getrc(), 0)
185
got = self.getfile('header')
186
self.failUnlessIn("command timed out: 2 seconds without output", got)
187
if runtime.platformType == "posix":
188
# the "killing pid" message is not present in windows
189
self.failUnlessIn("killing pid", got)
190
# but the process *ought* to be killed somehow
191
self.failUnlessIn("process killed by signal", got)
193
if runtime.platformType != 'posix':
194
testTimeout.todo = "timeout doesn't appear to work under windows"
196
def testInterrupt1(self):
197
args = {'command': [sys.executable, "sleep.py", "10"],
198
'workdir': '.', 'timeout': 20}
199
c = SlaveShellCommand(self.builder, None, args)
201
reactor.callLater(1, c.interrupt)
202
d.addCallback(self._testInterrupt1_1)
204
def _testInterrupt1_1(self, res):
205
self.failIfEqual(self.getrc(), 0)
206
got = self.getfile('header')
207
self.failUnlessIn("command interrupted", got)
208
if runtime.platformType == "posix":
209
self.failUnlessIn("process killed by signal", got)
212
# todo: twisted-specific command tests
214
class Shell(ShellBase, unittest.TestCase):
217
def testInterrupt2(self):
218
# test the backup timeout. This doesn't work under a PTY, because the
219
# transport.loseConnection we do in the timeout handler actually
220
# *does* kill the process.
221
args = {'command': [sys.executable, "sleep.py", "5"],
222
'workdir': '.', 'timeout': 20}
223
c = SlaveShellCommand(self.builder, None, args)
225
c.command.BACKUP_TIMEOUT = 1
226
# make it unable to kill the child, by changing the signal it uses
227
# from SIGKILL to the do-nothing signal 0.
228
c.command.KILL = None
229
reactor.callLater(1, c.interrupt)
230
d.addBoth(self._testInterrupt2_1)
232
def _testInterrupt2_1(self, res):
233
# the slave should raise a TimeoutError exception. In a normal build
234
# process (i.e. one that uses step.RemoteShellCommand), this
235
# exception will be handed to the Step, which will acquire an ERROR
236
# status. In our test environment, it isn't such a big deal.
237
self.failUnless(isinstance(res, failure.Failure),
238
"res is not a Failure: %s" % (res,))
239
self.failUnless(res.check(commands.TimeoutError))
242
# the command is still actually running. Start another command, to
243
# make sure that a) the old command's output doesn't interfere with
244
# the new one, and b) the old command's actual termination doesn't
246
args = {'command': [sys.executable, "sleep.py", "5"],
247
'workdir': '.', 'timeout': 20}
248
c = SlaveShellCommand(self.builder, None, args)
250
d.addCallback(self._testInterrupt2_2)
252
def _testInterrupt2_2(self, res):
254
# N.B.: under windows, the trial process hangs out for another few
255
# seconds. I assume that the win32eventreactor is waiting for one of
256
# the lingering child processes to really finish.
258
haveProcess = interfaces.IReactorProcess(reactor, None)
259
if runtime.platformType == 'posix':
260
# test with PTYs also
261
class ShellPTY(ShellBase, unittest.TestCase):
264
ShellPTY.skip = "this reactor doesn't support IReactorProcess"
266
Shell.skip = "this reactor doesn't support IReactorProcess"