~canonical-livepatch-dependencies/canonical-livepatch-service-dependencies/twisted

« back to all changes in this revision

Viewing changes to twisted/test/test_twistd.py

  • Committer: Free Ekanayaka
  • Date: 2016-07-01 12:22:33 UTC
  • Revision ID: free.ekanayaka@canonical.com-20160701122233-nh55w514zwzoz1ip
Tags: upstream-16.2.0
ImportĀ upstreamĀ versionĀ 16.2.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) Twisted Matrix Laboratories.
 
2
# See LICENSE for details.
 
3
 
 
4
"""
 
5
Tests for L{twisted.application.app} and L{twisted.scripts.twistd}.
 
6
"""
 
7
 
 
8
from __future__ import absolute_import, division
 
9
 
 
10
import errno
 
11
import inspect
 
12
import signal
 
13
import os
 
14
import sys
 
15
 
 
16
try:
 
17
    import pwd
 
18
    import grp
 
19
except ImportError:
 
20
    pwd = grp = None
 
21
 
 
22
try:
 
23
    import cPickle as pickle
 
24
except ImportError:
 
25
    import pickle
 
26
 
 
27
from zope.interface import implementer
 
28
from zope.interface.verify import verifyObject
 
29
 
 
30
from twisted.trial import unittest
 
31
from twisted.test.test_process import MockOS
 
32
 
 
33
from twisted import plugin, logger
 
34
from twisted.application.service import IServiceMaker
 
35
from twisted.application import service, app, reactors
 
36
from twisted.scripts import twistd
 
37
from twisted.python.compat import NativeStringIO
 
38
from twisted.python.usage import UsageError
 
39
from twisted.python.log import (ILogObserver as LegacyILogObserver,
 
40
                                textFromEventDict)
 
41
from twisted.python.components import Componentized
 
42
from twisted.internet.defer import Deferred
 
43
from twisted.internet.interfaces import IReactorDaemonize
 
44
from twisted.internet.test.modulehelpers import AlternateReactor
 
45
from twisted.python.fakepwd import UserDatabase
 
46
from twisted.logger import globalLogBeginner, globalLogPublisher, ILogObserver
 
47
 
 
48
try:
 
49
    from twisted.scripts import _twistd_unix
 
50
except ImportError:
 
51
    _twistd_unix = None
 
52
else:
 
53
    from twisted.scripts._twistd_unix import UnixApplicationRunner
 
54
    from twisted.scripts._twistd_unix import UnixAppLogger
 
55
 
 
56
 
 
57
try:
 
58
    from twisted.python import syslog
 
59
except ImportError:
 
60
    syslog = None
 
61
 
 
62
 
 
63
try:
 
64
    import profile
 
65
except ImportError:
 
66
    profile = None
 
67
 
 
68
 
 
69
try:
 
70
    import pstats
 
71
    import cProfile
 
72
except ImportError:
 
73
    cProfile = None
 
74
 
 
75
if getattr(os, 'setuid', None) is None:
 
76
    setuidSkip = "Platform does not support --uid/--gid twistd options."
 
77
else:
 
78
    setuidSkip = None
 
79
 
 
80
 
 
81
 
 
82
def patchUserDatabase(patch, user, uid, group, gid):
 
83
    """
 
84
    Patch L{pwd.getpwnam} so that it behaves as though only one user exists
 
85
    and patch L{grp.getgrnam} so that it behaves as though only one group
 
86
    exists.
 
87
 
 
88
    @param patch: A function like L{TestCase.patch} which will be used to
 
89
        install the fake implementations.
 
90
 
 
91
    @type user: C{str}
 
92
    @param user: The name of the single user which will exist.
 
93
 
 
94
    @type uid: C{int}
 
95
    @param uid: The UID of the single user which will exist.
 
96
 
 
97
    @type group: C{str}
 
98
    @param group: The name of the single user which will exist.
 
99
 
 
100
    @type gid: C{int}
 
101
    @param gid: The GID of the single group which will exist.
 
102
    """
 
103
    # Try not to be an unverified fake, but try not to depend on quirks of
 
104
    # the system either (eg, run as a process with a uid and gid which
 
105
    # equal each other, and so doesn't reliably test that uid is used where
 
106
    # uid should be used and gid is used where gid should be used). -exarkun
 
107
    pwent = pwd.getpwuid(os.getuid())
 
108
    grent = grp.getgrgid(os.getgid())
 
109
 
 
110
    database = UserDatabase()
 
111
    database.addUser(
 
112
        user, pwent.pw_passwd, uid, pwent.pw_gid,
 
113
        pwent.pw_gecos, pwent.pw_dir, pwent.pw_shell)
 
114
 
 
115
    def getgrnam(name):
 
116
        result = list(grent)
 
117
        result[result.index(grent.gr_name)] = group
 
118
        result[result.index(grent.gr_gid)] = gid
 
119
        result = tuple(result)
 
120
        return {group: result}[name]
 
121
 
 
122
    patch(pwd, "getpwnam", database.getpwnam)
 
123
    patch(grp, "getgrnam", getgrnam)
 
124
 
 
125
 
 
126
 
 
127
class MockServiceMaker(object):
 
128
    """
 
129
    A non-implementation of L{twisted.application.service.IServiceMaker}.
 
130
    """
 
131
    tapname = 'ueoa'
 
132
 
 
133
    def makeService(self, options):
 
134
        """
 
135
        Take a L{usage.Options} instance and return a
 
136
        L{service.IService} provider.
 
137
        """
 
138
        self.options = options
 
139
        self.service = service.Service()
 
140
        return self.service
 
141
 
 
142
 
 
143
 
 
144
class CrippledAppLogger(app.AppLogger):
 
145
    """
 
146
    @see: CrippledApplicationRunner.
 
147
    """
 
148
 
 
149
    def start(self, application):
 
150
        pass
 
151
 
 
152
 
 
153
 
 
154
class CrippledApplicationRunner(twistd._SomeApplicationRunner):
 
155
    """
 
156
    An application runner that cripples the platform-specific runner and
 
157
    nasty side-effect-having code so that we can use it without actually
 
158
    running any environment-affecting code.
 
159
    """
 
160
    loggerFactory = CrippledAppLogger
 
161
 
 
162
    def preApplication(self):
 
163
        pass
 
164
 
 
165
 
 
166
    def postApplication(self):
 
167
        pass
 
168
 
 
169
 
 
170
 
 
171
class ServerOptionsTests(unittest.TestCase):
 
172
    """
 
173
    Non-platform-specific tests for the pltaform-specific ServerOptions class.
 
174
    """
 
175
    def test_subCommands(self):
 
176
        """
 
177
        subCommands is built from IServiceMaker plugins, and is sorted
 
178
        alphabetically.
 
179
        """
 
180
        class FakePlugin(object):
 
181
            def __init__(self, name):
 
182
                self.tapname = name
 
183
                self._options = 'options for ' + name
 
184
                self.description = 'description of ' + name
 
185
 
 
186
            def options(self):
 
187
                return self._options
 
188
 
 
189
        apple = FakePlugin('apple')
 
190
        banana = FakePlugin('banana')
 
191
        coconut = FakePlugin('coconut')
 
192
        donut = FakePlugin('donut')
 
193
 
 
194
        def getPlugins(interface):
 
195
            self.assertEqual(interface, IServiceMaker)
 
196
            yield coconut
 
197
            yield banana
 
198
            yield donut
 
199
            yield apple
 
200
 
 
201
        config = twistd.ServerOptions()
 
202
        self.assertEqual(config._getPlugins, plugin.getPlugins)
 
203
        config._getPlugins = getPlugins
 
204
 
 
205
        # "subCommands is a list of 4-tuples of (command name, command
 
206
        # shortcut, parser class, documentation)."
 
207
        subCommands = config.subCommands
 
208
        expectedOrder = [apple, banana, coconut, donut]
 
209
 
 
210
        for subCommand, expectedCommand in zip(subCommands, expectedOrder):
 
211
            name, shortcut, parserClass, documentation = subCommand
 
212
            self.assertEqual(name, expectedCommand.tapname)
 
213
            self.assertEqual(shortcut, None)
 
214
            self.assertEqual(parserClass(), expectedCommand._options),
 
215
            self.assertEqual(documentation, expectedCommand.description)
 
216
 
 
217
 
 
218
    def test_sortedReactorHelp(self):
 
219
        """
 
220
        Reactor names are listed alphabetically by I{--help-reactors}.
 
221
        """
 
222
        class FakeReactorInstaller(object):
 
223
            def __init__(self, name):
 
224
                self.shortName = 'name of ' + name
 
225
                self.description = 'description of ' + name
 
226
 
 
227
        apple = FakeReactorInstaller('apple')
 
228
        banana = FakeReactorInstaller('banana')
 
229
        coconut = FakeReactorInstaller('coconut')
 
230
        donut = FakeReactorInstaller('donut')
 
231
 
 
232
        def getReactorTypes():
 
233
            yield coconut
 
234
            yield banana
 
235
            yield donut
 
236
            yield apple
 
237
 
 
238
        config = twistd.ServerOptions()
 
239
        self.assertEqual(config._getReactorTypes, reactors.getReactorTypes)
 
240
        config._getReactorTypes = getReactorTypes
 
241
        config.messageOutput = NativeStringIO()
 
242
 
 
243
        self.assertRaises(SystemExit, config.parseOptions, ['--help-reactors'])
 
244
        helpOutput = config.messageOutput.getvalue()
 
245
        indexes = []
 
246
        for reactor in apple, banana, coconut, donut:
 
247
            def getIndex(s):
 
248
                self.assertIn(s, helpOutput)
 
249
                indexes.append(helpOutput.index(s))
 
250
 
 
251
            getIndex(reactor.shortName)
 
252
            getIndex(reactor.description)
 
253
 
 
254
        self.assertEqual(
 
255
            indexes, sorted(indexes),
 
256
            'reactor descriptions were not in alphabetical order: %r' % (
 
257
                helpOutput,))
 
258
 
 
259
 
 
260
    def test_postOptionsSubCommandCausesNoSave(self):
 
261
        """
 
262
        postOptions should set no_save to True when a subcommand is used.
 
263
        """
 
264
        config = twistd.ServerOptions()
 
265
        config.subCommand = 'ueoa'
 
266
        config.postOptions()
 
267
        self.assertEqual(config['no_save'], True)
 
268
 
 
269
 
 
270
    def test_postOptionsNoSubCommandSavesAsUsual(self):
 
271
        """
 
272
        If no sub command is used, postOptions should not touch no_save.
 
273
        """
 
274
        config = twistd.ServerOptions()
 
275
        config.postOptions()
 
276
        self.assertEqual(config['no_save'], False)
 
277
 
 
278
 
 
279
    def test_listAllProfilers(self):
 
280
        """
 
281
        All the profilers that can be used in L{app.AppProfiler} are listed in
 
282
        the help output.
 
283
        """
 
284
        config = twistd.ServerOptions()
 
285
        helpOutput = str(config)
 
286
        for profiler in app.AppProfiler.profilers:
 
287
            self.assertIn(profiler, helpOutput)
 
288
 
 
289
 
 
290
    def test_defaultUmask(self):
 
291
        """
 
292
        The default value for the C{umask} option is C{None}.
 
293
        """
 
294
        config = twistd.ServerOptions()
 
295
        self.assertEqual(config['umask'], None)
 
296
 
 
297
 
 
298
    def test_umask(self):
 
299
        """
 
300
        The value given for the C{umask} option is parsed as an octal integer
 
301
        literal.
 
302
        """
 
303
        config = twistd.ServerOptions()
 
304
        config.parseOptions(['--umask', '123'])
 
305
        self.assertEqual(config['umask'], 83)
 
306
        config.parseOptions(['--umask', '0123'])
 
307
        self.assertEqual(config['umask'], 83)
 
308
 
 
309
 
 
310
    def test_invalidUmask(self):
 
311
        """
 
312
        If a value is given for the C{umask} option which cannot be parsed as
 
313
        an integer, L{UsageError} is raised by L{ServerOptions.parseOptions}.
 
314
        """
 
315
        config = twistd.ServerOptions()
 
316
        self.assertRaises(UsageError, config.parseOptions,
 
317
                          ['--umask', 'abcdef'])
 
318
 
 
319
    if _twistd_unix is None:
 
320
        msg = "twistd unix not available"
 
321
        test_defaultUmask.skip = test_umask.skip = test_invalidUmask.skip = msg
 
322
 
 
323
 
 
324
    def test_unimportableConfiguredLogObserver(self):
 
325
        """
 
326
        C{--logger} with an unimportable module raises a L{UsageError}.
 
327
        """
 
328
        config = twistd.ServerOptions()
 
329
        e = self.assertRaises(
 
330
            UsageError, config.parseOptions,
 
331
            ['--logger', 'no.such.module.I.hope'])
 
332
        self.assertTrue(
 
333
            e.args[0].startswith(
 
334
                "Logger 'no.such.module.I.hope' could not be imported: "
 
335
                "'no.such.module.I.hope' does not name an object"))
 
336
        self.assertNotIn('\n', e.args[0])
 
337
 
 
338
 
 
339
    def test_badAttributeWithConfiguredLogObserver(self):
 
340
        """
 
341
        C{--logger} with a non-existent object raises a L{UsageError}.
 
342
        """
 
343
        config = twistd.ServerOptions()
 
344
        e = self.assertRaises(UsageError, config.parseOptions,
 
345
                              ["--logger", "twisted.test.test_twistd.FOOBAR"])
 
346
        if sys.version_info <= (3, 5):
 
347
            self.assertTrue(
 
348
                e.args[0].startswith(
 
349
                    "Logger 'twisted.test.test_twistd.FOOBAR' could not be "
 
350
                    "imported: 'module' object has no attribute 'FOOBAR'"))
 
351
        else:
 
352
            self.assertTrue(
 
353
                e.args[0].startswith(
 
354
                    "Logger 'twisted.test.test_twistd.FOOBAR' could not be "
 
355
                    "imported: module 'twisted.test.test_twistd' "
 
356
                    "has no attribute 'FOOBAR'"))
 
357
        self.assertNotIn('\n', e.args[0])
 
358
 
 
359
 
 
360
 
 
361
class TapFileTests(unittest.TestCase):
 
362
    """
 
363
    Test twistd-related functionality that requires a tap file on disk.
 
364
    """
 
365
 
 
366
    def setUp(self):
 
367
        """
 
368
        Create a trivial Application and put it in a tap file on disk.
 
369
        """
 
370
        self.tapfile = self.mktemp()
 
371
        with open(self.tapfile, 'wb') as f:
 
372
            pickle.dump(service.Application("Hi!"), f)
 
373
 
 
374
 
 
375
    def test_createOrGetApplicationWithTapFile(self):
 
376
        """
 
377
        Ensure that the createOrGetApplication call that 'twistd -f foo.tap'
 
378
        makes will load the Application out of foo.tap.
 
379
        """
 
380
        config = twistd.ServerOptions()
 
381
        config.parseOptions(['-f', self.tapfile])
 
382
        application = CrippledApplicationRunner(
 
383
            config).createOrGetApplication()
 
384
        self.assertEqual(service.IService(application).name, 'Hi!')
 
385
 
 
386
 
 
387
 
 
388
class TestLoggerFactory(object):
 
389
    """
 
390
    A logger factory for L{TestApplicationRunner}.
 
391
    """
 
392
 
 
393
    def __init__(self, runner):
 
394
        self.runner = runner
 
395
 
 
396
 
 
397
    def start(self, application):
 
398
        """
 
399
        Save the logging start on the C{runner} instance.
 
400
        """
 
401
        self.runner.order.append("log")
 
402
        self.runner.hadApplicationLogObserver = hasattr(self.runner,
 
403
                                                        'application')
 
404
 
 
405
 
 
406
    def stop(self):
 
407
        """
 
408
        Don't log anything.
 
409
        """
 
410
 
 
411
 
 
412
 
 
413
class TestApplicationRunner(app.ApplicationRunner):
 
414
    """
 
415
    An ApplicationRunner which tracks the environment in which its methods are
 
416
    called.
 
417
    """
 
418
 
 
419
    def __init__(self, options):
 
420
        app.ApplicationRunner.__init__(self, options)
 
421
        self.order = []
 
422
        self.logger = TestLoggerFactory(self)
 
423
 
 
424
 
 
425
    def preApplication(self):
 
426
        self.order.append("pre")
 
427
        self.hadApplicationPreApplication = hasattr(self, 'application')
 
428
 
 
429
 
 
430
    def postApplication(self):
 
431
        self.order.append("post")
 
432
        self.hadApplicationPostApplication = hasattr(self, 'application')
 
433
 
 
434
 
 
435
 
 
436
class ApplicationRunnerTests(unittest.TestCase):
 
437
    """
 
438
    Non-platform-specific tests for the platform-specific ApplicationRunner.
 
439
    """
 
440
    def setUp(self):
 
441
        config = twistd.ServerOptions()
 
442
        self.serviceMaker = MockServiceMaker()
 
443
        # Set up a config object like it's been parsed with a subcommand
 
444
        config.loadedPlugins = {'test_command': self.serviceMaker}
 
445
        config.subOptions = object()
 
446
        config.subCommand = 'test_command'
 
447
        self.config = config
 
448
 
 
449
 
 
450
    def test_applicationRunnerGetsCorrectApplication(self):
 
451
        """
 
452
        Ensure that a twistd plugin gets used in appropriate ways: it
 
453
        is passed its Options instance, and the service it returns is
 
454
        added to the application.
 
455
        """
 
456
        arunner = CrippledApplicationRunner(self.config)
 
457
        arunner.run()
 
458
 
 
459
        self.assertIdentical(
 
460
            self.serviceMaker.options, self.config.subOptions,
 
461
            "ServiceMaker.makeService needs to be passed the correct "
 
462
            "sub Command object.")
 
463
        self.assertIdentical(
 
464
            self.serviceMaker.service,
 
465
            service.IService(arunner.application).services[0],
 
466
            "ServiceMaker.makeService's result needs to be set as a child "
 
467
            "of the Application.")
 
468
 
 
469
 
 
470
    def test_preAndPostApplication(self):
 
