1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
5 Tests for L{twisted.application.app} and L{twisted.scripts.twistd}.
8 import signal, inspect, errno
10 import os, sys, StringIO
18 import cPickle as pickle
22 from zope.interface import implements
23 from zope.interface.verify import verifyObject
25 from twisted.trial import unittest
26 from twisted.test.test_process import MockOS
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
42 from twisted.python import syslog
47 from twisted.scripts import _twistd_unix
51 from twisted.scripts._twistd_unix import UnixApplicationRunner
52 from twisted.scripts._twistd_unix import UnixAppLogger
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.
74 if getattr(os, 'setuid', None) is None:
75 setuidSkip = "Platform does not support --uid/--gid twistd options."
80 def patchUserDatabase(patch, user, uid, group, gid):
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
86 @param patch: A function like L{TestCase.patch} which will be used to
87 install the fake implementations.
90 @param user: The name of the single user which will exist.
93 @param uid: The UID of the single user which will exist.
96 @param group: The name of the single user which will exist.
99 @param gid: The GID of the single group which will exist.
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())
108 database = UserDatabase()
110 user, pwent.pw_passwd, uid, pwent.pw_gid,
111 pwent.pw_gecos, pwent.pw_dir, pwent.pw_shell)
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]
120 patch(pwd, "getpwnam", database.getpwnam)
121 patch(grp, "getgrnam", getgrnam)
125 class MockServiceMaker(object):
127 A non-implementation of L{twisted.application.service.IServiceMaker}.
131 def makeService(self, options):
133 Take a L{usage.Options} instance and return a
134 L{service.IService} provider.
136 self.options = options
137 self.service = service.Service()
142 class CrippledAppLogger(app.AppLogger):
144 @see: CrippledApplicationRunner.
147 def start(self, application):
152 class CrippledApplicationRunner(twistd._SomeApplicationRunner):
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.
158 loggerFactory = CrippledAppLogger
160 def preApplication(self):
164 def postApplication(self):
169 class ServerOptionsTest(unittest.TestCase):
171 Non-platform-specific tests for the pltaform-specific ServerOptions class.
173 def test_subCommands(self):
175 subCommands is built from IServiceMaker plugins, and is sorted
178 class FakePlugin(object):
179 def __init__(self, name):
181 self._options = 'options for ' + name
182 self.description = 'description of ' + name
187 apple = FakePlugin('apple')
188 banana = FakePlugin('banana')
189 coconut = FakePlugin('coconut')
190 donut = FakePlugin('donut')
192 def getPlugins(interface):
193 self.assertEqual(interface, IServiceMaker)
199 config = twistd.ServerOptions()
200 self.assertEqual(config._getPlugins, plugin.getPlugins)
201 config._getPlugins = getPlugins
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]
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)
216 def test_sortedReactorHelp(self):
218 Reactor names are listed alphabetically by I{--help-reactors}.
220 class FakeReactorInstaller(object):
221 def __init__(self, name):
222 self.shortName = 'name of ' + name
223 self.description = 'description of ' + name
225 apple = FakeReactorInstaller('apple')
226 banana = FakeReactorInstaller('banana')
227 coconut = FakeReactorInstaller('coconut')
228 donut = FakeReactorInstaller('donut')
230 def getReactorTypes():
236 config = twistd.ServerOptions()
237 self.assertEqual(config._getReactorTypes, reactors.getReactorTypes)
238 config._getReactorTypes = getReactorTypes
239 config.messageOutput = StringIO.StringIO()
241 self.assertRaises(SystemExit, config.parseOptions, ['--help-reactors'])
242 helpOutput = config.messageOutput.getvalue()
244 for reactor in apple, banana, coconut, donut:
246 self.assertIn(s, helpOutput)
247 indexes.append(helpOutput.index(s))
249 getIndex(reactor.shortName)
250 getIndex(reactor.description)
253 indexes, sorted(indexes),
254 'reactor descriptions were not in alphabetical order: %r' % (
258 def test_postOptionsSubCommandCausesNoSave(self):
260 postOptions should set no_save to True when a subcommand is used.
262 config = twistd.ServerOptions()
263 config.subCommand = 'ueoa'
265 self.assertEqual(config['no_save'], True)
268 def test_postOptionsNoSubCommandSavesAsUsual(self):
270 If no sub command is used, postOptions should not touch no_save.
272 config = twistd.ServerOptions()
274 self.assertEqual(config['no_save'], False)
277 def test_listAllProfilers(self):
279 All the profilers that can be used in L{app.AppProfiler} are listed in
282 config = twistd.ServerOptions()
283 helpOutput = str(config)
284 for profiler in app.AppProfiler.profilers:
285 self.assertIn(profiler, helpOutput)
288 def test_defaultUmask(self):
290 The default value for the C{umask} option is C{None}.
292 config = twistd.ServerOptions()
293 self.assertEqual(config['umask'], None)
296 def test_umask(self):
298 The value given for the C{umask} option is parsed as an octal integer
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)
308 def test_invalidUmask(self):
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}.
313 config = twistd.ServerOptions()
314 self.assertRaises(UsageError, config.parseOptions, ['--umask', 'abcdef'])
316 if _twistd_unix is None:
317 msg = "twistd unix not available"
318 test_defaultUmask.skip = test_umask.skip = test_invalidUmask.skip = msg
321 def test_unimportableConfiguredLogObserver(self):
323 C{--logger} with an unimportable module raises a L{UsageError}.
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])
334 def test_badAttributeWithConfiguredLogObserver(self):
336 C{--logger} with a non-existent object raises a L{UsageError}.
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])
348 class TapFileTest(unittest.TestCase):
350 Test twistd-related functionality that requires a tap file on disk.
355 Create a trivial Application and put it in a tap file on disk.
357 self.tapfile = self.mktemp()
358 f = file(self.tapfile, 'wb')
359 pickle.dump(service.Application("Hi!"), f)
363 def test_createOrGetApplicationWithTapFile(self):
365 Ensure that the createOrGetApplication call that 'twistd -f foo.tap'
366 makes will load the Application out of foo.tap.
368 config = twistd.ServerOptions()
369 config.parseOptions(['-f', self.tapfile])
370 application = CrippledApplicationRunner(config).createOrGetApplication()
371 self.assertEqual(service.IService(application).name, 'Hi!')
375 class TestLoggerFactory(object):
377 A logger factory for L{TestApplicationRunner}.
380 def __init__(self, runner):
384 def start(self, application):
386 Save the logging start on the C{runner} instance.
388 self.runner.order.append("log")
389 self.runner.hadApplicationLogObserver = hasattr(self.runner,
400 class TestApplicationRunner(app.ApplicationRunner):
402 An ApplicationRunner which tracks the environment in which its methods are
406 def __init__(self, options):
407 app.ApplicationRunner.__init__(self, options)
409 self.logger = TestLoggerFactory(self)
412 def preApplication(self):
413 self.order.append("pre")
414 self.hadApplicationPreApplication = hasattr(self, 'application')
417 def postApplication(self):
418 self.order.append("post")
419 self.hadApplicationPostApplication = hasattr(self, 'application')
423 class ApplicationRunnerTest(unittest.TestCase):
425 Non-platform-specific tests for the platform-specific ApplicationRunner.
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'
437 def test_applicationRunnerGetsCorrectApplication(self):
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.
443 arunner = CrippledApplicationRunner(self.config)
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.")
457 def test_preAndPostApplication(self):
459 Test thet preApplication and postApplication methods are
460 called by ApplicationRunner.run() when appropriate.
462 s = TestApplicationRunner(self.config)
464 self.assertFalse(s.hadApplicationPreApplication)
465 self.assertTrue(s.hadApplicationPostApplication)
466 self.assertTrue(s.hadApplicationLogObserver)
467 self.assertEqual(s.order, ["pre", "log", "post"])
470 def _applicationStartsWithConfiguredID(self, argv, uid, gid):
472 Assert that given a particular command line, an application is started
473 as a particular UID/GID.
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.
479 self.config.parseOptions(argv)
482 class FakeUnixApplicationRunner(twistd._SomeApplicationRunner):
483 def setupEnvironment(self, chroot, rundir, nodaemon, umask,
485 events.append('environment')
487 def shedPrivileges(self, euid, uid, gid):
488 events.append(('privileges', euid, uid, gid))
490 def startReactor(self, reactor, oldstdout, oldstderr):
491 events.append('reactor')
493 def removePID(self, pidfile):
497 class FakeService(object):
498 implements(service.IService, service.IProcess)
504 def setName(self, name):
507 def setServiceParent(self, parent):
510 def disownServiceParent(self):
513 def privilegedStartService(self):
514 events.append('privilegedStartService')
516 def startService(self):
517 events.append('startService')
519 def stopService(self):
522 application = FakeService()
523 verifyObject(service.IService, application)
524 verifyObject(service.IProcess, application)
526 runner = FakeUnixApplicationRunner(self.config)
527 runner.preApplication()
528 runner.application = application
529 runner.postApplication()
533 ['environment', 'privilegedStartService',
534 ('privileges', False, uid, gid), 'startService', 'reactor'])
537 def test_applicationStartsWithConfiguredNumericIDs(self):
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}.
546 self._applicationStartsWithConfiguredID(
547 ["--uid", str(uid), "--gid", str(gid)], uid, gid)
548 test_applicationStartsWithConfiguredNumericIDs.skip = setuidSkip
551 def test_applicationStartsWithConfiguredNameIDs(self):
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}.
562 patchUserDatabase(self.patch, user, uid, group, gid)
563 self._applicationStartsWithConfiguredID(
564 ["--uid", user, "--gid", group], uid, gid)
565 test_applicationStartsWithConfiguredNameIDs.skip = setuidSkip
568 def test_startReactorRunsTheReactor(self):
570 L{startReactor} calls L{reactor.run}.
572 reactor = DummyReactor()
573 runner = app.ApplicationRunner({
575 "profiler": "profile",
577 runner.startReactor(reactor, None, None)
579 reactor.called, "startReactor did not call reactor.run()")
583 class UnixApplicationRunnerSetupEnvironmentTests(unittest.TestCase):
585 Tests for L{UnixApplicationRunner.setupEnvironment}.
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 ).
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).
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).
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.
603 if _twistd_unix is None:
604 skip = "twistd unix not available"
609 self.root = self.unset
610 self.cwd = self.unset
611 self.mask = self.unset
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({})
621 def daemonize(self, reactor, os):
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.
627 self.patch(os, 'getpid', lambda: self.pid + 1)
630 def test_chroot(self):
632 L{UnixApplicationRunner.setupEnvironment} changes the root of the
633 filesystem if passed a non-C{None} value for the C{chroot} parameter.
635 self.runner.setupEnvironment("/foo/bar", ".", True, None, None)
636 self.assertEqual(self.root, "/foo/bar")
639 def test_noChroot(self):
641 L{UnixApplicationRunner.setupEnvironment} does not change the root of
642 the filesystem if passed C{None} for the C{chroot} parameter.
644 self.runner.setupEnvironment(None, ".", True, None, None)
645 self.assertIdentical(self.root, self.unset)
648 def test_changeWorkingDirectory(self):
650 L{UnixApplicationRunner.setupEnvironment} changes the working directory
651 of the process to the path given for the C{rundir} parameter.
653 self.runner.setupEnvironment(None, "/foo/bar", True, None, None)
654 self.assertEqual(self.cwd, "/foo/bar")
657 def test_daemonize(self):
659 L{UnixApplicationRunner.setupEnvironment} daemonizes the process if
660 C{False} is passed for the C{nodaemon} parameter.
662 self.runner.setupEnvironment(None, ".", False, None, None)
663 self.assertTrue(self.daemon)
666 def test_noDaemonize(self):
668 L{UnixApplicationRunner.setupEnvironment} does not daemonize the
669 process if C{True} is passed for the C{nodaemon} parameter.
671 self.runner.setupEnvironment(None, ".", True, None, None)
672 self.assertFalse(self.daemon)
675 def test_nonDaemonPIDFile(self):
677 L{UnixApplicationRunner.setupEnvironment} writes the process's PID to
678 the file specified by the C{pidfile} parameter.
680 pidfile = self.mktemp()
681 self.runner.setupEnvironment(None, ".", True, None, pidfile)
683 pid = int(fObj.read())
685 self.assertEqual(pid, self.pid)
688 def test_daemonPIDFile(self):
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}.
694 pidfile = self.mktemp()
695 self.runner.setupEnvironment(None, ".", False, None, pidfile)
697 pid = int(fObj.read())
699 self.assertEqual(pid, self.pid + 1)
702 def test_umask(self):
704 L{UnixApplicationRunner.setupEnvironment} changes the process umask to
705 the value specified by the C{umask} parameter.
707 self.runner.setupEnvironment(None, ".", False, 123, None)
708 self.assertEqual(self.mask, 123)
711 def test_noDaemonizeNoUmask(self):
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.
717 self.runner.setupEnvironment(None, ".", True, None, None)
718 self.assertIdentical(self.mask, self.unset)
721 def test_daemonizedNoUmask(self):
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.
727 self.runner.setupEnvironment(None, ".", False, None, None)
728 self.assertEqual(self.mask, 0077)
732 class UnixApplicationRunnerStartApplicationTests(unittest.TestCase):
734 Tests for L{UnixApplicationRunner.startApplication}.
736 if _twistd_unix is None:
737 skip = "twistd unix not available"
739 def test_setupEnvironment(self):
741 L{UnixApplicationRunner.startApplication} calls
742 L{UnixApplicationRunner.setupEnvironment} with the chroot, rundir,
743 nodaemon, umask, and pidfile parameters from the configuration it is
746 options = twistd.ServerOptions()
747 options.parseOptions([
750 '--chroot', '/foo/chroot',
751 '--rundir', '/foo/rundir',
752 '--pidfile', '/foo/pidfile'])
753 application = service.Application("test_setupEnvironment")
754 self.runner = UnixApplicationRunner(options)
757 def fakeSetupEnvironment(self, chroot, rundir, nodaemon, umask, pidfile):
758 args.extend((chroot, rundir, nodaemon, umask, pidfile))
762 inspect.getargspec(self.runner.setupEnvironment),
763 inspect.getargspec(fakeSetupEnvironment))
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)
772 ['/foo/chroot', '/foo/rundir', True, 56, '/foo/pidfile'])
776 class UnixApplicationRunnerRemovePID(unittest.TestCase):
778 Tests for L{UnixApplicationRunner.removePID}.
780 if _twistd_unix is None:
781 skip = "twistd unix not available"
784 def test_removePID(self):
786 L{UnixApplicationRunner.removePID} deletes the file the name of
787 which is passed to it.
789 runner = UnixApplicationRunner({})
792 pidfile = os.path.join(path, "foo.pid")
793 file(pidfile, "w").close()
794 runner.removePID(pidfile)
795 self.assertFalse(os.path.exists(pidfile))
798 def test_removePIDErrors(self):
800 Calling L{UnixApplicationRunner.removePID} with a non-existent filename logs
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)
811 class FakeNonDaemonizingReactor(object):
813 A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize} methods,
814 but not announcing this, and logging whether the methods have been called.
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}
823 self._beforeDaemonizeCalled = False
824 self._afterDaemonizeCalled = False
826 def beforeDaemonize(self):
827 self._beforeDaemonizeCalled = True
829 def afterDaemonize(self):
830 self._afterDaemonizeCalled = True
834 class FakeDaemonizingReactor(FakeNonDaemonizingReactor):
836 A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize} methods,
837 announcing this, and logging whether the methods have been called.
840 implements(IReactorDaemonize)
844 class ReactorDaemonizationTests(unittest.TestCase):
846 Tests for L{_twistd_unix.daemonize} and L{IReactorDaemonize}.
848 if _twistd_unix is None:
849 skip = "twistd unix not available"
852 def test_daemonizationHooksCalled(self):
854 L{_twistd_unix.daemonize} indeed calls
855 L{IReactorDaemonize.beforeDaemonize} and
856 L{IReactorDaemonize.afterDaemonize} if the reactor implements
857 L{IReactorDaemonize}.
859 reactor = FakeDaemonizingReactor()
861 _twistd_unix.daemonize(reactor, os)
862 self.assertTrue(reactor._beforeDaemonizeCalled)
863 self.assertTrue(reactor._afterDaemonizeCalled)
866 def test_daemonizationHooksNotCalled(self):
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}.
873 reactor = FakeNonDaemonizingReactor()
875 _twistd_unix.daemonize(reactor, os)
876 self.assertFalse(reactor._beforeDaemonizeCalled)
877 self.assertFalse(reactor._afterDaemonizeCalled)
881 class DummyReactor(object):
883 A dummy reactor, only providing a C{run} method and checking that it
886 @ivar called: if C{run} has been called or not.
887 @type called: C{bool}
893 A fake run method, checking that it's been called one and only time.
896 raise RuntimeError("Already called")
901 class AppProfilingTestCase(unittest.TestCase):
903 Tests for L{app.AppProfiler}.
906 def test_profile(self):
908 L{app.ProfileRunner.run} should call the C{run} method of the reactor
909 and save profile data in the specified file.
911 config = twistd.ServerOptions()
912 config["profile"] = self.mktemp()
913 config["profiler"] = "profile"
914 profiler = app.AppProfiler(config)
915 reactor = DummyReactor()
917 profiler.run(reactor)
919 self.assertTrue(reactor.called)
920 data = file(config["profile"]).read()
921 self.assertIn("DummyReactor.run", data)
922 self.assertIn("function calls", data)
925 test_profile.skip = "profile module not available"
928 def _testStats(self, statsClass, profile):
929 out = StringIO.StringIO()
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)
935 # If pstats.Stats can load the data and then reformat it, then the
936 # right thing probably happened.
937 stats = statsClass(profile)
941 data = out.getvalue()
942 self.assertIn("function calls", data)
943 self.assertIn("(run)", data)
946 def test_profileSaveStats(self):
948 With the C{savestats} option specified, L{app.ProfileRunner.run}
949 should save the raw stats object instead of a summary output.
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()
958 profiler.run(reactor)
960 self.assertTrue(reactor.called)
961 self._testStats(pstats.Stats, config['profile'])
964 test_profileSaveStats.skip = "profile module not available"
967 def test_withoutProfile(self):
969 When the C{profile} module is not present, L{app.ProfilerRunner.run}
970 should raise a C{SystemExit} exception.
972 savedModules = sys.modules.copy()
974 config = twistd.ServerOptions()
975 config["profiler"] = "profile"
976 profiler = app.AppProfiler(config)
978 sys.modules["profile"] = None
980 self.assertRaises(SystemExit, profiler.run, None)
983 sys.modules.update(savedModules)
986 def test_profilePrintStatsError(self):
988 When an error happens during the print of the stats, C{sys.stdout}
989 should be restored to its initial value.
991 class ErroneousProfile(profile.Profile):
992 def print_stats(self):
993 raise RuntimeError("Boom")
994 self.patch(profile, "Profile", ErroneousProfile)
996 config = twistd.ServerOptions()
997 config["profile"] = self.mktemp()
998 config["profiler"] = "profile"
999 profiler = app.AppProfiler(config)
1000 reactor = DummyReactor()
1002 oldStdout = sys.stdout
1003 self.assertRaises(RuntimeError, profiler.run, reactor)
1004 self.assertIdentical(sys.stdout, oldStdout)
1007 test_profilePrintStatsError.skip = "profile module not available"
1010 def test_hotshot(self):
1012 L{app.HotshotRunner.run} should call the C{run} method of the reactor
1013 and save profile data in the specified file.
1015 config = twistd.ServerOptions()
1016 config["profile"] = self.mktemp()
1017 config["profiler"] = "hotshot"
1018 profiler = app.AppProfiler(config)
1019 reactor = DummyReactor()
1021 profiler.run(reactor)
1023 self.assertTrue(reactor.called)
1024 data = file(config["profile"]).read()
1025 self.assertIn("run", data)
1026 self.assertIn("function calls", data)
1029 test_hotshot.skip = "hotshot module not available"
1032 def test_hotshotSaveStats(self):
1034 With the C{savestats} option specified, L{app.HotshotRunner.run} should
1035 save the raw stats object instead of a summary output.
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()
1044 profiler.run(reactor)
1046 self.assertTrue(reactor.called)
1047 self._testStats(hotshot.stats.load, config['profile'])
1050 test_hotshotSaveStats.skip = "hotshot module not available"
1053 def test_withoutHotshot(self):
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}.
1058 savedModules = sys.modules.copy()
1059 sys.modules["hotshot"] = None
1061 config = twistd.ServerOptions()
1062 config["profiler"] = "hotshot"
1063 profiler = app.AppProfiler(config)
1065 self.assertRaises(SystemExit, profiler.run, None)
1068 sys.modules.update(savedModules)
1071 def test_hotshotPrintStatsError(self):
1073 When an error happens while printing the stats, C{sys.stdout}
1074 should be restored to its initial value.
1076 class ErroneousStats(pstats.Stats):
1077 def print_stats(self):
1078 raise RuntimeError("Boom")
1079 self.patch(pstats, "Stats", ErroneousStats)
1081 config = twistd.ServerOptions()
1082 config["profile"] = self.mktemp()
1083 config["profiler"] = "hotshot"
1084 profiler = app.AppProfiler(config)
1085 reactor = DummyReactor()
1087 oldStdout = sys.stdout
1088 self.assertRaises(RuntimeError, profiler.run, reactor)
1089 self.assertIdentical(sys.stdout, oldStdout)
1092 test_hotshotPrintStatsError.skip = "hotshot module not available"
1095 def test_cProfile(self):
1097 L{app.CProfileRunner.run} should call the C{run} method of the
1098 reactor and save profile data in the specified file.
1100 config = twistd.ServerOptions()
1101 config["profile"] = self.mktemp()
1102 config["profiler"] = "cProfile"
1103 profiler = app.AppProfiler(config)
1104 reactor = DummyReactor()
1106 profiler.run(reactor)
1108 self.assertTrue(reactor.called)
1109 data = file(config["profile"]).read()
1110 self.assertIn("run", data)
1111 self.assertIn("function calls", data)
1113 if cProfile is None:
1114 test_cProfile.skip = "cProfile module not available"
1117 def test_cProfileSaveStats(self):
1119 With the C{savestats} option specified,
1120 L{app.CProfileRunner.run} should save the raw stats object
1121 instead of a summary output.
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()
1130 profiler.run(reactor)
1132 self.assertTrue(reactor.called)
1133 self._testStats(pstats.Stats, config['profile'])
1135 if cProfile is None:
1136 test_cProfileSaveStats.skip = "cProfile module not available"
1139 def test_withoutCProfile(self):
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}.
1145 savedModules = sys.modules.copy()
1146 sys.modules["cProfile"] = None
1148 config = twistd.ServerOptions()
1149 config["profiler"] = "cProfile"
1150 profiler = app.AppProfiler(config)
1152 self.assertRaises(SystemExit, profiler.run, None)
1155 sys.modules.update(savedModules)
1158 def test_unknownProfiler(self):
1160 Check that L{app.AppProfiler} raises L{SystemExit} when given an
1161 unknown profiler name.
1163 config = twistd.ServerOptions()
1164 config["profile"] = self.mktemp()
1165 config["profiler"] = "foobar"
1167 error = self.assertRaises(SystemExit, app.AppProfiler, config)
1168 self.assertEqual(str(error), "Unsupported profiler name: foobar")
1171 def test_defaultProfiler(self):
1173 L{app.Profiler} defaults to the hotshot profiler if not specified.
1175 profiler = app.AppProfiler({})
1176 self.assertEqual(profiler.profiler, "hotshot")
1179 def test_profilerNameCaseInsentive(self):
1181 The case of the profiler name passed to L{app.AppProfiler} is not
1184 profiler = app.AppProfiler({"profiler": "HotShot"})
1185 self.assertEqual(profiler.profiler, "hotshot")
1189 def _patchFileLogObserver(patch):
1191 Patch L{log.FileLogObserver} to record every call and keep a reference to
1192 the passed log file for tests.
1194 @param patch: a callback for patching (usually L{unittest.TestCase.patch}).
1196 @return: the list that keeps track of the log files.
1200 oldFileLobObserver = log.FileLogObserver
1201 def FileLogObserver(logFile):
1202 logFiles.append(logFile)
1203 return oldFileLobObserver(logFile)
1204 patch(log, 'FileLogObserver', FileLogObserver)
1209 def _setupSyslog(testCase):
1211 Make fake syslog, and return list to which prefix and then log
1212 messages will be appended if it is used.
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)
1225 class AppLoggerTestCase(unittest.TestCase):
1227 Tests for L{app.AppLogger}.
1229 @ivar observers: list of observers installed during the tests.
1230 @type observers: C{list}
1235 Override L{log.addObserver} so that we can trace the observers
1236 installed in C{self.observers}.
1239 def startLoggingWithObserver(observer):
1240 self.observers.append(observer)
1241 log.addObserver(observer)
1242 self.patch(log, 'startLoggingWithObserver', startLoggingWithObserver)
1247 Remove all installed observers.
1249 for observer in self.observers:
1250 log.removeObserver(observer)
1253 def _checkObserver(self, logs):
1255 Ensure that initial C{twistd} logs are written to the given list.
1258 @param logs: The list whose C{append} method was specified as the
1259 initial log observer.
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])
1266 def test_start(self):
1268 L{app.AppLogger.start} calls L{log.addObserver}, and then writes some
1269 messages about twistd and the reactor.
1271 logger = app.AppLogger({})
1273 logger._getLogObserver = lambda: observer.append
1274 logger.start(Componentized())
1275 self._checkObserver(observer)
1278 def test_startUsesApplicationLogObserver(self):
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
1284 application = Componentized()
1286 application.setComponent(ILogObserver, logs.append)
1287 logger = app.AppLogger({})
1288 logger.start(application)
1289 self._checkObserver(logs)
1292 def _setupConfiguredLogger(self, application, extraLogArgs={},
1293 appLogger=app.AppLogger):
1295 Set up an AppLogger which exercises the C{logger} configuration option.
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.
1306 @return: The logs accumulated by the log observer.
1309 logArgs = {"logger": lambda: logs.append}
1310 logArgs.update(extraLogArgs)
1311 logger = appLogger(logArgs)
1312 logger.start(application)
1316 def test_startUsesConfiguredLogObserver(self):
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.
1323 application = Componentized()
1324 self._checkObserver(self._setupConfiguredLogger(application))
1327 def test_configuredLogObserverBeatsComponent(self):
1329 C{--logger} takes precedence over a ILogObserver component set on
1333 application = Componentized()
1334 application.setComponent(ILogObserver, nonlogs.append)
1335 self._checkObserver(self._setupConfiguredLogger(application))
1336 self.assertEqual(nonlogs, [])
1339 def test_configuredLogObserverBeatsSyslog(self):
1341 C{--logger} takes precedence over a C{--syslog} command line
1344 logs = _setupSyslog(self)
1345 application = Componentized()
1346 self._checkObserver(self._setupConfiguredLogger(application,
1349 self.assertEqual(logs, [])
1351 if _twistd_unix is None or syslog is None:
1352 test_configuredLogObserverBeatsSyslog.skip = "Not on POSIX, or syslog not available."
1355 def test_configuredLogObserverBeatsLogfile(self):
1357 C{--logger} takes precedence over a C{--logfile} command line
1360 application = Componentized()
1361 path = self.mktemp()
1362 self._checkObserver(self._setupConfiguredLogger(application,
1363 {"logfile": "path"}))
1364 self.assertFalse(os.path.exists(path))
1367 def test_getLogObserverStdout(self):
1369 When logfile is empty or set to C{-}, L{app.AppLogger._getLogObserver}
1370 returns a log observer pointing at C{sys.stdout}.
1372 logger = app.AppLogger({"logfile": "-"})
1373 logFiles = _patchFileLogObserver(self.patch)
1375 observer = logger._getLogObserver()
1377 self.assertEqual(len(logFiles), 1)
1378 self.assertIdentical(logFiles[0], sys.stdout)
1380 logger = app.AppLogger({"logfile": ""})
1381 observer = logger._getLogObserver()
1383 self.assertEqual(len(logFiles), 2)
1384 self.assertIdentical(logFiles[1], sys.stdout)
1387 def test_getLogObserverFile(self):
1389 When passing the C{logfile} option, L{app.AppLogger._getLogObserver}
1390 returns a log observer pointing at the specified path.
1392 logFiles = _patchFileLogObserver(self.patch)
1393 filename = self.mktemp()
1394 logger = app.AppLogger({"logfile": filename})
1396 observer = logger._getLogObserver()
1398 self.assertEqual(len(logFiles), 1)
1399 self.assertEqual(logFiles[0].path,
1400 os.path.abspath(filename))
1403 def test_stop(self):
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.
1411 def remove(observer):
1412 removed.append(observer)
1413 self.patch(log, 'removeObserver', remove)
1414 logger = app.AppLogger({})
1415 logger._observer = observer
1417 self.assertEqual(removed, [observer])
1419 self.assertEqual(removed, [observer])
1420 self.assertIdentical(logger._observer, None)
1424 class UnixAppLoggerTestCase(unittest.TestCase):
1426 Tests for L{UnixAppLogger}.
1428 @ivar signals: list of signal handlers installed.
1429 @type signals: C{list}
1431 if _twistd_unix is None:
1432 skip = "twistd unix not available"
1436 Fake C{signal.signal} for not installing the handlers but saving them
1440 def fakeSignal(sig, f):
1441 self.signals.append((sig, f))
1442 self.patch(signal, "signal", fakeSignal)
1445 def test_getLogObserverStdout(self):
1447 When non-daemonized and C{logfile} is empty or set to C{-},
1448 L{UnixAppLogger._getLogObserver} returns a log observer pointing at
1451 logFiles = _patchFileLogObserver(self.patch)
1453 logger = UnixAppLogger({"logfile": "-", "nodaemon": True})
1454 observer = logger._getLogObserver()
1455 self.assertEqual(len(logFiles), 1)
1456 self.assertIdentical(logFiles[0], sys.stdout)
1458 logger = UnixAppLogger({"logfile": "", "nodaemon": True})
1459 observer = logger._getLogObserver()
1460 self.assertEqual(len(logFiles), 2)
1461 self.assertIdentical(logFiles[1], sys.stdout)
1464 def test_getLogObserverStdoutDaemon(self):
1466 When daemonized and C{logfile} is set to C{-},
1467 L{UnixAppLogger._getLogObserver} raises C{SystemExit}.
1469 logger = UnixAppLogger({"logfile": "-", "nodaemon": False})
1470 error = self.assertRaises(SystemExit, logger._getLogObserver)
1471 self.assertEqual(str(error), "Daemons cannot log to stdout, exiting!")
1474 def test_getLogObserverFile(self):
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.
1480 logFiles = _patchFileLogObserver(self.patch)
1481 filename = self.mktemp()
1482 logger = UnixAppLogger({"logfile": filename})
1483 observer = logger._getLogObserver()
1485 self.assertEqual(len(logFiles), 1)
1486 self.assertEqual(logFiles[0].path,
1487 os.path.abspath(filename))
1489 self.assertEqual(len(self.signals), 1)
1490 self.assertEqual(self.signals[0][0], signal.SIGUSR1)
1495 logFiles[0].rotate = rotate
1497 rotateLog = self.signals[0][1]
1498 rotateLog(None, None)
1502 def test_getLogObserverDontOverrideSignalHandler(self):
1504 If a signal handler is already installed,
1505 L{UnixAppLogger._getLogObserver} doesn't override it.
1507 def fakeGetSignal(sig):
1508 self.assertEqual(sig, signal.SIGUSR1)
1510 self.patch(signal, "getsignal", fakeGetSignal)
1511 filename = self.mktemp()
1512 logger = UnixAppLogger({"logfile": filename})
1513 observer = logger._getLogObserver()
1515 self.assertEqual(self.signals, [])
1518 def test_getLogObserverDefaultFile(self):
1520 When daemonized and C{logfile} is empty, the observer returned by
1521 L{UnixAppLogger._getLogObserver} points at C{twistd.log} in the current
1524 logFiles = _patchFileLogObserver(self.patch)
1525 logger = UnixAppLogger({"logfile": "", "nodaemon": False})
1526 observer = logger._getLogObserver()
1528 self.assertEqual(len(logFiles), 1)
1529 self.assertEqual(logFiles[0].path,
1530 os.path.abspath("twistd.log"))
1533 def test_getLogObserverSyslog(self):
1535 If C{syslog} is set to C{True}, L{UnixAppLogger._getLogObserver} starts
1536 a L{syslog.SyslogObserver} with given C{prefix}.
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"}])
1546 test_getLogObserverSyslog.skip = "Syslog not available"