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