471
        """
 
472
        Test thet preApplication and postApplication methods are
 
473
        called by ApplicationRunner.run() when appropriate.
 
474
        """
 
475
        s = TestApplicationRunner(self.config)
 
476
        s.run()
 
477
        self.assertFalse(s.hadApplicationPreApplication)
 
478
        self.assertTrue(s.hadApplicationPostApplication)
 
479
        self.assertTrue(s.hadApplicationLogObserver)
 
480
        self.assertEqual(s.order, ["pre", "log", "post"])
 
481
 
 
482
 
 
483
    def _applicationStartsWithConfiguredID(self, argv, uid, gid):
 
484
        """
 
485
        Assert that given a particular command line, an application is started
 
486
        as a particular UID/GID.
 
487
 
 
488
        @param argv: A list of strings giving the options to parse.
 
489
        @param uid: An integer giving the expected UID.
 
490
        @param gid: An integer giving the expected GID.
 
491
        """
 
492
        self.config.parseOptions(argv)
 
493
 
 
494
        events = []
 
495
 
 
496
        class FakeUnixApplicationRunner(twistd._SomeApplicationRunner):
 
497
            def setupEnvironment(self, chroot, rundir, nodaemon, umask,
 
498
                                 pidfile):
 
499
                events.append('environment')
 
500
 
 
501
            def shedPrivileges(self, euid, uid, gid):
 
502
                events.append(('privileges', euid, uid, gid))
 
503
 
 
504
            def startReactor(self, reactor, oldstdout, oldstderr):
 
505
                events.append('reactor')
 
506
 
 
507
            def removePID(self, pidfile):
 
508
                pass
 
509
 
 
510
        @implementer(service.IService, service.IProcess)
 
511
        class FakeService(object):
 
512
 
 
513
            processName = None
 
514
            uid = None
 
515
            gid = None
 
516
 
 
517
            def setName(self, name):
 
518
                pass
 
519
 
 
520
            def setServiceParent(self, parent):
 
521
                pass
 
522
 
 
523
            def disownServiceParent(self):
 
524
                pass
 
525
 
 
526
            def privilegedStartService(self):
 
527
                events.append('privilegedStartService')
 
528
 
 
529
            def startService(self):
 
530
                events.append('startService')
 
531
 
 
532
            def stopService(self):
 
533
                pass
 
534
 
 
535
        application = FakeService()
 
536
        verifyObject(service.IService, application)
 
537
        verifyObject(service.IProcess, application)
 
538
 
 
539
        runner = FakeUnixApplicationRunner(self.config)
 
540
        runner.preApplication()
 
541
        runner.application = application
 
542
        runner.postApplication()
 
543
 
 
544
        self.assertEqual(
 
545
            events,
 
546
            ['environment', 'privilegedStartService',
 
547
             ('privileges', False, uid, gid), 'startService', 'reactor'])
 
548
 
 
549
 
 
550
    def test_applicationStartsWithConfiguredNumericIDs(self):
 
551
        """
 
552
        L{postApplication} should change the UID and GID to the values
 
553
        specified as numeric strings by the configuration after running
 
554
        L{service.IService.privilegedStartService} and before running
 
555
        L{service.IService.startService}.
 
556
        """
 
557
        uid = 1234
 
558
        gid = 4321
 
559
        self._applicationStartsWithConfiguredID(
 
560
            ["--uid", str(uid), "--gid", str(gid)], uid, gid)
 
561
    test_applicationStartsWithConfiguredNumericIDs.skip = setuidSkip
 
562
 
 
563
 
 
564
    def test_applicationStartsWithConfiguredNameIDs(self):
 
565
        """
 
566
        L{postApplication} should change the UID and GID to the values
 
567
        specified as user and group names by the configuration after running
 
568
        L{service.IService.privilegedStartService} and before running
 
569
        L{service.IService.startService}.
 
570
        """
 
571
        user = "foo"
 
572
        uid = 1234
 
573
        group = "bar"
 
574
        gid = 4321
 
575
        patchUserDatabase(self.patch, user, uid, group, gid)
 
576
        self._applicationStartsWithConfiguredID(
 
577
            ["--uid", user, "--gid", group], uid, gid)
 
578
    test_applicationStartsWithConfiguredNameIDs.skip = setuidSkip
 
579
 
 
580
 
 
581
    def test_startReactorRunsTheReactor(self):
 
582
        """
 
583
        L{startReactor} calls L{reactor.run}.
 
584
        """
 
585
        reactor = DummyReactor()
 
586
        runner = app.ApplicationRunner({
 
587
            "profile": False,
 
588
            "profiler": "profile",
 
589
            "debug": False})
 
590
        runner.startReactor(reactor, None, None)
 
591
        self.assertTrue(
 
592
            reactor.called, "startReactor did not call reactor.run()")
 
593
 
 
594
 
 
595
 
 
596
class UnixApplicationRunnerSetupEnvironmentTests(unittest.TestCase):
 
597
    """
 
598
    Tests for L{UnixApplicationRunner.setupEnvironment}.
 
599
 
 
600
    @ivar root: The root of the filesystem, or C{unset} if none has been
 
601
        specified with a call to L{os.chroot} (patched for this TestCase with
 
602
        L{UnixApplicationRunnerSetupEnvironmentTests.chroot}).
 
603
 
 
604
    @ivar cwd: The current working directory of the process, or C{unset} if
 
605
        none has been specified with a call to L{os.chdir} (patched for this
 
606
        TestCase with L{UnixApplicationRunnerSetupEnvironmentTests.chdir}).
 
607
 
 
608
    @ivar mask: The current file creation mask of the process, or C{unset} if
 
609
        none has been specified with a call to L{os.umask} (patched for this
 
610
        TestCase with L{UnixApplicationRunnerSetupEnvironmentTests.umask}).
 
611
 
 
612
    @ivar daemon: A boolean indicating whether daemonization has been performed
 
613
        by a call to L{_twistd_unix.daemonize} (patched for this TestCase with
 
614
        L{UnixApplicationRunnerSetupEnvironmentTests}.
 
615
    """
 
616
    if _twistd_unix is None:
 
617
        skip = "twistd unix not available"
 
618
 
 
619
    unset = object()
 
620
 
 
621
    def setUp(self):
 
622
        self.root = self.unset
 
623
        self.cwd = self.unset
 
624
        self.mask = self.unset
 
625
        self.daemon = False
 
626
        self.pid = os.getpid()
 
627
        self.patch(os, 'chroot', lambda path: setattr(self, 'root', path))
 
628
        self.patch(os, 'chdir', lambda path: setattr(self, 'cwd', path))
 
629
        self.patch(os, 'umask', lambda mask: setattr(self, 'mask', mask))
 
630
        self.runner = UnixApplicationRunner(twistd.ServerOptions())
 
631
        self.runner.daemonize = self.daemonize
 
632
 
 
633
 
 
634
    def daemonize(self, reactor):
 
635
        """
 
636
        Indicate that daemonization has happened and change the PID so that the
 
637
        value written to the pidfile can be tested in the daemonization case.
 
638
        """
 
639
        self.daemon = True
 
640
        self.patch(os, 'getpid', lambda: self.pid + 1)
 
641
 
 
642
 
 
643
    def test_chroot(self):
 
644
        """
 
645
        L{UnixApplicationRunner.setupEnvironment} changes the root of the
 
646
        filesystem if passed a non-C{None} value for the C{chroot} parameter.
 
647
        """
 
648
        self.runner.setupEnvironment("/foo/bar", ".", True, None, None)
 
649
        self.assertEqual(self.root, "/foo/bar")
 
650
 
 
651
 
 
652
    def test_noChroot(self):
 
653
        """
 
654
        L{UnixApplicationRunner.setupEnvironment} does not change the root of
 
655
        the filesystem if passed C{None} for the C{chroot} parameter.
 
656
        """
 
657
        self.runner.setupEnvironment(None, ".", True, None, None)
 
658
        self.assertIdentical(self.root, self.unset)
 
659
 
 
660
 
 
661
    def test_changeWorkingDirectory(self):
 
662
        """
 
663
        L{UnixApplicationRunner.setupEnvironment} changes the working directory
 
664
        of the process to the path given for the C{rundir} parameter.
 
665
        """
 
666
        self.runner.setupEnvironment(None, "/foo/bar", True, None, None)
 
667
        self.assertEqual(self.cwd, "/foo/bar")
 
668
 
 
669
 
 
670
    def test_daemonize(self):
 
671
        """
 
672
        L{UnixApplicationRunner.setupEnvironment} daemonizes the process if
 
673
        C{False} is passed for the C{nodaemon} parameter.
 
674
        """
 
675
        with AlternateReactor(FakeDaemonizingReactor()):
 
676
            self.runner.setupEnvironment(None, ".", False, None, None)
 
677
        self.assertTrue(self.daemon)
 
678
 
 
679
 
 
680
    def test_noDaemonize(self):
 
681
        """
 
682
        L{UnixApplicationRunner.setupEnvironment} does not daemonize the
 
683
        process if C{True} is passed for the C{nodaemon} parameter.
 
684
        """
 
685
        self.runner.setupEnvironment(None, ".", True, None, None)
 
686
        self.assertFalse(self.daemon)
 
687
 
 
688
 
 
689
    def test_nonDaemonPIDFile(self):
 
