• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / zdaemon / 12825577578

17 Jan 2025 08:41AM UTC coverage: 79.914% (-0.08%) from 79.992%
12825577578

Pull #37

github

web-flow
Trigger CI
Pull Request #37: Update Python version support.

361 of 512 branches covered (70.51%)

Branch coverage included in aggregate %.

5 of 15 new or added lines in 5 files covered. (33.33%)

5 existing lines in 4 files now uncovered.

1676 of 2037 relevant lines covered (82.28%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.2
/src/zdaemon/tests/tests.py
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
##############################################################################
14

15
import doctest
1✔
16
import glob
1✔
17
import os
1✔
18
import re
1✔
19
import shutil
1✔
20
import signal
1✔
21
import subprocess
1✔
22
import sys
1✔
23
import tempfile
1✔
24
import unittest
1✔
25
from contextlib import contextmanager
1✔
26

27
import manuel.capture
1✔
28
import manuel.doctest
1✔
29
import manuel.testing
1✔
30
import zc.customdoctests
1✔
31
import ZConfig
1✔
32
from zope.testing import renormalizing
1✔
33

34
import zdaemon
1✔
35

36

37
try:
1✔
38
    import pkg_resources
1✔
39
    zdaemon_loc = pkg_resources.working_set.find(
1✔
40
        pkg_resources.Requirement.parse('zdaemon')).location
41
    zconfig_loc = pkg_resources.working_set.find(
1✔
42
        pkg_resources.Requirement.parse('ZConfig')).location
NEW
43
except (ModuleNotFoundError, AttributeError):
×
44
    zdaemon_loc = os.path.dirname(os.path.dirname(zdaemon.__file__))
×
45
    zconfig_loc = os.path.dirname(os.path.dirname(ZConfig.__file__))
×
46

47

48
def write(name, text):
1✔
49
    with open(name, 'w') as f:
1✔
50
        f.write(text)
1✔
51

52

53
def read(name):
1✔
54
    with open(name) as f:
1✔
55
        return f.read()
1✔
56

57

58
def test_ChildExits():
1✔
59
    """
60
    ``_ChildExits`` is used to record the exit status for a
61
    foreign process when it was captured by the ``SIGCHLD``handler
62
    due to a race condition.
63

64
    >>> from zdaemon.zdrun import _ChildExits
65
    >>> exits = _ChildExits()
66

67
    Initially, we have no exit status for a process.
68
    >>> exits.fetch(5)
69

70
    After we have recorded exit information, ``fetch`` will
71
    retrieve and reset it.
72
    >>> exits[5] = 0
73
    >>> exits.fetch(5)
74
    0
75
    >>> exits.fetch(5)
76

77
    """
78

79

80
def make_sure_non_daemon_mode_doesnt_hang_when_program_exits():
1✔
81
    """
82
    The whole awhile bit that waits for a program to start
83
    whouldn't be used on non-daemon mode.
84

85
    >>> write('conf',
86
    ... '''
87
    ... <runner>
88
    ...   program sleep 1
89
    ...   daemon off
90
    ... </runner>
91
    ... ''')
92

93
    >>> system("./zdaemon -Cconf start")
94

95
    """
96

97

98
def dont_hang_when_program_doesnt_start():
1✔
99
    """
100
    If a program doesn't start, we don't want to wait for ever.
101

102
    >>> write('conf',
103
    ... '''
104
    ... <runner>
105
    ...   program sleep
106
    ...   backoff-limit 2
107
    ... </runner>
108
    ... ''')
109

110
    >>> system("./zdaemon -Cconf start")
111
    . .
112
    daemon manager not running
113
    Failed: 1
114

115
    """
116

117

118
def allow_duplicate_arguments():
1✔
119
    """
120
    Wrapper scripts will often embed configuration arguments. This could
121
    cause a problem when zdaemon reinvokes itself, passing it's own set of
122
    configuration arguments.  To deal with this, we'll allow duplicate
123
    arguments that have the same values.
124

125
    >>> write('conf',
126
    ... '''
127
    ... <runner>
128
    ...   program sleep 10
129
    ... </runner>
130
    ... ''')
131

132
    >>> system("./zdaemon -Cconf -Cconf -Cconf start")
133
    . .
134
    daemon process started, pid=21446
135

136
    >>> system("./zdaemon -Cconf -Cconf -Cconf stop")
137
    . .
138
    daemon process stopped
139

140
    """
141

142

143
def test_stop_timeout():
1✔
144
    r"""
145

146
    >>> write('t.py',
147
    ... '''
148
    ... import time, signal
149
    ... signal.signal(signal.SIGTERM, lambda *a: None)
150
    ... while 1: time.sleep(9)
151
    ... ''')
152

153
    >>> write('conf',
154
    ... '''
155
    ... <runner>
156
    ...   program %s t.py
157
    ...   stop-timeout 1
158
    ... </runner>
159
    ... ''' % sys.executable)
160

161
    >>> system("./zdaemon -Cconf start")
162
    . .
163
    daemon process started, pid=21446
164

165
    >>> import threading, time
166
    >>> thread = threading.Thread(
167
    ...     target=system, args=("./zdaemon -Cconf stop",),
168
    ...     kwargs=dict(quiet=True))
169
    >>> thread.start()
170
    >>> time.sleep(.2)
171

172
    >>> system("./zdaemon -Cconf status")
173
    program running; pid=15372
174

175
    >>> thread.join(2)
176

177
    >>> system("./zdaemon -Cconf status")
178
    daemon manager not running
179
    Failed: 3
180

181
    """
182

183

184
def test_kill():
1✔
185
    """
186

187
    >>> write('conf',
188
    ... '''
189
    ... <runner>
190
    ...   program sleep 100
191
    ... </runner>
192
    ... ''')
193

194
    >>> system("./zdaemon -Cconf start")
195
    . .
196
    daemon process started, pid=1234
197

198
    >>> system("./zdaemon -Cconf kill ded")
199
    invalid signal 'ded'
200

201
    >>> system("./zdaemon -Cconf kill CONT")
202
    kill(1234, 18)
203
    signal SIGCONT sent to process 1234
204

205
    >>> system("./zdaemon -Cconf stop")
206
    . .
207
    daemon process stopped
208

209
    >>> system("./zdaemon -Cconf kill")
210
    daemon process not running
211

212
    """
213

214

215
def test_logreopen():
1✔
216
    """
217

218
    >>> write('conf',
219
    ... '''
220
    ... <runner>
221
    ...   program sleep 100
222
    ...   transcript transcript.log
223
    ... </runner>
224
    ... ''')
225

226
    >>> system("./zdaemon -Cconf start")
227
    . .
228
    daemon process started, pid=1234
229

230
    >>> os.rename('transcript.log', 'transcript.log.1')
231

232
    >>> system("./zdaemon -Cconf logreopen")
233
    kill(1234, 12)
234
    signal SIGUSR2 sent to process 1234
235

236
    This also reopens the transcript.log:
237

238
    >>> sorted(x
239
    ...        for x in os.listdir('.')
240
    ...        if x in [
241
    ...            'conf', 'transcript.log', 'transcript.log.1', 'zdaemon', 'zdsock'])
242
    ['conf', 'transcript.log', 'transcript.log.1', 'zdaemon', 'zdsock']
243

244
    >>> system("./zdaemon -Cconf stop")
245
    . .
246
    daemon process stopped
247

248
    """  # noqa: E501 line too long
249

250

251
def test_log_rotation():
1✔
252
    """
253

254
    >>> write('conf',
255
    ... '''
256
    ... <runner>
257
    ...   program sleep 100
258
    ...   transcript transcript.log
259
    ... </runner>
260
    ... <eventlog>
261
    ...   <logfile>
262
    ...     path event.log
263
    ...   </logfile>
264
    ... </eventlog>
265
    ... ''')
266

267
    >>> system("./zdaemon -Cconf start")
268
    . .
269
    daemon process started, pid=1234
270

271
    Pretend we did a logrotate:
272

273
    >>> os.rename('transcript.log', 'transcript.log.1')
274
    >>> os.rename('event.log', 'event.log.1')
275

276
    >>> system("./zdaemon -Cconf reopen_transcript")  # or logreopen
277

278
    This reopens both transcript.log and event.log:
279

280
    >>> sorted(glob.glob('transcript.log*'))
281
    ['transcript.log', 'transcript.log.1']
282

283
    >>> sorted(glob.glob('event.log*'))
284
    ['event.log', 'event.log.1']
285

286
    >>> system("./zdaemon -Cconf stop")
287
    . .
288
    daemon process stopped
289

290
    """
291

292

293
def test_start_test_program():
1✔
294
    """
295
    >>> write('t.py',
296
    ... '''
297
    ... import time
298
    ... time.sleep(2)
299
    ... open('x', 'w').close()
300
    ... time.sleep(99)
301
    ... ''')
302

303
    >>> write('conf',
304
    ... '''
305
    ... <runner>
306
    ...   program %s t.py
307
    ...   start-test-program cat x
308
    ...   daemon on
309
    ... </runner>
310
    ... <eventlog>
311
    ...   level debug
312
    ...   <logfile>
313
    ...      path log
314
    ...      level debug
315
    ...   </logfile>
316
    ... </eventlog>
317
    ... ''' % sys.executable)
318

319
    >>> import os
320

321
    >>> system("./zdaemon -Cconf start")
322
    . .
323
    daemon process started, pid=21446
324

325
    >>> os.path.exists('x')
326
    True
327
    >>> os.remove('x')
328

329
    >>> with open("log") as f:
330
    ...   logged = f.read()
331
    >>> nfailed = logged.count("start test failed")
332
    >>> nfailed > 0
333
    True
334
    >>> logged.count("start test succeeded")
335
    1
336

337
    >>> system("./zdaemon -Cconf restart")
338
    . . .
339
    daemon process restarted, pid=19622
340
    >>> os.path.exists('x')
341
    True
342

343
    >>> with open("log") as f:
344
    ...   logged = f.read()
345
    >>> nfailed < logged.count("start test failed")
346
    True
347
    >>> logged.count("start test succeeded")
348
    2
349

350
    >>> system("./zdaemon -Cconf stop")
351
    <BLANKLINE>
352
    daemon process stopped
353
    """
354

355

356
def test_start_timeout():
1✔
357
    """
358
    >>> write('t.py',
359
    ... '''
360
    ... import time
361
    ... time.sleep(9)
362
    ... ''')
363

364
    >>> write('conf',
365
    ... '''
366
    ... <runner>
367
    ...   program %s t.py
368
    ...   start-test-program cat x
369
    ...   start-timeout 1
370
    ... </runner>
371
    ... ''' % sys.executable)
372

373
    >>> import time
374
    >>> start = time.time()
375

376
    >>> system("./zdaemon -Cconf start")
377
    <BLANKLINE>
378
    Program took too long to start
379
    Failed: 1
380

381
    >>> system("./zdaemon -Cconf stop")
382
    <BLANKLINE>
383
    daemon process stopped
384
    """
385

386

387
def DAEMON_MANAGER_MODE_leak():
1✔
388
    """
389
    Zdaemon used an environment variable to flag that it's running in
390
    daemon-manager mode, as opposed to UI mode.  If this environment
391
    variable is allowed to leak to the program, them the program will
392
    be unable to invoke zdaemon correctly.
393

394
    >>> write('c', '''
395
    ... <runner>
396
    ...   program env
397
    ...   transcript t
398
    ... </runner>
399
    ... ''')
400

401
    >>> system('./zdaemon -b0 -T1 -Cc start', quiet=True)
402
    Failed: 1
403
    >>> 'DAEMON_MANAGER_MODE' not in read('t')
404
    True
405
    """
406

407

408
def nonzero_exit_on_program_failure():
1✔
409
    """
410
    >>> write('conf',
411
    ... '''
412
    ... <runner>
413
    ...   backoff-limit 1
414
    ...   program nosuch
415
    ... </runner>
416
    ... ''')
417

418
    >>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS
419
    ./zdaemon...
420
    daemon manager not running
421
    Failed: 1
422

423
    >>> write('conf',
424
    ... '''
425
    ... <runner>
426
    ...   backoff-limit 1
427
    ...   program cat nosuch
428
    ... </runner>
429
    ... ''')
430

431
    >>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS
432
    ./zdaemon...
433
    daemon manager not running
434
    Failed: 1
435

436
    >>> write('conf',
437
    ... '''
438
    ... <runner>
439
    ...   backoff-limit 1
440
    ...   program pwd
441
    ... </runner>
442
    ... ''')
443

444
    >>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS
445
    ./zdaemon...
446
    daemon manager not running
447
    Failed: 1
448

449
    """
450

451

452
def setUp(test):
1✔
453
    test.globs['_td'] = td = []
1✔
454
    here = os.getcwd()
1✔
455
    td.append(lambda: os.chdir(here))
1✔
456
    tmpdir = tempfile.mkdtemp()
1✔
457
    td.append(lambda: shutil.rmtree(tmpdir))
1✔
458
    test.globs['tmpdir'] = tmpdir
1✔
459
    workspace = tempfile.mkdtemp()
1✔
460
    td.append(lambda: shutil.rmtree(workspace))
1✔
461
    os.chdir(workspace)
1✔
462
    write('zdaemon', zdaemon_template % dict(
1✔
463
        python=sys.executable,
464
        zdaemon=zdaemon_loc,
465
        ZConfig=zconfig_loc,
466
    ))
467
    os.chmod('zdaemon', 0o755)
1✔
468
    test.globs['system'] = system
1✔
469

470

471
def tearDown(test):
1✔
472
    for f in test.globs['_td']:
1✔
473
        f()
1✔
474

475

476
class Timeout(BaseException):
1✔
477
    pass
1✔
478

479

480
@contextmanager
1✔
481
def timeout(seconds):
1✔
482
    this_frame = sys._getframe()
1✔
483

484
    def raiseTimeout(signal, frame):
1✔
485
        # the if statement here is meant to prevent an exception in the
486
        # finally: clause before clean up can take place
487
        if frame is not this_frame:
×
488
            raise Timeout('timed out after %s seconds' % seconds)
×
489

490
    try:
1✔
491
        prev_handler = signal.signal(signal.SIGALRM, raiseTimeout)
1✔
492
    except ValueError:
1✔
493
        # signal only works in main thread
494
        # let's ignore the request for a timeout and hope the test doesn't hang
495
        yield
1✔
496
    else:
497
        try:
1✔
498
            signal.alarm(seconds)
1✔
499
            yield
1✔
500
        finally:
501
            signal.alarm(0)
1✔
502
            signal.signal(signal.SIGALRM, prev_handler)
1✔
503

504

505
def system(command, input='', quiet=False, echo=False):
1✔
506
    if echo:
1✔
507
        print(command)
1✔
508
    p = subprocess.Popen(
1✔
509
        command, shell=True,
510
        stdin=subprocess.PIPE,
511
        stdout=subprocess.PIPE,
512
        stderr=subprocess.STDOUT)
513
    with timeout(60):
1✔
514
        data = p.communicate(input)[0]
1✔
515
    if not quiet:
1✔
516
        print(data.decode(), end='')
1✔
517
    r = p.wait()
1✔
518
    if r:
1✔
519
        print('Failed:', r)
1✔
520

521

522
def checkenv(match):
1✔
523
    match = sorted([a for a in match.group(1).split('\n')[:-1]
1✔
524
                    if a.split('=')[0] in ('HOME', 'LIBRARY_PATH')])
525
    return '\n'.join(match) + '\n'
1✔
526

527

528
zdaemon_template = """#!%(python)s
529

530
import sys
531
sys.path[0:0] = [
532
  %(zdaemon)r,
533
  %(ZConfig)r,
534
  ]
535

536
try:
537
    import coverage
538
except ModuleNotFoundError:
539
    pass
540
else:
541
    coverage.process_startup()
542

543
import zdaemon.zdctl
544

545
if __name__ == '__main__':
546
    zdaemon.zdctl.main()
547
"""
548

549

550
def test_suite():
1✔
551
    README_checker = renormalizing.RENormalizing([
1✔
552
        (re.compile(r'pid=\d+'), 'pid=NNN'),
553
        (re.compile(r'(\. )+\.?'), '<BLANKLINE>'),
554
        (re.compile('^env\n((?:.*\n)+)$'), checkenv),
555
    ])
556

557
    return unittest.TestSuite((
1✔
558
        doctest.DocTestSuite(
559
            setUp=setUp, tearDown=tearDown,
560
            checker=renormalizing.RENormalizing([
561
                (re.compile(r'pid=\d+'), 'pid=NNN'),
562
                (re.compile(r'(\. )+\.?'), '<BLANKLINE>'),
563
                (re.compile(r'process \d+'), 'process NNN'),
564
                (re.compile(r'kill\(\d+, \d+\)'), 'kill(NNN, MM)'),
565
            ])),
566
        manuel.testing.TestSuite(
567
            manuel.doctest.Manuel(
568
                parser=zc.customdoctests.DocTestParser(
569
                    ps1='sh>',
570
                    transform=lambda s: 'system("%s")\n' % s.rstrip()
571
                ),
572
                checker=README_checker,
573
            ) +
574
            manuel.doctest.Manuel(checker=README_checker) +
575
            manuel.capture.Manuel(),
576
            '../README.rst',
577
            setUp=setUp, tearDown=tearDown),
578
    ))
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc