1############################################################################## 2# 3# Copyright (c) 2004 Zope Foundation and Contributors. 4# All Rights Reserved. 5# 6# This software is subject to the provisions of the Zope Public License, 7# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. 8# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11# FOR A PARTICULAR PURPOSE. 12# 13############################################################################## 14from __future__ import print_function 15 16import doctest 17import glob 18import os 19import re 20import shutil 21import signal 22import subprocess 23import sys 24import tempfile 25import unittest 26from contextlib import contextmanager 27 28import ZConfig 29import manuel.capture 30import manuel.doctest 31import manuel.testing 32import zc.customdoctests 33import zdaemon 34from zope.testing import renormalizing 35 36try: 37 import pkg_resources 38 zdaemon_loc = pkg_resources.working_set.find( 39 pkg_resources.Requirement.parse('zdaemon')).location 40 zconfig_loc = pkg_resources.working_set.find( 41 pkg_resources.Requirement.parse('ZConfig')).location 42except (ImportError, AttributeError): 43 zdaemon_loc = os.path.dirname(os.path.dirname(zdaemon.__file__)) 44 zconfig_loc = os.path.dirname(os.path.dirname(ZConfig.__file__)) 45 46 47def write(name, text): 48 with open(name, 'w') as f: 49 f.write(text) 50 51 52def read(name): 53 with open(name) as f: 54 return f.read() 55 56 57def make_sure_non_daemon_mode_doesnt_hang_when_program_exits(): 58 """ 59 The whole awhile bit that waits for a program to start 60 whouldn't be used on non-daemon mode. 61 62 >>> write('conf', 63 ... ''' 64 ... <runner> 65 ... program sleep 1 66 ... daemon off 67 ... </runner> 68 ... ''') 69 70 >>> system("./zdaemon -Cconf start") 71 72 """ 73 74 75def dont_hang_when_program_doesnt_start(): 76 """ 77 If a program doesn't start, we don't want to wait for ever. 78 79 >>> write('conf', 80 ... ''' 81 ... <runner> 82 ... program sleep 83 ... backoff-limit 2 84 ... </runner> 85 ... ''') 86 87 >>> system("./zdaemon -Cconf start") 88 . . 89 daemon manager not running 90 Failed: 1 91 92 """ 93 94 95def allow_duplicate_arguments(): 96 """ 97 Wrapper scripts will often embed configuration arguments. This could 98 cause a problem when zdaemon reinvokes itself, passing it's own set of 99 configuration arguments. To deal with this, we'll allow duplicate 100 arguments that have the same values. 101 102 >>> write('conf', 103 ... ''' 104 ... <runner> 105 ... program sleep 10 106 ... </runner> 107 ... ''') 108 109 >>> system("./zdaemon -Cconf -Cconf -Cconf start") 110 . . 111 daemon process started, pid=21446 112 113 >>> system("./zdaemon -Cconf -Cconf -Cconf stop") 114 . . 115 daemon process stopped 116 117 """ 118 119 120def test_stop_timeout(): 121 r""" 122 123 >>> write('t.py', 124 ... ''' 125 ... import time, signal 126 ... signal.signal(signal.SIGTERM, lambda *a: None) 127 ... while 1: time.sleep(9) 128 ... ''') 129 130 >>> write('conf', 131 ... ''' 132 ... <runner> 133 ... program %s t.py 134 ... stop-timeout 1 135 ... </runner> 136 ... ''' % sys.executable) 137 138 >>> system("./zdaemon -Cconf start") 139 . . 140 daemon process started, pid=21446 141 142 >>> import threading, time 143 >>> thread = threading.Thread( 144 ... target=system, args=("./zdaemon -Cconf stop",), 145 ... kwargs=dict(quiet=True)) 146 >>> thread.start() 147 >>> time.sleep(.2) 148 149 >>> system("./zdaemon -Cconf status") 150 program running; pid=15372 151 152 >>> thread.join(2) 153 154 >>> system("./zdaemon -Cconf status") 155 daemon manager not running 156 Failed: 3 157 158 """ 159 160 161def test_kill(): 162 """ 163 164 >>> write('conf', 165 ... ''' 166 ... <runner> 167 ... program sleep 100 168 ... </runner> 169 ... ''') 170 171 >>> system("./zdaemon -Cconf start") 172 . . 173 daemon process started, pid=1234 174 175 >>> system("./zdaemon -Cconf kill ded") 176 invalid signal 'ded' 177 178 >>> system("./zdaemon -Cconf kill CONT") 179 kill(1234, 18) 180 signal SIGCONT sent to process 1234 181 182 >>> system("./zdaemon -Cconf stop") 183 . . 184 daemon process stopped 185 186 >>> system("./zdaemon -Cconf kill") 187 daemon process not running 188 189 """ 190 191 192def test_logreopen(): 193 """ 194 195 >>> write('conf', 196 ... ''' 197 ... <runner> 198 ... program sleep 100 199 ... transcript transcript.log 200 ... </runner> 201 ... ''') 202 203 >>> system("./zdaemon -Cconf start") 204 . . 205 daemon process started, pid=1234 206 207 >>> os.rename('transcript.log', 'transcript.log.1') 208 209 >>> system("./zdaemon -Cconf logreopen") 210 kill(1234, 12) 211 signal SIGUSR2 sent to process 1234 212 213 This also reopens the transcript.log: 214 215 >>> sorted(os.listdir('.')) 216 ['conf', 'transcript.log', 'transcript.log.1', 'zdaemon', 'zdsock'] 217 218 >>> system("./zdaemon -Cconf stop") 219 . . 220 daemon process stopped 221 222 """ 223 224 225def test_log_rotation(): 226 """ 227 228 >>> write('conf', 229 ... ''' 230 ... <runner> 231 ... program sleep 100 232 ... transcript transcript.log 233 ... </runner> 234 ... <eventlog> 235 ... <logfile> 236 ... path event.log 237 ... </logfile> 238 ... </eventlog> 239 ... ''') 240 241 >>> system("./zdaemon -Cconf start") 242 . . 243 daemon process started, pid=1234 244 245 Pretend we did a logrotate: 246 247 >>> os.rename('transcript.log', 'transcript.log.1') 248 >>> os.rename('event.log', 'event.log.1') 249 250 >>> system("./zdaemon -Cconf reopen_transcript") # or logreopen 251 252 This reopens both transcript.log and event.log: 253 254 >>> sorted(glob.glob('transcript.log*')) 255 ['transcript.log', 'transcript.log.1'] 256 257 >>> sorted(glob.glob('event.log*')) 258 ['event.log', 'event.log.1'] 259 260 >>> system("./zdaemon -Cconf stop") 261 . . 262 daemon process stopped 263 264 """ 265 266 267def test_start_test_program(): 268 """ 269 >>> write('t.py', 270 ... ''' 271 ... import time 272 ... time.sleep(1) 273 ... open('x', 'w').close() 274 ... time.sleep(99) 275 ... ''') 276 277 >>> write('conf', 278 ... ''' 279 ... <runner> 280 ... program %s t.py 281 ... start-test-program cat x 282 ... </runner> 283 ... ''' % sys.executable) 284 285 >>> import os 286 287 >>> system("./zdaemon -Cconf start") 288 . . 289 daemon process started, pid=21446 290 291 >>> os.path.exists('x') 292 True 293 >>> os.remove('x') 294 295 >>> system("./zdaemon -Cconf restart") 296 . . . 297 daemon process restarted, pid=19622 298 >>> os.path.exists('x') 299 True 300 301 >>> system("./zdaemon -Cconf stop") 302 <BLANKLINE> 303 daemon process stopped 304 """ 305 306 307def test_start_timeout(): 308 """ 309 >>> write('t.py', 310 ... ''' 311 ... import time 312 ... time.sleep(9) 313 ... ''') 314 315 >>> write('conf', 316 ... ''' 317 ... <runner> 318 ... program %s t.py 319 ... start-test-program cat x 320 ... start-timeout 1 321 ... </runner> 322 ... ''' % sys.executable) 323 324 >>> import time 325 >>> start = time.time() 326 327 >>> system("./zdaemon -Cconf start") 328 <BLANKLINE> 329 Program took too long to start 330 Failed: 1 331 332 >>> system("./zdaemon -Cconf stop") 333 <BLANKLINE> 334 daemon process stopped 335 """ 336 337 338def DAEMON_MANAGER_MODE_leak(): 339 """ 340 Zdaemon used an environment variable to flag that it's running in 341 daemon-manager mode, as opposed to UI mode. If this environment 342 variable is allowed to leak to the program, them the program will 343 be unable to invoke zdaemon correctly. 344 345 >>> write('c', ''' 346 ... <runner> 347 ... program env 348 ... transcript t 349 ... </runner> 350 ... ''') 351 352 >>> system('./zdaemon -b0 -T1 -Cc start', quiet=True) 353 Failed: 1 354 >>> 'DAEMON_MANAGER_MODE' not in read('t') 355 True 356 """ 357 358 359def nonzero_exit_on_program_failure(): 360 """ 361 >>> write('conf', 362 ... ''' 363 ... <runner> 364 ... backoff-limit 1 365 ... program nosuch 366 ... </runner> 367 ... ''') 368 369 >>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS 370 ./zdaemon... 371 daemon manager not running 372 Failed: 1 373 374 >>> write('conf', 375 ... ''' 376 ... <runner> 377 ... backoff-limit 1 378 ... program cat nosuch 379 ... </runner> 380 ... ''') 381 382 >>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS 383 ./zdaemon... 384 daemon manager not running 385 Failed: 1 386 387 >>> write('conf', 388 ... ''' 389 ... <runner> 390 ... backoff-limit 1 391 ... program pwd 392 ... </runner> 393 ... ''') 394 395 >>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS 396 ./zdaemon... 397 daemon manager not running 398 Failed: 1 399 400 """ 401 402 403def setUp(test): 404 test.globs['_td'] = td = [] 405 here = os.getcwd() 406 td.append(lambda: os.chdir(here)) 407 tmpdir = tempfile.mkdtemp() 408 td.append(lambda: shutil.rmtree(tmpdir)) 409 test.globs['tmpdir'] = tmpdir 410 workspace = tempfile.mkdtemp() 411 td.append(lambda: shutil.rmtree(workspace)) 412 os.chdir(workspace) 413 write('zdaemon', zdaemon_template % dict( 414 python=sys.executable, 415 zdaemon=zdaemon_loc, 416 ZConfig=zconfig_loc, 417 )) 418 os.chmod('zdaemon', 0o755) 419 test.globs['system'] = system 420 421 422def tearDown(test): 423 for f in test.globs['_td']: 424 f() 425 426 427class Timeout(BaseException): 428 pass 429 430 431@contextmanager 432def timeout(seconds): 433 this_frame = sys._getframe() 434 435 def raiseTimeout(signal, frame): 436 # the if statement here is meant to prevent an exception in the 437 # finally: clause before clean up can take place 438 if frame is not this_frame: 439 raise Timeout('timed out after %s seconds' % seconds) 440 441 try: 442 prev_handler = signal.signal(signal.SIGALRM, raiseTimeout) 443 except ValueError: 444 # signal only works in main thread 445 # let's ignore the request for a timeout and hope the test doesn't hang 446 yield 447 else: 448 try: 449 signal.alarm(seconds) 450 yield 451 finally: 452 signal.alarm(0) 453 signal.signal(signal.SIGALRM, prev_handler) 454 455 456def system(command, input='', quiet=False, echo=False): 457 if echo: 458 print(command) 459 p = subprocess.Popen( 460 command, shell=True, 461 stdin=subprocess.PIPE, 462 stdout=subprocess.PIPE, 463 stderr=subprocess.STDOUT) 464 with timeout(60): 465 data = p.communicate(input)[0] 466 if not quiet: 467 print(data.decode(), end='') 468 r = p.wait() 469 if r: 470 print('Failed:', r) 471 472 473def checkenv(match): 474 match = [a for a in match.group(1).split('\n')[:-1] 475 if a.split('=')[0] in ('HOME', 'LD_LIBRARY_PATH')] 476 match.sort() 477 return '\n'.join(match) + '\n' 478 479 480zdaemon_template = """#!%(python)s 481 482import sys 483sys.path[0:0] = [ 484 %(zdaemon)r, 485 %(ZConfig)r, 486 ] 487 488try: 489 import coverage 490except ImportError: 491 pass 492else: 493 coverage.process_startup() 494 495import zdaemon.zdctl 496 497if __name__ == '__main__': 498 zdaemon.zdctl.main() 499""" 500 501 502def test_suite(): 503 README_checker = renormalizing.RENormalizing([ 504 (re.compile('pid=\d+'), 'pid=NNN'), 505 (re.compile('(\. )+\.?'), '<BLANKLINE>'), 506 (re.compile('^env\n((?:.*\n)+)$'), checkenv), 507 ]) 508 509 return unittest.TestSuite(( 510 doctest.DocTestSuite( 511 setUp=setUp, tearDown=tearDown, 512 checker=renormalizing.RENormalizing([ 513 (re.compile('pid=\d+'), 'pid=NNN'), 514 (re.compile('(\. )+\.?'), '<BLANKLINE>'), 515 (re.compile('process \d+'), 'process NNN'), 516 (re.compile('kill\(\d+,'), 'kill(NNN,'), 517 ])), 518 manuel.testing.TestSuite( 519 manuel.doctest.Manuel( 520 parser=zc.customdoctests.DocTestParser( 521 ps1='sh>', 522 transform=lambda s: 'system("%s")\n' % s.rstrip() 523 ), 524 checker=README_checker, 525 ) + 526 manuel.doctest.Manuel(checker=README_checker) + 527 manuel.capture.Manuel(), 528 '../README.rst', 529 setUp=setUp, tearDown=tearDown), 530 )) 531