690
        """
 
691
        L{UnixApplicationRunner.setupEnvironment} writes the process's PID to
 
692
        the file specified by the C{pidfile} parameter.
 
693
        """
 
694
        pidfile = self.mktemp()
 
695
        self.runner.setupEnvironment(None, ".", True, None, pidfile)
 
696
        with open(pidfile, 'rb') as f:
 
697
            pid = int(f.read())
 
698
        self.assertEqual(pid, self.pid)
 
699
 
 
700
 
 
701
    def test_daemonPIDFile(self):
 
702
        """
 
703
        L{UnixApplicationRunner.setupEnvironment} writes the daemonized
 
704
        process's PID to the file specified by the C{pidfile} parameter if
 
705
        C{nodaemon} is C{False}.
 
706
        """
 
707
        pidfile = self.mktemp()
 
708
        with AlternateReactor(FakeDaemonizingReactor()):
 
709
            self.runner.setupEnvironment(None, ".", False, None, pidfile)
 
710
        with open(pidfile, 'rb') as f:
 
711
            pid = int(f.read())
 
712
        self.assertEqual(pid, self.pid + 1)
 
713
 
 
714
 
 
715
    def test_umask(self):
 
716
        """
 
717
        L{UnixApplicationRunner.setupEnvironment} changes the process umask to
 
718
        the value specified by the C{umask} parameter.
 
719
        """
 
720
        with AlternateReactor(FakeDaemonizingReactor()):
 
721
            self.runner.setupEnvironment(None, ".", False, 123, None)
 
722
        self.assertEqual(self.mask, 123)
 
723
 
 
724
 
 
725
    def test_noDaemonizeNoUmask(self):
 
726
        """
 
727
        L{UnixApplicationRunner.setupEnvironment} doesn't change the process
 
728
        umask if C{None} is passed for the C{umask} parameter and C{True} is
 
729
        passed for the C{nodaemon} parameter.
 
730
        """
 
731
        self.runner.setupEnvironment(None, ".", True, None, None)
 
732
        self.assertIdentical(self.mask, self.unset)
 
733
 
 
734
 
 
735
    def test_daemonizedNoUmask(self):
 
736
        """
 
737
        L{UnixApplicationRunner.setupEnvironment} changes the process umask to
 
738
        C{0077} if C{None} is passed for the C{umask} parameter and C{False} is
 
739
        passed for the C{nodaemon} parameter.
 
740
        """
 
741
        with AlternateReactor(FakeDaemonizingReactor()):
 
742
            self.runner.setupEnvironment(None, ".", False, None, None)
 
743
        self.assertEqual(self.mask, 0o077)
 
744
 
 
745
 
 
746
 
 
747
class UnixApplicationRunnerStartApplicationTests(unittest.TestCase):
 
748
    """
 
749
    Tests for L{UnixApplicationRunner.startApplication}.
 
750
    """
 
751
    if _twistd_unix is None:
 
752
        skip = "twistd unix not available"
 
753
 
 
754
 
 
755
    def test_setupEnvironment(self):
 
756
        """
 
757
        L{UnixApplicationRunner.startApplication} calls
 
758
        L{UnixApplicationRunner.setupEnvironment} with the chroot, rundir,
 
759
        nodaemon, umask, and pidfile parameters from the configuration it is
 
760
        constructed with.
 
761
        """
 
762
        options = twistd.ServerOptions()
 
763
        options.parseOptions([
 
764
            '--nodaemon',
 
765
            '--umask', '0070',
 
766
            '--chroot', '/foo/chroot',
 
767
            '--rundir', '/foo/rundir',
 
768
            '--pidfile', '/foo/pidfile'])
 
769
        application = service.Application("test_setupEnvironment")
 
770
        self.runner = UnixApplicationRunner(options)
 
771
 
 
772
        args = []
 
773
        def fakeSetupEnvironment(self, chroot, rundir, nodaemon, umask,
 
774
                                 pidfile):
 
775
            args.extend((chroot, rundir, nodaemon, umask, pidfile))
 
776
 
 
777
        # Sanity check
 
778
        self.assertEqual(
 
779
            inspect.getargspec(self.runner.setupEnvironment),
 
780
            inspect.getargspec(fakeSetupEnvironment))
 
781
 
 
782
        self.patch(UnixApplicationRunner, 'setupEnvironment',
 
783
                   fakeSetupEnvironment)
 
784
        self.patch(UnixApplicationRunner, 'shedPrivileges',
 
785
                   lambda *a, **kw: None)
 
786
        self.patch(app, 'startApplication', lambda *a, **kw: None)
 
787
        self.runner.startApplication(application)
 
788
 
 
789
        self.assertEqual(
 
790
            args,
 
791
            ['/foo/chroot', '/foo/rundir', True, 56, '/foo/pidfile'])
 
792
 
 
793
 
 
794
 
 
795
class UnixApplicationRunnerRemovePIDTests(unittest.TestCase):
 
796
    """
 
797
    Tests for L{UnixApplicationRunner.removePID}.
 
798
    """
 
799
    if _twistd_unix is None:
 
800
        skip = "twistd unix not available"
 
801
 
 
802
 
 
803
    def test_removePID(self):
 
804
        """
 
805
        L{UnixApplicationRunner.removePID} deletes the file the name of
 
806
        which is passed to it.
 
807
        """
 
808
        runner = UnixApplicationRunner({})
 
809
        path = self.mktemp()
 
810
        os.makedirs(path)
 
811
        pidfile = os.path.join(path, "foo.pid")
 
812
        open(pidfile, "w").close()
 
813
        runner.removePID(pidfile)
 
814
        self.assertFalse(os.path.exists(pidfile))
 
815
 
 
816
 
 
817
    def test_removePIDErrors(self):
 
818
        """
 
819
        Calling L{UnixApplicationRunner.removePID} with a non-existent filename
 
820
        logs an OSError.
 
821
        """
 
822
        runner = UnixApplicationRunner({})
 
823
        runner.removePID("fakepid")
 
824
        errors = self.flushLoggedErrors(OSError)
 
825
        self.assertEqual(len(errors), 1)
 
826
        self.assertEqual(errors[0].value.errno, errno.ENOENT)
 
827
 
 
828
 
 
829
 
 
830
class FakeNonDaemonizingReactor(object):
 
831
    """
 
832
    A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize}
 
833
    methods, but not announcing this, and logging whether the methods have been
 
834
    called.
 
835
 
 
836
    @ivar _beforeDaemonizeCalled: if C{beforeDaemonize} has been called or not.
 
837
    @type _beforeDaemonizeCalled: C{bool}
 
838
    @ivar _afterDaemonizeCalled: if C{afterDaemonize} has been called or not.
 
839
    @type _afterDaemonizeCalled: C{bool}
 
840
    """
 
841
 
 
842
    def __init__(self):
 
843
        self._beforeDaemonizeCalled = False
 
844
        self._afterDaemonizeCalled = False
 
845
 
 
846
 
 
847
    def beforeDaemonize(self):
 
848
        self._beforeDaemonizeCalled = True
 
849
 
 
850
 
 
851
    def afterDaemonize(self):
 
852
        self._afterDaemonizeCalled = True
 
853
 
 
854
 
 
855
    def addSystemEventTrigger(self, *args, **kw):
 
856
        """
 
857
        Skip event registration.
 
858
        """
 
859
 
 
860
 
 
861
 
 
862
@implementer(IReactorDaemonize)
 
863
class FakeDaemonizingReactor(FakeNonDaemonizingReactor):
 
864
    """
 
865
    A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize}
 
866
    methods, announcing this, and logging whether the methods have been called.
 
867
    """
 
868
 
 
869
 
 
870
 
 
871
class DummyReactor(object):
 
872
    """
 
873
    A dummy reactor, only providing a C{run} method and checking that it
 
874
    has been called.
 
875
 
 
876
    @ivar called: if C{run} has been called or not.
 
877
    @type called: C{bool}
 
878
    """
 
879
    called = False
 
880
 
 
881
    def run(self):
 
882
        """
 
883
        A fake run method, checking that it's been called one and only time.
 
884
        """
 
885
        if self.called:
 
886
            raise RuntimeError("Already called")
 
887
        self.called = True
 
888
 
 
889
 
 
890
 
 
891
class AppProfilingTests(unittest.TestCase):
 
892
    """
 
893
    Tests for L{app.AppProfiler}.
 
894
    """
 
895
 
 
896
    def test_profile(self):
 
897
        """
 
898
        L{app.ProfileRunner.run} should call the C{run} method of the reactor
 
899
        and save profile data in the specified file.
 
900
        """
 
901
        config = twistd.ServerOptions()
 
902
        config["profile"] = self.mktemp()
 
903
        config["profiler"] = "profile"
 
904
        profiler = app.AppProfiler(config)
 
905
        reactor = DummyReactor()
 
906
 
 
907
        profiler.run(reactor)
 
908
 
 
909
        self.assertTrue(reactor.called)
 
910
        with open(config["profile"]) as f:
 
911
            data = f.read()
 
912
        self.assertIn("DummyReactor.run", data)
 
913
        self.assertIn("function calls", data)
 
914
 
 
915
    if profile is None:
 
