1"""Test suite for zdrun.py.""" 2from __future__ import print_function 3 4import os 5import sys 6import time 7import shutil 8import signal 9import tempfile 10import unittest 11import socket 12 13try: 14 from StringIO import StringIO 15except: 16 # Python 3 support. 17 from io import StringIO 18 19import ZConfig 20 21from zdaemon import zdrun, zdctl 22 23 24class ConfiguredOptions: 25 """Options class that loads configuration from a specified string. 26 27 This always loads from the string, regardless of any -C option 28 that may be given. 29 """ 30 31 def set_configuration(self, configuration): 32 self.__configuration = configuration 33 self.configfile = "<preloaded string>" 34 35 def load_configfile(self): 36 sio = StringIO(self.__configuration) 37 cfg = ZConfig.loadConfigFile(self.schema, sio, self.zconfig_options) 38 self.configroot, self.confighandlers = cfg 39 40 41class ConfiguredZDRunOptions(ConfiguredOptions, zdrun.ZDRunOptions): 42 43 def __init__(self, configuration): 44 zdrun.ZDRunOptions.__init__(self) 45 self.set_configuration(configuration) 46 47 48class ZDaemonTests(unittest.TestCase): 49 50 python = os.path.abspath(sys.executable) 51 assert os.path.exists(python) 52 here = os.path.abspath(os.path.dirname(__file__)) 53 assert os.path.isdir(here) 54 nokill = os.path.join(here, "nokill.py") 55 assert os.path.exists(nokill) 56 parent = os.path.dirname(here) 57 zdrun = os.path.join(parent, "zdrun.py") 58 assert os.path.exists(zdrun) 59 60 ppath = os.pathsep.join(sys.path) 61 62 def setUp(self): 63 self.zdsock = tempfile.mktemp() 64 self.new_stdout = StringIO() 65 self.save_stdout = sys.stdout 66 sys.stdout = self.new_stdout 67 self.expect = "" 68 69 def tearDown(self): 70 sys.stdout = self.save_stdout 71 for sig in (signal.SIGTERM, 72 signal.SIGHUP, 73 signal.SIGINT, 74 signal.SIGCHLD): 75 signal.signal(sig, signal.SIG_DFL) 76 try: 77 os.unlink(self.zdsock) 78 except os.error: 79 pass 80 output = self.new_stdout.getvalue() 81 self.assertEqual(self.expect, output) 82 83 def quoteargs(self, args): 84 for i in range(len(args)): 85 if " " in args[i]: 86 args[i] = '"%s"' % args[i] 87 return " ".join(args) 88 89 def rundaemon(self, args): 90 # Add quotes, in case some pathname contains spaces (e.g. Mac OS X) 91 args = self.quoteargs(args) 92 cmd = ('PYTHONPATH="%s" "%s" "%s" -d -s "%s" %s' % 93 (self.ppath, self.python, self.zdrun, self.zdsock, args)) 94 os.system(cmd) 95 # When the daemon crashes, the following may help debug it: 96 # os.system("PYTHONPATH=%s %s %s -s %s %s &" % 97 # (self.ppath, self.python, self.zdrun, self.zdsock, args)) 98 99 def _run(self, args, cmdclass=None, module=zdctl): 100 if isinstance(args, str): 101 args = args.split() 102 kw = {} 103 if cmdclass: 104 kw['cmdclass'] = cmdclass 105 try: 106 module.main(["-s", self.zdsock] + args, **kw) 107 except SystemExit: 108 pass 109 110 def testCmdclassOverride(self): 111 class MyCmd(zdctl.ZDCmd): 112 def do_sproing(self, rest): 113 print(rest) 114 self._run("-p echo sproing expected", cmdclass=MyCmd) 115 self.expect = "expected\n" 116 117 def testSystem(self): 118 self.rundaemon(["echo", "-n"]) 119 self.expect = "" 120 121 def test_help_zdrun(self): 122 self._run("-h", module=zdrun) 123 self.expect = zdrun.__doc__ 124 125 def test_help_zdctl(self): 126 self._run("-h") 127 self.expect = zdctl.__doc__ 128 129 def testOptionsSysArgv(self): 130 # Check that options are parsed from sys.argv by default 131 options = zdrun.ZDRunOptions() 132 save_sys_argv = sys.argv 133 try: 134 sys.argv = ["A", "B", "C"] 135 options.realize() 136 finally: 137 sys.argv = save_sys_argv 138 self.assertEqual(options.options, []) 139 self.assertEqual(options.args, ["B", "C"]) 140 141 def testOptionsBasic(self): 142 # Check basic option parsing 143 options = zdrun.ZDRunOptions() 144 options.realize(["B", "C"], "foo") 145 self.assertEqual(options.options, []) 146 self.assertEqual(options.args, ["B", "C"]) 147 self.assertEqual(options.progname, "foo") 148 149 def testOptionsHelp(self): 150 # Check that -h behaves properly 151 options = zdrun.ZDRunOptions() 152 try: 153 options.realize(["-h"], doc=zdrun.__doc__) 154 except SystemExit as err: 155 self.assertEqual(err.code, 0) 156 else: 157 self.fail("SystemExit expected") 158 self.expect = zdrun.__doc__ 159 160 def testSubprocessBasic(self): 161 # Check basic subprocess management: spawn, kill, wait 162 options = zdrun.ZDRunOptions() 163 options.realize(["sleep", "100"]) 164 proc = zdrun.Subprocess(options) 165 self.assertEqual(proc.pid, 0) 166 pid = proc.spawn() 167 self.assertEqual(proc.pid, pid) 168 msg = proc.kill(signal.SIGTERM) 169 self.assertEqual(msg, None) 170 wpid, wsts = os.waitpid(pid, 0) 171 self.assertEqual(wpid, pid) 172 self.assertEqual(os.WIFSIGNALED(wsts), 1) 173 self.assertEqual(os.WTERMSIG(wsts), signal.SIGTERM) 174 proc.setstatus(wsts) 175 self.assertEqual(proc.pid, 0) 176 177 def testEventlogOverride(self): 178 # Make sure runner.eventlog is used if it exists 179 options = ConfiguredZDRunOptions("""\ 180 <runner> 181 program /bin/true 182 <eventlog> 183 level 42 184 </eventlog> 185 </runner> 186 187 <eventlog> 188 level 35 189 </eventlog> 190 """) 191 options.realize(["/bin/true"]) 192 self.assertEqual(options.config_logger.level, 42) 193 194 def testEventlogWithoutOverride(self): 195 # Make sure eventlog is used if runner.eventlog doesn't exist 196 options = ConfiguredZDRunOptions("""\ 197 <runner> 198 program /bin/true 199 </runner> 200 201 <eventlog> 202 level 35 203 </eventlog> 204 """) 205 options.realize(["/bin/true"]) 206 self.assertEqual(options.config_logger.level, 35) 207 208 def testRunIgnoresParentSignals(self): 209 # Spawn a process which will in turn spawn a zdrun process. 210 # We make sure that the zdrun process is still running even if 211 # its parent process receives an interrupt signal (it should 212 # not be passed to zdrun). 213 tmp = tempfile.mkdtemp() 214 zdrun_socket = os.path.join(tmp, 'testsock') 215 try: 216 zdctlpid = os.spawnvpe( 217 os.P_NOWAIT, 218 sys.executable, 219 [sys.executable, os.path.join(self.here, 'parent.py'), tmp], 220 dict(os.environ, 221 PYTHONPATH=":".join(sys.path), 222 ) 223 ) 224 # Wait for it to start, but no longer than a minute. 225 deadline = time.time() + 60 226 is_started = False 227 while time.time() < deadline: 228 response = send_action('status\n', zdrun_socket) 229 if response is None: 230 time.sleep(0.05) 231 else: 232 is_started = True 233 break 234 self.assertTrue(is_started, 235 "spawned process failed to start in a minute") 236 # Kill it, and wait a little to ensure it's dead. 237 os.kill(zdctlpid, signal.SIGINT) 238 time.sleep(0.25) 239 # Make sure the child is still responsive. 240 response = send_action('status\n', zdrun_socket, 241 raise_on_error=True) 242 self.assertTrue(b'\n' in response, 243 'no newline in response: ' + repr(response)) 244 # Kill the process. 245 send_action('stop\n', zdrun_socket) 246 finally: 247 # Remove the tmp directory. 248 # Caution: this is delicate. The code here used to do 249 # shutil.rmtree(tmp), but that suffers a sometimes-fatal 250 # race with zdrun.py. The 'testsock' socket is created 251 # by zdrun in the tmp directory, and zdrun tries to 252 # unlink it. If shutil.rmtree sees 'testsock' too, it 253 # will also try to unlink it, but zdrun may complete 254 # unlinking it before shutil gets to it (there's more 255 # than one process here). So, in effect, we code a 256 # 1-level rmtree inline here, suppressing errors. 257 for fname in os.listdir(tmp): 258 try: 259 os.unlink(os.path.join(tmp, fname)) 260 except os.error: 261 pass 262 os.rmdir(tmp) 263 264 def testUmask(self): 265 # people have a strange tendency to run the tests as root 266 if os.getuid() == 0: 267 self.fail(""" 268I am root! 269Do not run the tests as root. 270Testing proper umask handling cannot be done as root. 271Furthermore, it is not a good idea and strongly discouraged to run zope, the 272build system (configure, make) or the tests as root. 273In general do not run anything as root unless absolutely necessary. 274""") 275 276 path = tempfile.mktemp() 277 # With umask 666, we should create a file that we aren't able 278 # to write. If access says no, assume that umask works. 279 try: 280 touch_cmd = "/bin/touch" 281 if not os.path.exists(touch_cmd): 282 touch_cmd = "/usr/bin/touch" # Mac OS X 283 self.rundaemon(["-m", "666", touch_cmd, path]) 284 for i in range(5): 285 if not os.path.exists(path): 286 time.sleep(0.1) 287 self.assertTrue(os.path.exists(path)) 288 self.assertTrue(not os.access(path, os.W_OK)) 289 finally: 290 if os.path.exists(path): 291 os.remove(path) 292 293 294class TestRunnerDirectory(unittest.TestCase): 295 296 def setUp(self): 297 super(TestRunnerDirectory, self).setUp() 298 self.root = tempfile.mkdtemp() 299 self.save_stdout = sys.stdout 300 self.save_stderr = sys.stdout 301 sys.stdout = StringIO() 302 sys.stderr = StringIO() 303 self.expect = '' 304 self.cmd = "/bin/true" 305 if not os.path.exists(self.cmd): 306 self.cmd = "/usr/bin/true" # Mac OS X 307 308 def tearDown(self): 309 shutil.rmtree(self.root) 310 got = sys.stdout.getvalue() 311 err = sys.stderr.getvalue() 312 sys.stdout = self.save_stdout 313 sys.stderr = self.save_stderr 314 if err: 315 print(err, end='', file=sys.stderr) 316 self.assertEqual(self.expect, got) 317 super(TestRunnerDirectory, self).tearDown() 318 319 def run_ctl(self, opts): 320 options = zdctl.ZDCtlOptions() 321 options.realize(opts + ['fg']) 322 self.expect = self.cmd + '\n' 323 proc = zdctl.ZDCmd(options) 324 proc.onecmd(" ".join(options.args)) 325 326 def testCtlRunDirectoryCreation(self): 327 path = os.path.join(self.root, 'rundir') 328 self.run_ctl(['-z', path, '-p', self.cmd]) 329 self.assertTrue(os.path.exists(path)) 330 331 def testCtlRunDirectoryCreationFromConfigFile(self): 332 path = os.path.join(self.root, 'rundir') 333 options = ['directory ' + path, 334 'program ' + self.cmd] 335 config = self.writeConfig( 336 '<runner>\n%s\n</runner>' % '\n'.join(options)) 337 self.run_ctl(['-C', config]) 338 self.assertTrue(os.path.exists(path)) 339 340 def testCtlRunDirectoryCreationOnlyOne(self): 341 path = os.path.join(self.root, 'rundir', 'not-created') 342 self.assertRaises(SystemExit, 343 self.run_ctl, ['-z', path, '-p', self.cmd]) 344 self.assertFalse(os.path.exists(path)) 345 got = sys.stderr.getvalue().strip() 346 sys.stderr = StringIO() 347 self.assertTrue(got.startswith('Error: invalid value for -z')) 348 349 def testCtlSocketDirectoryCreation(self): 350 path = os.path.join(self.root, 'rundir', 'sock') 351 self.run_ctl(['-s', path, '-p', self.cmd]) 352 self.assertTrue(os.path.exists(os.path.dirname(path))) 353 354 def testCtlSocketDirectoryCreationRelativePath(self): 355 path = os.path.join('rundir', 'sock') 356 self.run_ctl(['-s', path, '-p', self.cmd]) 357 self.assertTrue( 358 os.path.exists(os.path.dirname(os.path.join(os.getcwd(), path)))) 359 360 def testCtlSocketDirectoryCreationOnlyOne(self): 361 path = os.path.join(self.root, 'rundir', 'not-created', 'sock') 362 self.assertRaises(SystemExit, 363 self.run_ctl, ['-s', path, '-p', self.cmd]) 364 self.assertFalse(os.path.exists(path)) 365 got = sys.stderr.getvalue().strip() 366 sys.stderr = StringIO() 367 self.assertTrue(got.startswith('Error: invalid value for -s')) 368 369 def testCtlSocketDirectoryCreationFromConfigFile(self): 370 path = os.path.join(self.root, 'rundir') 371 options = ['socket-name %s/sock' % path, 372 'program ' + self.cmd] 373 config = self.writeConfig( 374 '<runner>\n%s\n</runner>' % '\n'.join(options)) 375 self.run_ctl(['-C', config]) 376 self.assertTrue(os.path.exists(path)) 377 378 def testCtlSocketDirectoryCreationFromConfigFileRelativePath(self): 379 path = 'rel-rundir' 380 options = ['socket-name %s/sock' % path, 381 'program ' + self.cmd] 382 config = self.writeConfig( 383 '<runner>\n%s\n</runner>' % '\n'.join(options)) 384 self.run_ctl(['-C', config]) 385 self.assertTrue(os.path.exists(os.path.join(os.getcwd(), path))) 386 387 def writeConfig(self, config): 388 config_file = os.path.join(self.root, 'config') 389 with open(config_file, 'w') as f: 390 f.write(config) 391 return config_file 392 393 def testDirectoryChown(self): 394 path = os.path.join(self.root, 'foodir') 395 options = zdctl.ZDCtlOptions() 396 options.realize(['-p', self.cmd, 'status']) 397 cmd = zdctl.ZDCmd(options) 398 options.uid = 27 399 options.gid = 28 400 # Patch chown and geteuid, because we're not root 401 chown = os.chown 402 geteuid = os.geteuid 403 calls = [] 404 405 def my_chown(*args): 406 calls.append(('chown',) + args) 407 408 def my_geteuid(): 409 return 0 410 411 try: 412 os.chown = my_chown 413 os.geteuid = my_geteuid 414 cmd.create_directory(path) 415 finally: 416 os.chown = chown 417 os.geteuid = geteuid 418 self.assertEqual([('chown', path, 27, 28)], calls) 419 420 421def send_action(action, sockname, raise_on_error=False): 422 """Send an action to the zdrun server and return the response. 423 424 Return None if the server is not up or any other error happened. 425 """ 426 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 427 try: 428 sock.connect(sockname) 429 sock.send(action.encode() + b"\n") 430 sock.shutdown(1) # We're not writing any more 431 response = b"" 432 while 1: 433 data = sock.recv(1000) 434 if not data: 435 break 436 response += data 437 sock.close() 438 return response 439 except socket.error as msg: 440 if str(msg) == 'AF_UNIX path too long': 441 # MacOS has apparent small limits on the length of a UNIX 442 # domain socket filename, we want to make MacOS users aware 443 # of the actual problem 444 raise 445 if raise_on_error: 446 raise 447 return None 448 finally: 449 sock.close() 450 451 452def test_suite(): 453 suite = unittest.TestSuite() 454 if os.name == "posix": 455 suite.addTest(unittest.makeSuite(ZDaemonTests)) 456 suite.addTest(unittest.makeSuite(TestRunnerDirectory)) 457 return suite 458 459if __name__ == '__main__': 460 __file__ = sys.argv[0] 461 unittest.main(defaultTest='test_suite') 462