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