916
        test_profile.skip = "profile module not available"
 
917
 
 
918
 
 
919
    def _testStats(self, statsClass, profile):
 
920
        out = NativeStringIO()
 
921
 
 
922
        # Patch before creating the pstats, because pstats binds self.stream to
 
923
        # sys.stdout early in 2.5 and newer.
 
924
        stdout = self.patch(sys, 'stdout', out)
 
925
 
 
926
        # If pstats.Stats can load the data and then reformat it, then the
 
927
        # right thing probably happened.
 
928
        stats = statsClass(profile)
 
929
        stats.print_stats()
 
930
        stdout.restore()
 
931
 
 
932
        data = out.getvalue()
 
933
        self.assertIn("function calls", data)
 
934
        self.assertIn("(run)", data)
 
935
 
 
936
 
 
937
    def test_profileSaveStats(self):
 
938
        """
 
939
        With the C{savestats} option specified, L{app.ProfileRunner.run}
 
940
        should save the raw stats object instead of a summary output.
 
941
        """
 
942
        config = twistd.ServerOptions()
 
943
        config["profile"] = self.mktemp()
 
944
        config["profiler"] = "profile"
 
945
        config["savestats"] = True
 
946
        profiler = app.AppProfiler(config)
 
947
        reactor = DummyReactor()
 
948
 
 
949
        profiler.run(reactor)
 
950
 
 
951
        self.assertTrue(reactor.called)
 
952
        self._testStats(pstats.Stats, config['profile'])
 
953
 
 
954
    if profile is None:
 
955
        test_profileSaveStats.skip = "profile module not available"
 
956
 
 
957
 
 
958
    def test_withoutProfile(self):
 
959
        """
 
960
        When the C{profile} module is not present, L{app.ProfilerRunner.run}
 
961
        should raise a C{SystemExit} exception.
 
962
        """
 
963
        savedModules = sys.modules.copy()
 
964
 
 
965
        config = twistd.ServerOptions()
 
966
        config["profiler"] = "profile"
 
967
        profiler = app.AppProfiler(config)
 
968
 
 
969
        sys.modules["profile"] = None
 
970
        try:
 
971
            self.assertRaises(SystemExit, profiler.run, None)
 
972
        finally:
 
973
            sys.modules.clear()
 
974
            sys.modules.update(savedModules)
 
975
 
 
976
 
 
977
    def test_profilePrintStatsError(self):
 
978
        """
 
979
        When an error happens during the print of the stats, C{sys.stdout}
 
980
        should be restored to its initial value.
 
981
        """
 
982
        class ErroneousProfile(profile.Profile):
 
983
            def print_stats(self):
 
984
                raise RuntimeError("Boom")
 
985
        self.patch(profile, "Profile", ErroneousProfile)
 
986
 
 
987
        config = twistd.ServerOptions()
 
988
        config["profile"] = self.mktemp()
 
989
        config["profiler"] = "profile"
 
990
        profiler = app.AppProfiler(config)
 
991
        reactor = DummyReactor()
 
992
 
 
993
        oldStdout = sys.stdout
 
994
        self.assertRaises(RuntimeError, profiler.run, reactor)
 
995
        self.assertIdentical(sys.stdout, oldStdout)
 
996
 
 
997
    if profile is None:
 
998
        test_profilePrintStatsError.skip = "profile module not available"
 
999
 
 
1000
 
 
1001
    def test_cProfile(self):
 
1002
        """
 
1003
        L{app.CProfileRunner.run} should call the C{run} method of the
 
1004
        reactor and save profile data in the specified file.
 
1005
        """
 
1006
        config = twistd.ServerOptions()
 
1007
        config["profile"] = self.mktemp()
 
1008
        config["profiler"] = "cProfile"
 
1009
        profiler = app.AppProfiler(config)
 
1010
        reactor = DummyReactor()
 
1011
 
 
1012
        profiler.run(reactor)
 
1013
 
 
1014
        self.assertTrue(reactor.called)
 
1015
        with open(config["profile"]) as f:
 
1016
            data = f.read()
 
1017
        self.assertIn("run", data)
 
1018
        self.assertIn("function calls", data)
 
1019
 
 
1020
    if cProfile is None:
 
1021
        test_cProfile.skip = "cProfile module not available"
 
1022
 
 
1023
 
 
1024
    def test_cProfileSaveStats(self):
 
1025
        """
 
1026
        With the C{savestats} option specified,
 
1027
        L{app.CProfileRunner.run} should save the raw stats object
 
1028
        instead of a summary output.
 
1029
        """
 
1030
        config = twistd.ServerOptions()
 
1031
        config["profile"] = self.mktemp()
 
1032
        config["profiler"] = "cProfile"
 
1033
        config["savestats"] = True
 
1034
        profiler = app.AppProfiler(config)
 
1035
        reactor = DummyReactor()
 
1036
 
 
1037
        profiler.run(reactor)
 
1038
 
 
1039
        self.assertTrue(reactor.called)
 
1040
        self._testStats(pstats.Stats, config['profile'])
 
1041
 
 
1042
    if cProfile is None:
 
1043
        test_cProfileSaveStats.skip = "cProfile module not available"
 
1044
 
 
1045
 
 
1046
    def test_withoutCProfile(self):
 
1047
        """
 
1048
        When the C{cProfile} module is not present,
 
1049
        L{app.CProfileRunner.run} should raise a C{SystemExit}
 
1050
        exception and log the C{ImportError}.
 
1051
        """
 
1052
        savedModules = sys.modules.copy()
 
1053
        sys.modules["cProfile"] = None
 
1054
 
 
1055
        config = twistd.ServerOptions()
 
1056
        config["profiler"] = "cProfile"
 
1057
        profiler = app.AppProfiler(config)
 
1058
        try:
 
1059
            self.assertRaises(SystemExit, profiler.run, None)
 
1060
        finally:
 
1061
            sys.modules.clear()
 
1062
            sys.modules.update(savedModules)
 
1063
 
 
1064
 
 
1065
    def test_unknownProfiler(self):
 
1066
        """
 
1067
        Check that L{app.AppProfiler} raises L{SystemExit} when given an
 
1068
        unknown profiler name.
 
1069
        """
 
1070
        config = twistd.ServerOptions()
 
1071
        config["profile"] = self.mktemp()
 
1072
        config["profiler"] = "foobar"
 
1073
 
 
1074
        error = self.assertRaises(SystemExit, app.AppProfiler, config)
 
1075
        self.assertEqual(str(error), "Unsupported profiler name: foobar")
 
1076
 
 
1077
 
 
1078
    def test_defaultProfiler(self):
 
1079
        """
 
1080
        L{app.Profiler} defaults to the cprofile profiler if not specified.
 
1081
        """
 
1082
        profiler = app.AppProfiler({})
 
1083
        self.assertEqual(profiler.profiler, "cprofile")
 
1084
 
 
1085
 
 
1086
    def test_profilerNameCaseInsentive(self):
 
1087
        """
 
1088
        The case of the profiler name passed to L{app.AppProfiler} is not
 
1089
        relevant.
 
1090
        """
 
1091
        profiler = app.AppProfiler({"profiler": "CprOfile"})
 
1092
        self.assertEqual(profiler.profiler, "cprofile")
 
1093
 
 
1094
 
 
1095
 
 
1096
def _patchTextFileLogObserver(patch):
 
1097
    """
 
1098
    Patch L{logger.textFileLogObserver} to record every call and keep a
 
1099
    reference to the passed log file for tests.
 
1100
 
 
1101
    @param patch: a callback for patching (usually L{unittest.TestCase.patch}).
 
1102
 
 
1103
    @return: the list that keeps track of the log files.
 
1104
    @rtype: C{list}
 
1105
    """
 
1106
    logFiles = []
 
1107
    oldFileLogObserver = logger.textFileLogObserver
 
1108
 
 
1109
    def observer(logFile, *args, **kwargs):
 
1110
        logFiles.append(logFile)
 
1111
        return oldFileLogObserver(logFile, *args, **kwargs)
 
1112
 
 
1113
    patch(logger, 'textFileLogObserver', observer)
 
1114
    return logFiles
 
1115
 
 
1116
 
 
1117
 
 
1118
def _setupSyslog(testCase):
 
1119
    """
 
1120
    Make fake syslog, and return list to which prefix and then log
 
1121
    messages will be appended if it is used.
 
1122
    """
 
1123
    logMessages = []
 
1124
 
 
1125
    class fakesyslogobserver(object):
 
1126
        def __init__(self, prefix):
 
1127
            logMessages.append(prefix)
 
1128
 
 
1129
        def emit(self, eventDict):
 
1130
            logMessages.append(eventDict)
 
1131
 
 
1132
    testCase.patch(syslog, "SyslogObserver", fakesyslogobserver)
 
1133
    return logMessages
 
1134
 
 
1135
 
 
1136
 
 
1137
class AppLoggerTests(unittest.TestCase):
 
1138
    """
 
1139
    Tests for L{app.AppLogger}.
 
1140
 
 
1141
    @ivar observers: list of observers installed during the tests.
 
1142
    @type observers: C{list}
 
1143
    """
 
1144
 
 
1145
    def setUp(self):
 
