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
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
)
24 def touch(self
, basedir
, path
, mode
=None):
25 fn
= os
.path
.join(basedir
, path
)
32 def test_rmdirRecursive(self
):
33 basedir
= "slavecommand/Utilities/test_rmdirRecursive"
35 d
= os
.path
.join(basedir
, "doomed")
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)
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)
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
):
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
):
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"),
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
):
76 for r
in self
.builder
.updates
:
81 def checkOutput(self
, expected
):
83 @type expected: list of (streamname, contents) tuples
84 @param expected: the expected output
86 expected_linesep
= os
.linesep
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
]
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
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
)
105 self
.failUnless(self
.builder
.updates
[-1].has_key('rc'))
106 got
= self
.builder
.updates
[-1]['rc']
108 def checkrc(self
, expected
):
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
)
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
)
128 def _checkPass(self
, res
, expected
, rc
):
129 self
.checkOutput(expected
)
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
)
137 expected
= [('stdout', "this is stdout\n"),
138 ('stderr', "this is stderr\n")]
139 d
.addCallback(self
._checkPass
, expected
, 0)
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
)
147 expected
= [('stdout', "this is stdout\n"),
148 ('stderr', "this is stderr\n")]
149 d
.addCallback(self
._checkPass
, expected
, 1)
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
)
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)
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
)
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
)
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
)
188 d
.addCallback(self
._testShellMissingCommand
_1)
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
197 def testTimeout(self
):
198 args
= {'command': [sys
.executable
, self
.sleepcmd
, "10"],
199 'workdir': '.', 'timeout': 2}
200 c
= SlaveShellCommand(self
.builder
, None, args
)
202 d
.addCallback(self
._testTimeout
_1)
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
)
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
)
222 reactor
.callLater(1, c
.interrupt
)
223 d
.addCallback(self
._testInterrupt
1_1)
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
):
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
)
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
._testInterrupt
2_1)
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
))
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
269 args
= {'command': [sys
.executable
, self
.sleepcmd
, "5"],
270 'workdir': '.', 'timeout': 20}
271 c
= SlaveShellCommand(self
.builder
, None, args
)
273 d
.addCallback(self
._testInterrupt
2_2)
275 def _testInterrupt2_2(self
, res
):
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
):
287 ShellPTY
.skip
= "this reactor doesn't support IReactorProcess"
289 Shell
.skip
= "this reactor doesn't support IReactorProcess"