1
# Copyright (c) Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
Tests for L{twisted.application.app} and L{twisted.scripts.twistd}.
8
from __future__ import absolute_import, division
23
import cPickle as pickle
27
from zope.interface import implementer
28
from zope.interface.verify import verifyObject
30
from twisted.trial import unittest
31
from twisted.test.test_process import MockOS
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,
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
49
from twisted.scripts import _twistd_unix
53
from twisted.scripts._twistd_unix import UnixApplicationRunner
54
from twisted.scripts._twistd_unix import UnixAppLogger
58
from twisted.python import syslog
75
if getattr(os, 'setuid', None) is None:
76
setuidSkip = "Platform does not support --uid/--gid twistd options."
82
def patchUserDatabase(patch, user, uid, group, gid):
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
88
@param patch: A function like L{TestCase.patch} which will be used to
89
install the fake implementations.
92
@param user: The name of the single user which will exist.
95
@param uid: The UID of the single user which will exist.
98
@param group: The name of the single user which will exist.
101
@param gid: The GID of the single group which will exist.
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())
110
database = UserDatabase()
112
user, pwent.pw_passwd, uid, pwent.pw_gid,
113
pwent.pw_gecos, pwent.pw_dir, pwent.pw_shell)
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]
122
patch(pwd, "getpwnam", database.getpwnam)
123
patch(grp, "getgrnam", getgrnam)
127
class MockServiceMaker(object):
129
A non-implementation of L{twisted.application.service.IServiceMaker}.
133
def makeService(self, options):
135
Take a L{usage.Options} instance and return a
136
L{service.IService} provider.
138
self.options = options
139
self.service = service.Service()
144
class CrippledAppLogger(app.AppLogger):
146
@see: CrippledApplicationRunner.
149
def start(self, application):
154
class CrippledApplicationRunner(twistd._SomeApplicationRunner):
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.
160
loggerFactory = CrippledAppLogger
162
def preApplication(self):
166
def postApplication(self):
171
class ServerOptionsTests(unittest.TestCase):
173
Non-platform-specific tests for the pltaform-specific ServerOptions class.
175
def test_subCommands(self):
177
subCommands is built from IServiceMaker plugins, and is sorted
180
class FakePlugin(object):
181
def __init__(self, name):
183
self._options = 'options for ' + name
184
self.description = 'description of ' + name
189
apple = FakePlugin('apple')
190
banana = FakePlugin('banana')
191
coconut = FakePlugin('coconut')
192
donut = FakePlugin('donut')
194
def getPlugins(interface):
195
self.assertEqual(interface, IServiceMaker)
201
config = twistd.ServerOptions()
202
self.assertEqual(config._getPlugins, plugin.getPlugins)
203
config._getPlugins = getPlugins
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]
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)
218
def test_sortedReactorHelp(self):
220
Reactor names are listed alphabetically by I{--help-reactors}.
222
class FakeReactorInstaller(object):
223
def __init__(self, name):
224
self.shortName = 'name of ' + name
225
self.description = 'description of ' + name
227
apple = FakeReactorInstaller('apple')
228
banana = FakeReactorInstaller('banana')
229
coconut = FakeReactorInstaller('coconut')
230
donut = FakeReactorInstaller('donut')
232
def getReactorTypes():
238
config = twistd.ServerOptions()
239
self.assertEqual(config._getReactorTypes, reactors.getReactorTypes)
240
config._getReactorTypes = getReactorTypes
241
config.messageOutput = NativeStringIO()
243
self.assertRaises(SystemExit, config.parseOptions, ['--help-reactors'])
244
helpOutput = config.messageOutput.getvalue()
246
for reactor in apple, banana, coconut, donut:
248
self.assertIn(s, helpOutput)
249
indexes.append(helpOutput.index(s))
251
getIndex(reactor.shortName)
252
getIndex(reactor.description)
255
indexes, sorted(indexes),
256
'reactor descriptions were not in alphabetical order: %r' % (
260
def test_postOptionsSubCommandCausesNoSave(self):
262
postOptions should set no_save to True when a subcommand is used.
264
config = twistd.ServerOptions()
265
config.subCommand = 'ueoa'
267
self.assertEqual(config['no_save'], True)
270
def test_postOptionsNoSubCommandSavesAsUsual(self):
272
If no sub command is used, postOptions should not touch no_save.
274
config = twistd.ServerOptions()
276
self.assertEqual(config['no_save'], False)
279
def test_listAllProfilers(self):
281
All the profilers that can be used in L{app.AppProfiler} are listed in
284
config = twistd.ServerOptions()
285
helpOutput = str(config)
286
for profiler in app.AppProfiler.profilers:
287
self.assertIn(profiler, helpOutput)
290
def test_defaultUmask(self):
292
The default value for the C{umask} option is C{None}.
294
config = twistd.ServerOptions()
295
self.assertEqual(config['umask'], None)
298
def test_umask(self):
300
The value given for the C{umask} option is parsed as an octal integer
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)
310
def test_invalidUmask(self):
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}.
315
config = twistd.ServerOptions()
316
self.assertRaises(UsageError, config.parseOptions,
317
['--umask', 'abcdef'])
319
if _twistd_unix is None:
320
msg = "twistd unix not available"
321
test_defaultUmask.skip = test_umask.skip = test_invalidUmask.skip = msg
324
def test_unimportableConfiguredLogObserver(self):
326
C{--logger} with an unimportable module raises a L{UsageError}.
328
config = twistd.ServerOptions()
329
e = self.assertRaises(
330
UsageError, config.parseOptions,
331
['--logger', 'no.such.module.I.hope'])
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])
339
def test_badAttributeWithConfiguredLogObserver(self):
341
C{--logger} with a non-existent object raises a L{UsageError}.
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):
348
e.args[0].startswith(
349
"Logger 'twisted.test.test_twistd.FOOBAR' could not be "
350
"imported: 'module' object has no attribute 'FOOBAR'"))
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])
361
class TapFileTests(unittest.TestCase):
363
Test twistd-related functionality that requires a tap file on disk.
368
Create a trivial Application and put it in a tap file on disk.
370
self.tapfile = self.mktemp()
371
with open(self.tapfile, 'wb') as f:
372
pickle.dump(service.Application("Hi!"), f)
375
def test_createOrGetApplicationWithTapFile(self):
377
Ensure that the createOrGetApplication call that 'twistd -f foo.tap'
378
makes will load the Application out of foo.tap.
380
config = twistd.ServerOptions()
381
config.parseOptions(['-f', self.tapfile])
382
application = CrippledApplicationRunner(
383
config).createOrGetApplication()
384
self.assertEqual(service.IService(application).name, 'Hi!')
388
class TestLoggerFactory(object):
390
A logger factory for L{TestApplicationRunner}.
393
def __init__(self, runner):
397
def start(self, application):
399
Save the logging start on the C{runner} instance.
401
self.runner.order.append("log")
402
self.runner.hadApplicationLogObserver = hasattr(self.runner,
413
class TestApplicationRunner(app.ApplicationRunner):
415
An ApplicationRunner which tracks the environment in which its methods are
419
def __init__(self, options):
420
app.ApplicationRunner.__init__(self, options)
422
self.logger = TestLoggerFactory(self)
425
def preApplication(self):
426
self.order.append("pre")
427
self.hadApplicationPreApplication = hasattr(self, 'application')
430
def postApplication(self):
431
self.order.append("post")
432
self.hadApplicationPostApplication = hasattr(self, 'application')
436
class ApplicationRunnerTests(unittest.TestCase):
438
Non-platform-specific tests for the platform-specific ApplicationRunner.
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'
450
def test_applicationRunnerGetsCorrectApplication(self):
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.
456
arunner = CrippledApplicationRunner(self.config)
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.")
470
def test_preAndPostApplication(self):
472
Test thet preApplication and postApplication methods are
473
called by ApplicationRunner.run() when appropriate.
475
s = TestApplicationRunner(self.config)
477
self.assertFalse(s.hadApplicationPreApplication)
478
self.assertTrue(s.hadApplicationPostApplication)
479
self.assertTrue(s.hadApplicationLogObserver)
480
self.assertEqual(s.order, ["pre", "log", "post"])
483
def _applicationStartsWithConfiguredID(self, argv, uid, gid):
485
Assert that given a particular command line, an application is started
486
as a particular UID/GID.
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.
492
self.config.parseOptions(argv)
496
class FakeUnixApplicationRunner(twistd._SomeApplicationRunner):
497
def setupEnvironment(self, chroot, rundir, nodaemon, umask,
499
events.append('environment')
501
def shedPrivileges(self, euid, uid, gid):
502
events.append(('privileges', euid, uid, gid))
504
def startReactor(self, reactor, oldstdout, oldstderr):
505
events.append('reactor')
507
def removePID(self, pidfile):
510
@implementer(service.IService, service.IProcess)
511
class FakeService(object):
517
def setName(self, name):
520
def setServiceParent(self, parent):
523
def disownServiceParent(self):
526
def privilegedStartService(self):
527
events.append('privilegedStartService')
529
def startService(self):
530
events.append('startService')
532
def stopService(self):
535
application = FakeService()
536
verifyObject(service.IService, application)
537
verifyObject(service.IProcess, application)
539
runner = FakeUnixApplicationRunner(self.config)
540
runner.preApplication()
541
runner.application = application
542
runner.postApplication()
546
['environment', 'privilegedStartService',
547
('privileges', False, uid, gid), 'startService', 'reactor'])
550
def test_applicationStartsWithConfiguredNumericIDs(self):
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}.
559
self._applicationStartsWithConfiguredID(
560
["--uid", str(uid), "--gid", str(gid)], uid, gid)
561
test_applicationStartsWithConfiguredNumericIDs.skip = setuidSkip
564
def test_applicationStartsWithConfiguredNameIDs(self):
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}.
575
patchUserDatabase(self.patch, user, uid, group, gid)
576
self._applicationStartsWithConfiguredID(
577
["--uid", user, "--gid", group], uid, gid)
578
test_applicationStartsWithConfiguredNameIDs.skip = setuidSkip
581
def test_startReactorRunsTheReactor(self):
583
L{startReactor} calls L{reactor.run}.
585
reactor = DummyReactor()
586
runner = app.ApplicationRunner({
588
"profiler": "profile",
590
runner.startReactor(reactor, None, None)
592
reactor.called, "startReactor did not call reactor.run()")
596
class UnixApplicationRunnerSetupEnvironmentTests(unittest.TestCase):
598
Tests for L{UnixApplicationRunner.setupEnvironment}.
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}).
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}).
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}).
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}.
616
if _twistd_unix is None:
617
skip = "twistd unix not available"
622
self.root = self.unset
623
self.cwd = self.unset
624
self.mask = self.unset
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
634
def daemonize(self, reactor):
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.
640
self.patch(os, 'getpid', lambda: self.pid + 1)
643
def test_chroot(self):
645
L{UnixApplicationRunner.setupEnvironment} changes the root of the
646
filesystem if passed a non-C{None} value for the C{chroot} parameter.
648
self.runner.setupEnvironment("/foo/bar", ".", True, None, None)
649
self.assertEqual(self.root, "/foo/bar")
652
def test_noChroot(self):
654
L{UnixApplicationRunner.setupEnvironment} does not change the root of
655
the filesystem if passed C{None} for the C{chroot} parameter.
657
self.runner.setupEnvironment(None, ".", True, None, None)
658
self.assertIdentical(self.root, self.unset)
661
def test_changeWorkingDirectory(self):
663
L{UnixApplicationRunner.setupEnvironment} changes the working directory
664
of the process to the path given for the C{rundir} parameter.
666
self.runner.setupEnvironment(None, "/foo/bar", True, None, None)
667
self.assertEqual(self.cwd, "/foo/bar")
670
def test_daemonize(self):
672
L{UnixApplicationRunner.setupEnvironment} daemonizes the process if
673
C{False} is passed for the C{nodaemon} parameter.
675
with AlternateReactor(FakeDaemonizingReactor()):
676
self.runner.setupEnvironment(None, ".", False, None, None)
677
self.assertTrue(self.daemon)
680
def test_noDaemonize(self):
682
L{UnixApplicationRunner.setupEnvironment} does not daemonize the
683
process if C{True} is passed for the C{nodaemon} parameter.
685
self.runner.setupEnvironment(None, ".", True, None, None)
686
self.assertFalse(self.daemon)
689
def test_nonDaemonPIDFile(self):
691
L{UnixApplicationRunner.setupEnvironment} writes the process's PID to
692
the file specified by the C{pidfile} parameter.
694
pidfile = self.mktemp()
695
self.runner.setupEnvironment(None, ".", True, None, pidfile)
696
with open(pidfile, 'rb') as f:
698
self.assertEqual(pid, self.pid)
701
def test_daemonPIDFile(self):
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}.
707
pidfile = self.mktemp()
708
with AlternateReactor(FakeDaemonizingReactor()):
709
self.runner.setupEnvironment(None, ".", False, None, pidfile)
710
with open(pidfile, 'rb') as f:
712
self.assertEqual(pid, self.pid + 1)
715
def test_umask(self):
717
L{UnixApplicationRunner.setupEnvironment} changes the process umask to
718
the value specified by the C{umask} parameter.
720
with AlternateReactor(FakeDaemonizingReactor()):
721
self.runner.setupEnvironment(None, ".", False, 123, None)
722
self.assertEqual(self.mask, 123)
725
def test_noDaemonizeNoUmask(self):
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.
731
self.runner.setupEnvironment(None, ".", True, None, None)
732
self.assertIdentical(self.mask, self.unset)
735
def test_daemonizedNoUmask(self):
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.
741
with AlternateReactor(FakeDaemonizingReactor()):
742
self.runner.setupEnvironment(None, ".", False, None, None)
743
self.assertEqual(self.mask, 0o077)
747
class UnixApplicationRunnerStartApplicationTests(unittest.TestCase):
749
Tests for L{UnixApplicationRunner.startApplication}.
751
if _twistd_unix is None:
752
skip = "twistd unix not available"
755
def test_setupEnvironment(self):
757
L{UnixApplicationRunner.startApplication} calls
758
L{UnixApplicationRunner.setupEnvironment} with the chroot, rundir,
759
nodaemon, umask, and pidfile parameters from the configuration it is
762
options = twistd.ServerOptions()
763
options.parseOptions([
766
'--chroot', '/foo/chroot',
767
'--rundir', '/foo/rundir',
768
'--pidfile', '/foo/pidfile'])
769
application = service.Application("test_setupEnvironment")
770
self.runner = UnixApplicationRunner(options)
773
def fakeSetupEnvironment(self, chroot, rundir, nodaemon, umask,
775
args.extend((chroot, rundir, nodaemon, umask, pidfile))
779
inspect.getargspec(self.runner.setupEnvironment),
780
inspect.getargspec(fakeSetupEnvironment))
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)
791
['/foo/chroot', '/foo/rundir', True, 56, '/foo/pidfile'])
795
class UnixApplicationRunnerRemovePIDTests(unittest.TestCase):
797
Tests for L{UnixApplicationRunner.removePID}.
799
if _twistd_unix is None:
800
skip = "twistd unix not available"
803
def test_removePID(self):
805
L{UnixApplicationRunner.removePID} deletes the file the name of
806
which is passed to it.
808
runner = UnixApplicationRunner({})
811
pidfile = os.path.join(path, "foo.pid")
812
open(pidfile, "w").close()
813
runner.removePID(pidfile)
814
self.assertFalse(os.path.exists(pidfile))
817
def test_removePIDErrors(self):
819
Calling L{UnixApplicationRunner.removePID} with a non-existent filename
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)
830
class FakeNonDaemonizingReactor(object):
832
A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize}
833
methods, but not announcing this, and logging whether the methods have been
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}
843
self._beforeDaemonizeCalled = False
844
self._afterDaemonizeCalled = False
847
def beforeDaemonize(self):
848
self._beforeDaemonizeCalled = True
851
def afterDaemonize(self):
852
self._afterDaemonizeCalled = True
855
def addSystemEventTrigger(self, *args, **kw):
857
Skip event registration.
862
@implementer(IReactorDaemonize)
863
class FakeDaemonizingReactor(FakeNonDaemonizingReactor):
865
A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize}
866
methods, announcing this, and logging whether the methods have been called.
871
class DummyReactor(object):
873
A dummy reactor, only providing a C{run} method and checking that it
876
@ivar called: if C{run} has been called or not.
877
@type called: C{bool}
883
A fake run method, checking that it's been called one and only time.
886
raise RuntimeError("Already called")
891
class AppProfilingTests(unittest.TestCase):
893
Tests for L{app.AppProfiler}.
896
def test_profile(self):
898
L{app.ProfileRunner.run} should call the C{run} method of the reactor
899
and save profile data in the specified file.
901
config = twistd.ServerOptions()
902
config["profile"] = self.mktemp()
903
config["profiler"] = "profile"
904
profiler = app.AppProfiler(config)
905
reactor = DummyReactor()
907
profiler.run(reactor)
909
self.assertTrue(reactor.called)
910
with open(config["profile"]) as f:
912
self.assertIn("DummyReactor.run", data)
913
self.assertIn("function calls", data)
916
test_profile.skip = "profile module not available"
919
def _testStats(self, statsClass, profile):
920
out = NativeStringIO()
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)
926
# If pstats.Stats can load the data and then reformat it, then the
927
# right thing probably happened.
928
stats = statsClass(profile)
932
data = out.getvalue()
933
self.assertIn("function calls", data)
934
self.assertIn("(run)", data)
937
def test_profileSaveStats(self):
939
With the C{savestats} option specified, L{app.ProfileRunner.run}
940
should save the raw stats object instead of a summary output.
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()
949
profiler.run(reactor)
951
self.assertTrue(reactor.called)
952
self._testStats(pstats.Stats, config['profile'])
955
test_profileSaveStats.skip = "profile module not available"
958
def test_withoutProfile(self):
960
When the C{profile} module is not present, L{app.ProfilerRunner.run}
961
should raise a C{SystemExit} exception.
963
savedModules = sys.modules.copy()
965
config = twistd.ServerOptions()
966
config["profiler"] = "profile"
967
profiler = app.AppProfiler(config)
969
sys.modules["profile"] = None
971
self.assertRaises(SystemExit, profiler.run, None)
974
sys.modules.update(savedModules)
977
def test_profilePrintStatsError(self):
979
When an error happens during the print of the stats, C{sys.stdout}
980
should be restored to its initial value.
982
class ErroneousProfile(profile.Profile):
983
def print_stats(self):
984
raise RuntimeError("Boom")
985
self.patch(profile, "Profile", ErroneousProfile)
987
config = twistd.ServerOptions()
988
config["profile"] = self.mktemp()
989
config["profiler"] = "profile"
990
profiler = app.AppProfiler(config)
991
reactor = DummyReactor()
993
oldStdout = sys.stdout
994
self.assertRaises(RuntimeError, profiler.run, reactor)
995
self.assertIdentical(sys.stdout, oldStdout)
998
test_profilePrintStatsError.skip = "profile module not available"
1001
def test_cProfile(self):
1003
L{app.CProfileRunner.run} should call the C{run} method of the
1004
reactor and save profile data in the specified file.
1006
config = twistd.ServerOptions()
1007
config["profile"] = self.mktemp()
1008
config["profiler"] = "cProfile"
1009
profiler = app.AppProfiler(config)
1010
reactor = DummyReactor()
1012
profiler.run(reactor)
1014
self.assertTrue(reactor.called)
1015
with open(config["profile"]) as f:
1017
self.assertIn("run", data)
1018
self.assertIn("function calls", data)
1020
if cProfile is None:
1021
test_cProfile.skip = "cProfile module not available"
1024
def test_cProfileSaveStats(self):
1026
With the C{savestats} option specified,
1027
L{app.CProfileRunner.run} should save the raw stats object
1028
instead of a summary output.
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()
1037
profiler.run(reactor)
1039
self.assertTrue(reactor.called)
1040
self._testStats(pstats.Stats, config['profile'])
1042
if cProfile is None:
1043
test_cProfileSaveStats.skip = "cProfile module not available"
1046
def test_withoutCProfile(self):
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}.
1052
savedModules = sys.modules.copy()
1053
sys.modules["cProfile"] = None
1055
config = twistd.ServerOptions()
1056
config["profiler"] = "cProfile"
1057
profiler = app.AppProfiler(config)
1059
self.assertRaises(SystemExit, profiler.run, None)
1062
sys.modules.update(savedModules)
1065
def test_unknownProfiler(self):
1067
Check that L{app.AppProfiler} raises L{SystemExit} when given an
1068
unknown profiler name.
1070
config = twistd.ServerOptions()
1071
config["profile"] = self.mktemp()
1072
config["profiler"] = "foobar"
1074
error = self.assertRaises(SystemExit, app.AppProfiler, config)
1075
self.assertEqual(str(error), "Unsupported profiler name: foobar")
1078
def test_defaultProfiler(self):
1080
L{app.Profiler} defaults to the cprofile profiler if not specified.
1082
profiler = app.AppProfiler({})
1083
self.assertEqual(profiler.profiler, "cprofile")
1086
def test_profilerNameCaseInsentive(self):
1088
The case of the profiler name passed to L{app.AppProfiler} is not
1091
profiler = app.AppProfiler({"profiler": "CprOfile"})
1092
self.assertEqual(profiler.profiler, "cprofile")
1096
def _patchTextFileLogObserver(patch):
1098
Patch L{logger.textFileLogObserver} to record every call and keep a
1099
reference to the passed log file for tests.
1101
@param patch: a callback for patching (usually L{unittest.TestCase.patch}).
1103
@return: the list that keeps track of the log files.
1107
oldFileLogObserver = logger.textFileLogObserver
1109
def observer(logFile, *args, **kwargs):
1110
logFiles.append(logFile)
1111
return oldFileLogObserver(logFile, *args, **kwargs)
1113
patch(logger, 'textFileLogObserver', observer)
1118
def _setupSyslog(testCase):
1120
Make fake syslog, and return list to which prefix and then log
1121
messages will be appended if it is used.
1125
class fakesyslogobserver(object):
1126
def __init__(self, prefix):
1127
logMessages.append(prefix)
1129
def emit(self, eventDict):
1130
logMessages.append(eventDict)
1132
testCase.patch(syslog, "SyslogObserver", fakesyslogobserver)
1137
class AppLoggerTests(unittest.TestCase):
1139
Tests for L{app.AppLogger}.
1141
@ivar observers: list of observers installed during the tests.
1142
@type observers: C{list}
1147
Override L{globaLogBeginner.beginLoggingTo} so that we can trace the
1148
observers installed in C{self.observers}.
1152
def beginLoggingTo(observers):
1153
for observer in observers:
1154
self.observers.append(observer)
1155
globalLogPublisher.addObserver(observer)
1157
self.patch(globalLogBeginner, 'beginLoggingTo', beginLoggingTo)
1162
Remove all installed observers.
1164
for observer in self.observers:
1165
globalLogPublisher.removeObserver(observer)
1168
def _makeObserver(self):
1170
Make a new observer which captures all logs sent to it.
1172
@return: An observer that stores all logs sent to it.
1173
@rtype: Callable that implements L{ILogObserver}.
1175
@implementer(ILogObserver)
1176
class TestObserver(object):
1179
def __call__(self, event):
1180
self._logs.append(event)
1182
return TestObserver()
1185
def _checkObserver(self, observer):
1187
Ensure that initial C{twistd} logs are written to logs.
1189
@param observer: The observer made by L{self._makeObserver).
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"])
1196
def test_start(self):
1198
L{app.AppLogger.start} calls L{globalLogBeginner.addObserver}, and then
1199
writes some messages about twistd and the reactor.
1201
logger = app.AppLogger({})
1202
observer = self._makeObserver()
1203
logger._getLogObserver = lambda: observer
1204
logger.start(Componentized())
1205
self._checkObserver(observer)
1208
def test_startUsesApplicationLogObserver(self):
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
1214
application = Componentized()
1215
observer = self._makeObserver()
1216
application.setComponent(ILogObserver, observer)
1217
logger = app.AppLogger({})
1218
logger.start(application)
1219
self._checkObserver(observer)
1222
def _setupConfiguredLogger(self, application, extraLogArgs={},
1223
appLogger=app.AppLogger):
1225
Set up an AppLogger which exercises the C{logger} configuration option.
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.
1236
@return: The logs accumulated by the log observer.
1238
observer = self._makeObserver()
1239
logArgs = {"logger": lambda: observer}
1240
logArgs.update(extraLogArgs)
1241
logger = appLogger(logArgs)
1242
logger.start(application)
1246
def test_startUsesConfiguredLogObserver(self):
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.
1253
application = Componentized()
1254
self._checkObserver(self._setupConfiguredLogger(application))
1257
def test_configuredLogObserverBeatsComponent(self):
1259
C{--logger} takes precedence over a L{ILogObserver} component set on
1262
observer = self._makeObserver()
1263
application = Componentized()
1264
application.setComponent(ILogObserver, observer)
1265
self._checkObserver(self._setupConfiguredLogger(application))
1266
self.assertEqual(observer._logs, [])
1269
def test_configuredLogObserverBeatsLegacyComponent(self):
1271
C{--logger} takes precedence over a L{LegacyILogObserver} component
1275
application = Componentized()
1276
application.setComponent(LegacyILogObserver, nonlogs.append)
1277
self._checkObserver(self._setupConfiguredLogger(application))
1278
self.assertEqual(nonlogs, [])
1281
def test_loggerComponentBeatsLegacyLoggerComponent(self):
1283
A L{ILogObserver} takes precedence over a L{LegacyILogObserver}
1284
component set on Application.
1287
observer = self._makeObserver()
1288
application = Componentized()
1289
application.setComponent(ILogObserver, observer)
1290
application.setComponent(LegacyILogObserver, nonlogs.append)
1292
logger = app.AppLogger({})
1293
logger.start(application)
1295
self._checkObserver(observer)
1296
self.assertEqual(nonlogs, [])
1299
def test_configuredLogObserverBeatsSyslog(self):
1301
C{--logger} takes precedence over a C{--syslog} command line
1304
logs = _setupSyslog(self)
1305
application = Componentized()
1306
self._checkObserver(self._setupConfiguredLogger(application,
1309
self.assertEqual(logs, [])
1311
if _twistd_unix is None or syslog is None:
1312
test_configuredLogObserverBeatsSyslog.skip = (
1313
"Not on POSIX, or syslog not available."
1317
def test_configuredLogObserverBeatsLogfile(self):
1319
C{--logger} takes precedence over a C{--logfile} command line
1322
application = Componentized()
1323
path = self.mktemp()
1324
self._checkObserver(self._setupConfiguredLogger(application,
1325
{"logfile": "path"}))
1326
self.assertFalse(os.path.exists(path))
1329
def test_getLogObserverStdout(self):
1331
When logfile is empty or set to C{-}, L{app.AppLogger._getLogObserver}
1332
returns a log observer pointing at C{sys.stdout}.
1334
logger = app.AppLogger({"logfile": "-"})
1335
logFiles = _patchTextFileLogObserver(self.patch)
1337
logger._getLogObserver()
1339
self.assertEqual(len(logFiles), 1)
1340
self.assertIdentical(logFiles[0], sys.stdout)
1342
logger = app.AppLogger({"logfile": ""})
1343
logger._getLogObserver()
1345
self.assertEqual(len(logFiles), 2)
1346
self.assertIdentical(logFiles[1], sys.stdout)
1349
def test_getLogObserverFile(self):
1351
When passing the C{logfile} option, L{app.AppLogger._getLogObserver}
1352
returns a log observer pointing at the specified path.
1354
logFiles = _patchTextFileLogObserver(self.patch)
1355
filename = self.mktemp()
1356
logger = app.AppLogger({"logfile": filename})
1358
logger._getLogObserver()
1360
self.assertEqual(len(logFiles), 1)
1361
self.assertEqual(logFiles[0].path,
1362
os.path.abspath(filename))
1365
def test_stop(self):
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.
1374
def remove(observer):
1375
removed.append(observer)
1377
self.patch(globalLogPublisher, 'removeObserver', remove)
1378
logger = app.AppLogger({})
1379
logger._observer = observer
1381
self.assertEqual(removed, [observer])
1383
self.assertEqual(removed, [observer])
1384
self.assertIdentical(logger._observer, None)
1387
def test_legacyObservers(self):
1389
L{app.AppLogger} using a legacy logger observer still works, wrapping
1390
it in a compat shim.
1393
logger = app.AppLogger({})
1395
@implementer(LegacyILogObserver)
1396
class LoggerObserver(object):
1398
An observer which implements the legacy L{LegacyILogObserver}.
1400
def __call__(self, x):
1402
Add C{x} to the logs list.
1406
logger._observerFactory = lambda: LoggerObserver()
1407
logger.start(Componentized())
1409
self.assertIn("starting up", textFromEventDict(logs[0]))
1410
warnings = self.flushWarnings(
1411
[self.test_legacyObservers])
1412
self.assertEqual(len(warnings), 0)
1415
def test_unmarkedObserversDeprecated(self):
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}.
1422
logger = app.AppLogger({})
1423
logger._getLogObserver = lambda: logs.append
1424
logger.start(Componentized())
1426
self.assertIn("starting up", textFromEventDict(logs[0]))
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."))
1443
class UnixAppLoggerTests(unittest.TestCase):
1445
Tests for L{UnixAppLogger}.
1447
@ivar signals: list of signal handlers installed.
1448
@type signals: C{list}
1450
if _twistd_unix is None:
1451
skip = "twistd unix not available"
1456
Fake C{signal.signal} for not installing the handlers but saving them
1461
def fakeSignal(sig, f):
1462
self.signals.append((sig, f))
1464
self.patch(signal, "signal", fakeSignal)
1467
def test_getLogObserverStdout(self):
1469
When non-daemonized and C{logfile} is empty or set to C{-},
1470
L{UnixAppLogger._getLogObserver} returns a log observer pointing at
1473
logFiles = _patchTextFileLogObserver(self.patch)
1475
logger = UnixAppLogger({"logfile": "-", "nodaemon": True})
1476
logger._getLogObserver()
1477
self.assertEqual(len(logFiles), 1)
1478
self.assertIdentical(logFiles[0], sys.stdout)
1480
logger = UnixAppLogger({"logfile": "", "nodaemon": True})
1481
logger._getLogObserver()
1482
self.assertEqual(len(logFiles), 2)
1483
self.assertIdentical(logFiles[1], sys.stdout)
1486
def test_getLogObserverStdoutDaemon(self):
1488
When daemonized and C{logfile} is set to C{-},
1489
L{UnixAppLogger._getLogObserver} raises C{SystemExit}.
1491
logger = UnixAppLogger({"logfile": "-", "nodaemon": False})
1492
error = self.assertRaises(SystemExit, logger._getLogObserver)
1493
self.assertEqual(str(error), "Daemons cannot log to stdout, exiting!")
1496
def test_getLogObserverFile(self):
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.
1502
logFiles = _patchTextFileLogObserver(self.patch)
1503
filename = self.mktemp()
1504
logger = UnixAppLogger({"logfile": filename})
1505
logger._getLogObserver()
1507
self.assertEqual(len(logFiles), 1)
1508
self.assertEqual(logFiles[0].path, os.path.abspath(filename))
1510
self.assertEqual(len(self.signals), 1)
1511
self.assertEqual(self.signals[0][0], signal.SIGUSR1)
1518
logFiles[0].rotate = rotate
1520
rotateLog = self.signals[0][1]
1521
rotateLog(None, None)
1525
def test_getLogObserverDontOverrideSignalHandler(self):
1527
If a signal handler is already installed,
1528
L{UnixAppLogger._getLogObserver} doesn't override it.
1530
def fakeGetSignal(sig):
1531
self.assertEqual(sig, signal.SIGUSR1)
1533
self.patch(signal, "getsignal", fakeGetSignal)
1534
filename = self.mktemp()
1535
logger = UnixAppLogger({"logfile": filename})
1536
logger._getLogObserver()
1538
self.assertEqual(self.signals, [])
1541
def test_getLogObserverDefaultFile(self):
1543
When daemonized and C{logfile} is empty, the observer returned by
1544
L{UnixAppLogger._getLogObserver} points at C{twistd.log} in the current
1547
logFiles = _patchTextFileLogObserver(self.patch)
1548
logger = UnixAppLogger({"logfile": "", "nodaemon": False})
1549
logger._getLogObserver()
1551
self.assertEqual(len(logFiles), 1)
1552
self.assertEqual(logFiles[0].path, os.path.abspath("twistd.log"))
1555
def test_getLogObserverSyslog(self):
1557
If C{syslog} is set to C{True}, L{UnixAppLogger._getLogObserver} starts
1558
a L{syslog.SyslogObserver} with given C{prefix}.
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"}])
1568
test_getLogObserverSyslog.skip = "Syslog not available"
1572
class DaemonizeTests(unittest.TestCase):
1574
Tests for L{_twistd_unix.UnixApplicationRunner} daemonization.
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
1588
def test_success(self):
1590
When double fork succeeded in C{daemonize}, the child process writes
1591
B{0} to the status pipe.
1593
with AlternateReactor(FakeDaemonizingReactor()):
1594
self.runner.postApplication()
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])
1602
def test_successInParent(self):
1604
The parent process initiating the C{daemonize} call reads data from the
1605
status pipe and then exit the process.
1607
self.mockos.child = False
1608
self.mockos.readData = "0"
1609
with AlternateReactor(FakeDaemonizingReactor()):
1610
self.assertRaises(SystemError, self.runner.postApplication)
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])
1618
def test_successEINTR(self):
1620
If the C{os.write} call to the status pipe raises an B{EINTR} error,
1621
the process child retries to write.
1625
def raisingWrite(fd, data):
1626
written.append((fd, data))
1627
if len(written) == 1:
1628
raise IOError(errno.EINTR)
1630
self.mockos.write = raisingWrite
1631
with AlternateReactor(FakeDaemonizingReactor()):
1632
self.runner.postApplication()
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)
1641
def test_successInParentEINTR(self):
1643
If the C{os.read} call on the status pipe raises an B{EINTR} error, the
1644
parent child retries to read.
1648
def raisingRead(fd, size):
1649
read.append((fd, size))
1651
raise IOError(errno.EINTR)
1654
self.mockos.read = raisingRead
1655
self.mockos.child = False
1656
with AlternateReactor(FakeDaemonizingReactor()):
1657
self.assertRaises(SystemError, self.runner.postApplication)
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)
1666
def test_error(self):
1668
If an error happens during daemonization, the child process writes the
1669
exception error to the status pipe.
1672
class FakeService(service.Service):
1674
def startService(self):
1675
raise RuntimeError("Something is wrong")
1677
errorService = FakeService()
1678
errorService.setServiceParent(self.runner.application)
1680
with AlternateReactor(FakeDaemonizingReactor()):
1681
self.assertRaises(RuntimeError, self.runner.postApplication)
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])
1690
def test_errorInParent(self):
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.
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)
1704
"An error has occurred: ' An identified error'\n"
1705
"Please look at log file for more information.\n")
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])
1713
def test_errorMessageTruncated(self):
1715
If an error in daemonize gives a too big error message, it's truncated
1719
class FakeService(service.Service):
1721
def startService(self):
1722
raise RuntimeError("x" * 200)
1724
errorService = FakeService()
1725
errorService.setServiceParent(self.runner.application)
1727
with AlternateReactor(FakeDaemonizingReactor()):
1728
self.assertRaises(RuntimeError, self.runner.postApplication)
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])
1737
def test_hooksCalled(self):
1739
C{daemonize} indeed calls L{IReactorDaemonize.beforeDaemonize} and
1740
L{IReactorDaemonize.afterDaemonize} if the reactor implements
1741
L{IReactorDaemonize}.
1743
reactor = FakeDaemonizingReactor()
1744
self.runner.daemonize(reactor)
1745
self.assertTrue(reactor._beforeDaemonizeCalled)
1746
self.assertTrue(reactor._afterDaemonizeCalled)
1749
def test_hooksNotCalled(self):
1751
C{daemonize} does NOT call L{IReactorDaemonize.beforeDaemonize} or
1752
L{IReactorDaemonize.afterDaemonize} if the reactor does NOT implement
1753
L{IReactorDaemonize}.
1755
reactor = FakeNonDaemonizingReactor()
1756
self.runner.daemonize(reactor)
1757
self.assertFalse(reactor._beforeDaemonizeCalled)
1758
self.assertFalse(reactor._afterDaemonizeCalled)
1762
if _twistd_unix is None:
1763
DaemonizeTests.skip = "twistd unix support not available"