1146
        """
 
1147
        Override L{globaLogBeginner.beginLoggingTo} so that we can trace the
 
1148
        observers installed in C{self.observers}.
 
1149
        """
 
1150
        self.observers = []
 
1151
 
 
1152
        def beginLoggingTo(observers):
 
1153
            for observer in observers:
 
1154
                self.observers.append(observer)
 
1155
                globalLogPublisher.addObserver(observer)
 
1156
 
 
1157
        self.patch(globalLogBeginner, 'beginLoggingTo', beginLoggingTo)
 
1158
 
 
1159
 
 
1160
    def tearDown(self):
 
1161
        """
 
1162
        Remove all installed observers.
 
1163
        """
 
1164
        for observer in self.observers:
 
1165
            globalLogPublisher.removeObserver(observer)
 
1166
 
 
1167
 
 
1168
    def _makeObserver(self):
 
1169
        """
 
1170
        Make a new observer which captures all logs sent to it.
 
1171
 
 
1172
        @return: An observer that stores all logs sent to it.
 
1173
        @rtype: Callable that implements L{ILogObserver}.
 
1174
        """
 
1175
        @implementer(ILogObserver)
 
1176
        class TestObserver(object):
 
1177
            _logs = []
 
1178
 
 
1179
            def __call__(self, event):
 
1180
                self._logs.append(event)
 
1181
 
 
1182
        return TestObserver()
 
1183
 
 
1184
 
 
1185
    def _checkObserver(self, observer):
 
1186
        """
 
1187
        Ensure that initial C{twistd} logs are written to logs.
 
1188
 
 
1189
        @param observer: The observer made by L{self._makeObserver).
 
1190
        """
 
1191
        self.assertEqual(self.observers, [observer])
 
1192
        self.assertIn("starting up", observer._logs[0]["log_format"])
 
1193
        self.assertIn("reactor class", observer._logs[1]["log_format"])
 
1194
 
 
1195
 
 
1196
    def test_start(self):
 
1197
        """
 
1198
        L{app.AppLogger.start} calls L{globalLogBeginner.addObserver}, and then
 
1199
        writes some messages about twistd and the reactor.
 
1200
        """
 
1201
        logger = app.AppLogger({})
 
1202
        observer = self._makeObserver()
 
1203
        logger._getLogObserver = lambda: observer
 
1204
        logger.start(Componentized())
 
1205
        self._checkObserver(observer)
 
1206
 
 
1207
 
 
1208
    def test_startUsesApplicationLogObserver(self):
 
1209
        """
 
1210
        When the L{ILogObserver} component is available on the application,
 
1211
        that object will be used as the log observer instead of constructing a
 
1212
        new one.
 
1213
        """
 
1214
        application = Componentized()
 
1215
        observer = self._makeObserver()
 
1216
        application.setComponent(ILogObserver, observer)
 
1217
        logger = app.AppLogger({})
 
1218
        logger.start(application)
 
1219
        self._checkObserver(observer)
 
1220
 
 
1221
 
 
1222
    def _setupConfiguredLogger(self, application, extraLogArgs={},
 
1223
                               appLogger=app.AppLogger):
 
1224
        """
 
1225
        Set up an AppLogger which exercises the C{logger} configuration option.
 
1226
 
 
1227
        @type application: L{Componentized}
 
1228
        @param application: The L{Application} object to pass to
 
1229
            L{app.AppLogger.start}.
 
1230
        @type extraLogArgs: C{dict}
 
1231
        @param extraLogArgs: extra values to pass to AppLogger.
 
1232
        @type appLogger: L{AppLogger} class, or a subclass
 
1233
        @param appLogger: factory for L{AppLogger} instances.
 
1234
 
 
1235
        @rtype: C{list}
 
1236
        @return: The logs accumulated by the log observer.
 
1237
        """
 
1238
        observer = self._makeObserver()
 
1239
        logArgs = {"logger": lambda: observer}
 
1240
        logArgs.update(extraLogArgs)
 
1241
        logger = appLogger(logArgs)
 
1242
        logger.start(application)
 
1243
        return observer
 
1244
 
 
1245
 
 
1246
    def test_startUsesConfiguredLogObserver(self):
 
1247
        """
 
1248
        When the C{logger} key is specified in the configuration dictionary
 
1249
        (i.e., when C{--logger} is passed to twistd), the initial log observer
 
1250
        will be the log observer returned from the callable which the value
 
1251
        refers to in FQPN form.
 
1252
        """
 
1253
        application = Componentized()
 
1254
        self._checkObserver(self._setupConfiguredLogger(application))
 
1255
 
 
1256
 
 
1257
    def test_configuredLogObserverBeatsComponent(self):
 
1258
        """
 
1259
        C{--logger} takes precedence over a L{ILogObserver} component set on
 
1260
        Application.
 
1261
        """
 
1262
        observer = self._makeObserver()
 
1263
        application = Componentized()
 
1264
        application.setComponent(ILogObserver, observer)
 
1265
        self._checkObserver(self._setupConfiguredLogger(application))
 
1266
        self.assertEqual(observer._logs, [])
 
1267
 
 
1268
 
 
1269
    def test_configuredLogObserverBeatsLegacyComponent(self):
 
1270
        """
 
1271
        C{--logger} takes precedence over a L{LegacyILogObserver} component
 
1272
        set on Application.
 
1273
        """
 
1274
        nonlogs = []
 
1275
        application = Componentized()
 
1276
        application.setComponent(LegacyILogObserver, nonlogs.append)
 
1277
        self._checkObserver(self._setupConfiguredLogger(application))
 
1278
        self.assertEqual(nonlogs, [])
 
1279
 
 
1280
 
 
1281
    def test_loggerComponentBeatsLegacyLoggerComponent(self):
 
1282
        """
 
1283
        A L{ILogObserver} takes precedence over a L{LegacyILogObserver}
 
1284
        component set on Application.
 
1285
        """
 
1286
        nonlogs = []
 
1287
        observer = self._makeObserver()
 
1288
        application = Componentized()
 
1289
        application.setComponent(ILogObserver, observer)
 
1290
        application.setComponent(LegacyILogObserver, nonlogs.append)
 
1291
 
 
1292
        logger = app.AppLogger({})
 
1293
        logger.start(application)
 
1294
 
 
1295
        self._checkObserver(observer)
 
1296
        self.assertEqual(nonlogs, [])
 
1297
 
 
1298
 
 
1299
    def test_configuredLogObserverBeatsSyslog(self):
 
1300
        """
 
1301
        C{--logger} takes precedence over a C{--syslog} command line
 
1302
        argument.
 
1303
        """
 
1304
        logs = _setupSyslog(self)
 
1305
        application = Componentized()
 
1306
        self._checkObserver(self._setupConfiguredLogger(application,
 
1307
                                                        {"syslog": True},
 
1308
                                                        UnixAppLogger))
 
1309
        self.assertEqual(logs, [])
 
1310
 
 
1311
    if _twistd_unix is None or syslog is None:
 
1312
        test_configuredLogObserverBeatsSyslog.skip = (
 
1313
            "Not on POSIX, or syslog not available."
 
1314
        )
 
1315
 
 
1316
 
 
1317
    def test_configuredLogObserverBeatsLogfile(self):
 
1318
        """
 
1319
        C{--logger} takes precedence over a C{--logfile} command line
 
1320
        argument.
 
1321
        """
 
1322
        application = Componentized()
 
1323
        path = self.mktemp()
 
1324
        self._checkObserver(self._setupConfiguredLogger(application,
 
1325
                                                        {"logfile": "path"}))
 
1326
        self.assertFalse(os.path.exists(path))
 
1327
 
 
1328
 
 
1329
    def test_getLogObserverStdout(self):
 
1330
        """
 
1331
        When logfile is empty or set to C{-}, L{app.AppLogger._getLogObserver}
 
1332
        returns a log observer pointing at C{sys.stdout}.
 
1333
        """
 
1334
        logger = app.AppLogger({"logfile": "-"})
 
1335
        logFiles = _patchTextFileLogObserver(self.patch)
 
1336
 
 
1337
        logger._getLogObserver()
 
1338
 
 
1339
        self.assertEqual(len(logFiles), 1)
 
1340
        self.assertIdentical(logFiles[0], sys.stdout)
 
1341
 
 
1342
        logger = app.AppLogger({"logfile": ""})
 
1343
        logger._getLogObserver()
 
1344
 
 
1345
        self.assertEqual(len(logFiles), 2)
 
1346
        self.assertIdentical(logFiles[1], sys.stdout)
 
1347
 
 
1348
 
 
1349
    def test_getLogObserverFile(self):
 
1350
        """
 
1351
        When passing the C{logfile} option, L{app.AppLogger._getLogObserver}
 
1352
        returns a log observer pointing at the specified path.
 
1353
        """
 
1354
        logFiles = _patchTextFileLogObserver(self.patch)
 
1355
        filename = self.mktemp()
 
1356
        logger = app.AppLogger({"logfile": filename})
 
1357
 
 
1358
        logger._getLogObserver()
 
1359
 
 
1360
        self.assertEqual(len(logFiles), 1)
 
1361
        self.assertEqual(logFiles[0].path,
 
1362
                         os.path.abspath(filename))
 
1363
 
 
1364
 
 
1365
    def test_stop(self):
 
