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