1366
        """
 
1367
        L{app.AppLogger.stop} removes the observer created in C{start}, and
 
1368
        reinitialize its C{_observer} so that if C{stop} is called several
 
1369
        times it doesn't break.
 
1370
        """
 
1371
        removed = []
 
1372
        observer = object()
 
1373
 
 
1374
        def remove(observer):
 
1375
            removed.append(observer)
 
1376
 
 
1377
        self.patch(globalLogPublisher, 'removeObserver', remove)
 
1378
        logger = app.AppLogger({})
 
1379
        logger._observer = observer
 
1380
        logger.stop()
 
1381
        self.assertEqual(removed, [observer])
 
1382
        logger.stop()
 
1383
        self.assertEqual(removed, [observer])
 
1384
        self.assertIdentical(logger._observer, None)
 
1385
 
 
1386
 
 
1387
    def test_legacyObservers(self):
 
1388
        """
 
1389
        L{app.AppLogger} using a legacy logger observer still works, wrapping
 
1390
        it in a compat shim.
 
1391
        """
 
1392
        logs = []
 
1393
        logger = app.AppLogger({})
 
1394
 
 
1395
        @implementer(LegacyILogObserver)
 
1396
        class LoggerObserver(object):
 
1397
            """
 
1398
            An observer which implements the legacy L{LegacyILogObserver}.
 
1399
            """
 
1400
            def __call__(self, x):
 
1401
                """
 
1402
                Add C{x} to the logs list.
 
1403
                """
 
1404
                logs.append(x)
 
1405
 
 
1406
        logger._observerFactory = lambda: LoggerObserver()
 
1407
        logger.start(Componentized())
 
1408
 
 
1409
        self.assertIn("starting up", textFromEventDict(logs[0]))
 
1410
        warnings = self.flushWarnings(
 
1411
            [self.test_legacyObservers])
 
1412
        self.assertEqual(len(warnings), 0)
 
1413
 
 
1414
 
 
1415
    def test_unmarkedObserversDeprecated(self):
 
1416
        """
 
1417
        L{app.AppLogger} using a logger observer which does not implement
 
1418
        L{ILogObserver} or L{LegacyILogObserver} will be wrapped in a compat
 
1419
        shim and raise a L{DeprecationWarning}.
 
1420
        """
 
1421
        logs = []
 
1422
        logger = app.AppLogger({})
 
1423
        logger._getLogObserver = lambda: logs.append
 
1424
        logger.start(Componentized())
 
1425
 
 
1426
        self.assertIn("starting up", textFromEventDict(logs[0]))
 
1427
 
 
1428
        warnings = self.flushWarnings(
 
1429
            [self.test_unmarkedObserversDeprecated])
 
1430
        self.assertEqual(len(warnings), 1)
 
1431
        self.assertEqual(warnings[0]["message"],
 
1432
                         ("Passing a logger factory which makes log observers "
 
1433
                          "which do not implement twisted.logger.ILogObserver "
 
1434
                          "or twisted.python.log.ILogObserver to "
 
1435
                          "twisted.application.app.AppLogger was deprecated "
 
1436
                          "in Twisted 16.2. Please use a factory that "
 
1437
                          "produces twisted.logger.ILogObserver (or the "
 
1438
                          "legacy twisted.python.log.ILogObserver) "
 
1439
                          "implementing objects instead."))
 
1440
 
 
1441
 
 
1442
 
 
1443
class UnixAppLoggerTests(unittest.TestCase):
 
1444
    """
 
1445
    Tests for L{UnixAppLogger}.
 
1446
 
 
1447
    @ivar signals: list of signal handlers installed.
 
1448
    @type signals: C{list}
 
1449
    """
 
1450
    if _twistd_unix is None:
 
1451
        skip = "twistd unix not available"
 
1452
 
 
1453
 
 
1454
    def setUp(self):
 
1455
        """
 
1456
        Fake C{signal.signal} for not installing the handlers but saving them
 
1457
        in C{self.signals}.
 
1458
        """
 
1459
        self.signals = []
 
1460
 
 
1461
        def fakeSignal(sig, f):
 
1462
            self.signals.append((sig, f))
 
1463
 
 
1464
        self.patch(signal, "signal", fakeSignal)
 
1465
 
 
1466
 
 
1467
    def test_getLogObserverStdout(self):
 
1468
        """
 
1469
        When non-daemonized and C{logfile} is empty or set to C{-},
 
1470
        L{UnixAppLogger._getLogObserver} returns a log observer pointing at
 
1471
        C{sys.stdout}.
 
1472
        """
 
1473
        logFiles = _patchTextFileLogObserver(self.patch)
 
1474
 
 
1475
        logger = UnixAppLogger({"logfile": "-", "nodaemon": True})
 
1476
        logger._getLogObserver()
 
1477
        self.assertEqual(len(logFiles), 1)
 
1478
        self.assertIdentical(logFiles[0], sys.stdout)
 
1479
 
 
1480
        logger = UnixAppLogger({"logfile": "", "nodaemon": True})
 
1481
        logger._getLogObserver()
 
1482
        self.assertEqual(len(logFiles), 2)
 
1483
        self.assertIdentical(logFiles[1], sys.stdout)
 
1484
 
 
1485
 
 
1486
    def test_getLogObserverStdoutDaemon(self):
 
1487
        """
 
1488
        When daemonized and C{logfile} is set to C{-},
 
1489
        L{UnixAppLogger._getLogObserver} raises C{SystemExit}.
 
1490
        """
 
1491
        logger = UnixAppLogger({"logfile": "-", "nodaemon": False})
 
1492
        error = self.assertRaises(SystemExit, logger._getLogObserver)
 
1493
        self.assertEqual(str(error), "Daemons cannot log to stdout, exiting!")
 
1494
 
 
1495
 
 
1496
    def test_getLogObserverFile(self):
 
1497
        """
 
1498
        When C{logfile} contains a file name, L{app.AppLogger._getLogObserver}
 
1499
        returns a log observer pointing at the specified path, and a signal
 
1500
        handler rotating the log is installed.
 
1501
        """
 
1502
        logFiles = _patchTextFileLogObserver(self.patch)
 
1503
        filename = self.mktemp()
 
1504
        logger = UnixAppLogger({"logfile": filename})
 
1505
        logger._getLogObserver()
 
1506
 
 
1507
        self.assertEqual(len(logFiles), 1)
 
1508
        self.assertEqual(logFiles[0].path, os.path.abspath(filename))
 
1509
 
 
1510
        self.assertEqual(len(self.signals), 1)
 
1511
        self.assertEqual(self.signals[0][0], signal.SIGUSR1)
 
1512
 
 
1513
        d = Deferred()
 
1514
 
 
1515
        def rotate():
 
1516
            d.callback(None)
 
1517
 
 
1518
        logFiles[0].rotate = rotate
 
1519
 
 
1520
        rotateLog = self.signals[0][1]
 
1521
        rotateLog(None, None)
 
1522
        return d
 
1523
 
 
1524
 
 
1525
    def test_getLogObserverDontOverrideSignalHandler(self):
 
1526
        """
 
1527
        If a signal handler is already installed,
 
1528
        L{UnixAppLogger._getLogObserver} doesn't override it.
 
1529
        """
 
1530
        def fakeGetSignal(sig):
 
1531
            self.assertEqual(sig, signal.SIGUSR1)
 
1532
            return object()
 
1533
        self.patch(signal, "getsignal", fakeGetSignal)
 
1534
        filename = self.mktemp()
 
1535
        logger = UnixAppLogger({"logfile": filename})
 
1536
        logger._getLogObserver()
 
1537
 
 
1538
        self.assertEqual(self.signals, [])
 
1539
 
 
1540
 
 
1541
    def test_getLogObserverDefaultFile(self):
 
1542
        """
 
1543
        When daemonized and C{logfile} is empty, the observer returned by
 
1544
        L{UnixAppLogger._getLogObserver} points at C{twistd.log} in the current
 
1545
        directory.
 
1546
        """
 
1547
        logFiles = _patchTextFileLogObserver(self.patch)
 
1548
        logger = UnixAppLogger({"logfile": "", "nodaemon": False})
 
1549
        logger._getLogObserver()
 
1550
 
 
1551
        self.assertEqual(len(logFiles), 1)
 
1552
        self.assertEqual(logFiles[0].path, os.path.abspath("twistd.log"))
 
1553
 
 
1554
 
 
1555
    def test_getLogObserverSyslog(self):
 
1556
        """
 
1557
        If C{syslog} is set to C{True}, L{UnixAppLogger._getLogObserver} starts
 
1558
        a L{syslog.SyslogObserver} with given C{prefix}.
 
1559
        """
 
1560
        logs = _setupSyslog(self)
 
1561
        logger = UnixAppLogger({"syslog": True, "prefix": "test-prefix"})
 
1562
        observer = logger._getLogObserver()
 
1563
        self.assertEqual(logs, ["test-prefix"])
 
1564
        observer({"a": "b"})
 
1565
        self.assertEqual(logs, ["test-prefix", {"a": "b"}])
 
1566
 
 
1567
    if syslog is None:
 
1568
        test_getLogObserverSyslog.skip = "Syslog not available"
 
1569
 
 
1570
 
 
1571
 
 
1572
class DaemonizeTests(unittest.TestCase):
 
1573
    """
 
1574
    Tests for L{_twistd_unix.UnixApplicationRunner} daemonization.
 
1575
    """
 
1576
 
 
1577
    def setUp(self):
 
1578
        self.mockos = MockOS()
 
1579
        self.config = twistd.ServerOptions()
 
1580
        self.patch(_twistd_unix, 'os', self.mockos)
 
1581
        self.runner = _twistd_unix.UnixApplicationRunner(self.config)
 
1582
        self.runner.application = service.Application("Hi!")
 
1583
        self.runner.oldstdout = sys.stdout
 
1584
        self.runner.oldstderr = sys.stderr
 
1585
        self.runner.startReactor = lambda *args: None
 
1586
 
 
1587
 
 
1588
    def test_success(self):
 
1589
        """
 
1590
        When double fork succeeded in C{daemonize}, the child process writes
 
1591
        B{0} to the status pipe.
 
1592
        """
 
1593
        with AlternateReactor(FakeDaemonizingReactor()):
 
1594
            self.runner.postApplication()
 
1595
        self.assertEqual(
 
1596
            self.mockos.actions,
 
1597
            [('chdir', '.'), ('umask', 0o077), ('fork', True), 'setsid',
 
1598
             ('fork', True), ('write', -2, '0'), ('unlink', 'twistd.pid')])
 
1599
        self.assertEqual(self.mockos.closed, [-3, -2])
 
1600
 
 
1601
 
 
1602
    def test_successInParent(self):
 
1603
        """
 
1604
        The parent process initiating the C{daemonize} call reads data from the
 
1605
        status pipe and then exit the process.
 
1606
        """
 
1607
        self.mockos.child = False
 
1608
        self.mockos.readData = "0"
 
1609
        with AlternateReactor(FakeDaemonizingReactor()):
 
1610
            self.assertRaises(SystemError, self.runner.postApplication)
 
1611
        self.assertEqual(
 
1612
            self.mockos.actions,
 
1613
            [('chdir', '.'), ('umask', 0o077), ('fork', True),
 
1614
             ('read', -1, 100), ('exit', 0), ('unlink', 'twistd.pid')])
 
1615
        self.assertEqual(self.mockos.closed, [-1])
 
1616
 
 
1617
 
 
1618
    def test_successEINTR(self):
 
1619
        """
 
1620
        If the C{os.write} call to the status pipe raises an B{EINTR} error,
 
1621
        the process child retries to write.
 
1622
        """
 
1623
        written = []
 
1624
 
 
1625
        def raisingWrite(fd, data):
 
1626
            written.append((fd, data))
 
1627
            if len(written) == 1:
 
1628
                raise IOError(errno.EINTR)
 
1629
 
 
1630
        self.mockos.write = raisingWrite
 
1631
        with AlternateReactor(FakeDaemonizingReactor()):
 
1632
            self.runner.postApplication()
 
1633
        self.assertEqual(
 
1634
            self.mockos.actions,
 
1635
            [('chdir', '.'), ('umask', 0o077), ('fork', True), 'setsid',
 
1636
             ('fork', True), ('unlink', 'twistd.pid')])
 
1637
        self.assertEqual(self.mockos.closed, [-3, -2])
 
1638
        self.assertEqual([(-2, '0'), (-2, '0')], written)
 
1639
 
 
1640
 
 
1641
    def test_successInParentEINTR(self):
 
1642
        """
 
1643
        If the C{os.read} call on the status pipe raises an B{EINTR} error, the
 
1644
        parent child retries to read.
 
1645
        """
 
1646
        read = []
 
1647
 
 
1648
        def raisingRead(fd, size):
 
1649
            read.append((fd, size))
 
1650
            if len(read) == 1:
 
1651
                raise IOError(errno.EINTR)
 
1652
            return "0"
 
1653
 
 
1654
        self.mockos.read = raisingRead
 
1655
        self.mockos.child = False
 
1656
        with AlternateReactor(FakeDaemonizingReactor()):
 
1657
            self.assertRaises(SystemError, self.runner.postApplication)
 
1658
        self.assertEqual(
 
1659
            self.mockos.actions,
 
1660
            [('chdir', '.'), ('umask', 0o077), ('fork', True),
 
1661
             ('exit', 0), ('unlink', 'twistd.pid')])
 
1662
        self.assertEqual(self.mockos.closed, [-1])
 
1663
        self.assertEqual([(-1, 100), (-1, 100)], read)
 
1664
 
 
1665
 
 
1666
    def test_error(self):
 
1667
        """
 
1668
        If an error happens during daemonization, the child process writes the
 
1669
        exception error to the status pipe.
 
1670
        """
 
1671
 
 
1672
        class FakeService(service.Service):
 
1673
 
 
1674
            def startService(self):
 
1675
                raise RuntimeError("Something is wrong")
 
1676
 
 
1677
        errorService = FakeService()
 
1678
        errorService.setServiceParent(self.runner.application)
 
1679
 
 
1680
        with AlternateReactor(FakeDaemonizingReactor()):
 
1681
            self.assertRaises(RuntimeError, self.runner.postApplication)
 
1682
        self.assertEqual(
 
1683
            self.mockos.actions,
 
1684
            [('chdir', '.'), ('umask', 0o077), ('fork', True), 'setsid',
 
1685
             ('fork', True), ('write', -2, '1 Something is wrong'),
 
1686
             ('unlink', 'twistd.pid')])
 
1687
        self.assertEqual(self.mockos.closed, [-3, -2])
 
1688
 
 
1689
 
 
1690
    def test_errorInParent(self):
 
1691
        """
 
1692
        When the child writes an error message to the status pipe during
 
1693
        daemonization, the parent writes the message to C{stderr} and exits
 
1694
        with non-zero status code.
 
1695
        """
 
1696
        self.mockos.child = False
 
1697
        self.mockos.readData = "1: An identified error"
 
1698
        errorIO = NativeStringIO()
 
1699
        self.patch(sys, '__stderr__', errorIO)
 
1700
        with AlternateReactor(FakeDaemonizingReactor()):
 
1701
            self.assertRaises(SystemError, self.runner.postApplication)
 
1702
        self.assertEqual(
 
1703
            errorIO.getvalue(),
 
1704
            "An error has occurred: ' An identified error'\n"
 
1705
            "Please look at log file for more information.\n")
 
1706
        self.assertEqual(
 
1707
            self.mockos.actions,
 
1708
            [('chdir', '.'), ('umask', 0o077), ('fork', True),
 
1709
             ('read', -1, 100), ('exit', 1), ('unlink', 'twistd.pid')])
 
1710
        self.assertEqual(self.mockos.closed, [-1])
 
1711
 
 
1712
 
 
1713
    def test_errorMessageTruncated(self):
 
1714
        """
 
1715
        If an error in daemonize gives a too big error message, it's truncated
 
1716
        by the child.
 
1717
        """
 
1718
 
 
1719
        class FakeService(service.Service):
 
1720
 
 
1721
            def startService(self):
 
1722
                raise RuntimeError("x" * 200)
 
1723
 
 
1724
        errorService = FakeService()
 
1725
        errorService.setServiceParent(self.runner.application)
 
1726
 
 
1727
        with AlternateReactor(FakeDaemonizingReactor()):
 
1728
            self.assertRaises(RuntimeError, self.runner.postApplication)
 
1729
        self.assertEqual(
 
1730
            self.mockos.actions,
 
1731
            [('chdir', '.'), ('umask', 0o077), ('fork', True), 'setsid',
 
1732
             ('fork', True), ('write', -2, '1 ' + 'x' * 98),
 
1733
             ('unlink', 'twistd.pid')])
 
1734
        self.assertEqual(self.mockos.closed, [-3, -2])
 
1735
 
 
1736
 
 
1737
    def test_hooksCalled(self):
 
1738
        """
 
1739
        C{daemonize} indeed calls L{IReactorDaemonize.beforeDaemonize} and
 
1740
        L{IReactorDaemonize.afterDaemonize} if the reactor implements
 
1741
        L{IReactorDaemonize}.
 
1742
        """
 
1743
        reactor = FakeDaemonizingReactor()
 
1744
        self.runner.daemonize(reactor)
 
1745
        self.assertTrue(reactor._beforeDaemonizeCalled)
 
1746
        self.assertTrue(reactor._afterDaemonizeCalled)
 
1747
 
 
1748
 
 
1749
    def test_hooksNotCalled(self):
 
1750
        """
 
1751
        C{daemonize} does NOT call L{IReactorDaemonize.beforeDaemonize} or
 
1752
        L{IReactorDaemonize.afterDaemonize} if the reactor does NOT implement
 
1753
        L{IReactorDaemonize}.
 
1754
        """
 
1755
        reactor = FakeNonDaemonizingReactor()
 
1756
        self.runner.daemonize(reactor)
 
1757
        self.assertFalse(reactor._beforeDaemonizeCalled)
 
1758
        self.assertFalse(reactor._afterDaemonizeCalled)
 
1759
 
 
1760
 
 
1761
 
 
1762
if _twistd_unix is None:
 
1763
    DaemonizeTests.skip = "twistd unix support not available"