123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- Tests for L{twisted.application.runner._runner}.
- """
- from signal import SIGTERM
- from io import BytesIO
- import errno
- from twisted.logger import (
- LogLevel, LogPublisher, LogBeginner,
- FileLogObserver, FilteringLogObserver, LogLevelFilterPredicate,
- )
- from ...runner import _runner
- from .._exit import ExitStatus
- from .._pidfile import PIDFile, NonePIDFile
- from .._runner import Runner
- from .test_pidfile import DummyFilePath
- from .mockreactor import MockReactor
- import twisted.trial.unittest
- class RunnerTests(twisted.trial.unittest.TestCase):
- """
- Tests for L{Runner}.
- """
- def setUp(self):
- # Patch exit and kill so we can capture usage and prevent actual exits
- # and kills.
- self.exit = DummyExit()
- self.kill = DummyKill()
- self.patch(_runner, "exit", self.exit)
- self.patch(_runner, "kill", self.kill)
- # Patch getpid so we get a known result
- self.pid = 1337
- self.pidFileContent = u"{}\n".format(self.pid).encode("utf-8")
- # Patch globalLogBeginner so that we aren't trying to install multiple
- # global log observers.
- self.stdout = BytesIO()
- self.stderr = BytesIO()
- self.stdio = DummyStandardIO(self.stdout, self.stderr)
- self.warnings = DummyWarningsModule()
- self.globalLogPublisher = LogPublisher()
- self.globalLogBeginner = LogBeginner(
- self.globalLogPublisher,
- self.stdio.stderr, self.stdio,
- self.warnings,
- )
- self.patch(_runner, "stderr", self.stderr)
- self.patch(_runner, "globalLogBeginner", self.globalLogBeginner)
- def test_runInOrder(self):
- """
- L{Runner.run} calls the expected methods in order.
- """
- runner = DummyRunner({})
- runner.run()
- self.assertEqual(
- runner.calledMethods,
- [
- "killIfRequested",
- "startLogging",
- "startReactor",
- "reactorExited",
- ]
- )
- def test_runUsesPIDFile(self):
- """
- L{Runner.run} uses the provided PID file.
- """
- pidFile = DummyPIDFile()
- runner = DummyRunner(pidFile=pidFile)
- self.assertFalse(pidFile.entered)
- self.assertFalse(pidFile.exited)
- runner.run()
- self.assertTrue(pidFile.entered)
- self.assertTrue(pidFile.exited)
- def test_runAlreadyRunning(self):
- """
- L{Runner.run} exits with L{ExitStatus.EX_USAGE} and the expected
- message if a process is already running that corresponds to the given
- PID file.
- """
- pidFile = PIDFile(DummyFilePath(self.pidFileContent))
- pidFile.isRunning = lambda: True
- runner = DummyRunner(pidFile=pidFile)
- runner.run()
- self.assertEqual(self.exit.status, ExitStatus.EX_CONFIG)
- self.assertEqual(self.exit.message, "Already running.")
- def test_killNotRequested(self):
- """
- L{Runner.killIfRequested} when C{kill} is false doesn't exit and
- doesn't indiscriminately murder anyone.
- """
- runner = Runner({})
- runner.killIfRequested()
- self.assertEqual(self.kill.calls, [])
- self.assertFalse(self.exit.exited)
- def test_killRequestedWithoutPIDFile(self):
- """
- L{Runner.killIfRequested} when C{kill} is true but C{pidFile} is
- L{nonePIDFile} exits with L{ExitStatus.EX_USAGE} and the expected
- message; and also doesn't indiscriminately murder anyone.
- """
- runner = Runner(kill=True)
- runner.killIfRequested()
- self.assertEqual(self.kill.calls, [])
- self.assertEqual(self.exit.status, ExitStatus.EX_USAGE)
- self.assertEqual(self.exit.message, "No PID file specified.")
- def test_killRequestedWithPIDFile(self):
- """
- L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
- performs a targeted killing of the appropriate process.
- """
- pidFile = PIDFile(DummyFilePath(self.pidFileContent))
- runner = Runner(kill=True, pidFile=pidFile)
- runner.killIfRequested()
- self.assertEqual(self.kill.calls, [(self.pid, SIGTERM)])
- self.assertEqual(self.exit.status, ExitStatus.EX_OK)
- self.assertIdentical(self.exit.message, None)
- def test_killRequestedWithPIDFileCantRead(self):
- """
- L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
- that it can't read exits with L{ExitStatus.EX_IOERR}.
- """
- pidFile = PIDFile(DummyFilePath(None))
- def read():
- raise OSError(errno.EACCES, "Permission denied")
- pidFile.read = read
- runner = Runner(kill=True, pidFile=pidFile)
- runner.killIfRequested()
- self.assertEqual(self.exit.status, ExitStatus.EX_IOERR)
- self.assertEqual(self.exit.message, "Unable to read PID file.")
- def test_killRequestedWithPIDFileEmpty(self):
- """
- L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
- containing no value exits with L{ExitStatus.EX_DATAERR}.
- """
- pidFile = PIDFile(DummyFilePath(b""))
- runner = Runner(kill=True, pidFile=pidFile)
- runner.killIfRequested()
- self.assertEqual(self.exit.status, ExitStatus.EX_DATAERR)
- self.assertEqual(self.exit.message, "Invalid PID file.")
- def test_killRequestedWithPIDFileNotAnInt(self):
- """
- L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
- containing a non-integer value exits with L{ExitStatus.EX_DATAERR}.
- """
- pidFile = PIDFile(DummyFilePath(b"** totally not a number, dude **"))
- runner = Runner(kill=True, pidFile=pidFile)
- runner.killIfRequested()
- self.assertEqual(self.exit.status, ExitStatus.EX_DATAERR)
- self.assertEqual(self.exit.message, "Invalid PID file.")
- def test_startLogging(self):
- """
- L{Runner.startLogging} sets up a filtering observer with a log level
- predicate set to the given log level that contains a file observer of
- the given type which writes to the given file.
- """
- logFile = BytesIO()
- # Patch the log beginner so that we don't try to start the already
- # running (started by trial) logging system.
- class LogBeginner(object):
- def beginLoggingTo(self, observers):
- LogBeginner.observers = observers
- self.patch(_runner, "globalLogBeginner", LogBeginner())
- # Patch FilteringLogObserver so we can capture its arguments
- class MockFilteringLogObserver(FilteringLogObserver):
- def __init__(
- self, observer, predicates,
- negativeObserver=lambda event: None
- ):
- MockFilteringLogObserver.observer = observer
- MockFilteringLogObserver.predicates = predicates
- FilteringLogObserver.__init__(
- self, observer, predicates, negativeObserver
- )
- self.patch(_runner, "FilteringLogObserver", MockFilteringLogObserver)
- # Patch FileLogObserver so we can capture its arguments
- class MockFileLogObserver(FileLogObserver):
- def __init__(self, outFile):
- MockFileLogObserver.outFile = outFile
- FileLogObserver.__init__(self, outFile, str)
- # Start logging
- runner = Runner(
- defaultLogLevel=LogLevel.critical,
- logFile=logFile,
- fileLogObserverFactory=MockFileLogObserver,
- )
- runner.startLogging()
- # Check for a filtering observer
- self.assertEqual(len(LogBeginner.observers), 1)
- self.assertIsInstance(LogBeginner.observers[0], FilteringLogObserver)
- # Check log level predicate with the correct default log level
- self.assertEqual(len(MockFilteringLogObserver.predicates), 1)
- self.assertIsInstance(
- MockFilteringLogObserver.predicates[0],
- LogLevelFilterPredicate
- )
- self.assertIdentical(
- MockFilteringLogObserver.predicates[0].defaultLogLevel,
- LogLevel.critical
- )
- # Check for a file observer attached to the filtering observer
- self.assertIsInstance(
- MockFilteringLogObserver.observer, MockFileLogObserver
- )
- # Check for the file we gave it
- self.assertIdentical(
- MockFilteringLogObserver.observer.outFile, logFile
- )
- def test_startReactorWithoutReactor(self):
- """
- L{Runner.startReactor} without the C{reactor} argument runs the default
- reactor.
- """
- reactor = MockReactor(self)
- self.patch(_runner, "defaultReactor", reactor)
- runner = Runner()
- runner.startReactor()
- self.assertTrue(reactor.hasInstalled)
- self.assertTrue(reactor.hasRun)
- def test_startReactorWithReactor(self):
- """
- L{Runner.startReactor} with the C{reactor} argument runs the given
- reactor.
- """
- reactor = MockReactor(self)
- runner = Runner(reactor=reactor)
- runner.startReactor()
- self.assertTrue(reactor.hasRun)
- def test_startReactorWhenRunning(self):
- """
- L{Runner.startReactor} ensures that C{whenRunning} is called with
- C{whenRunningArguments} when the reactor is running.
- """
- self._testHook("whenRunning", "startReactor")
- def test_whenRunningWithArguments(self):
- """
- L{Runner.whenRunning} calls C{whenRunning} with
- C{whenRunningArguments}.
- """
- self._testHook("whenRunning")
- def test_reactorExitedWithArguments(self):
- """
- L{Runner.whenRunning} calls C{reactorExited} with
- C{reactorExitedArguments}.
- """
- self._testHook("reactorExited")
- def _testHook(self, methodName, callerName=None):
- """
- Verify that the named hook is run with the expected arguments as
- specified by the arguments used to create the L{Runner}, when the
- specified caller is invoked.
- @param methodName: The name of the hook to verify.
- @type methodName: L{str}
- @param callerName: The name of the method that is expected to cause the
- hook to be called.
- If C{None}, use the L{Runner} method with the same name as the
- hook.
- @type callerName: L{str}
- """
- if callerName is None:
- callerName = methodName
- arguments = dict(a=object(), b=object(), c=object())
- argumentsSeen = []
- def hook(**arguments):
- argumentsSeen.append(arguments)
- runnerArguments = {
- methodName: hook,
- "{}Arguments".format(methodName): arguments.copy(),
- }
- runner = Runner(reactor=MockReactor(self), **runnerArguments)
- hookCaller = getattr(runner, callerName)
- hookCaller()
- self.assertEqual(len(argumentsSeen), 1)
- self.assertEqual(argumentsSeen[0], arguments)
- class DummyRunner(Runner):
- """
- Stub for L{Runner}.
- Keep track of calls to some methods without actually doing anything.
- """
- def __init__(self, *args, **kwargs):
- Runner.__init__(self, *args, **kwargs)
- self.calledMethods = []
- def killIfRequested(self):
- self.calledMethods.append("killIfRequested")
- def startLogging(self):
- self.calledMethods.append("startLogging")
- def startReactor(self):
- self.calledMethods.append("startReactor")
- def reactorExited(self):
- self.calledMethods.append("reactorExited")
- class DummyPIDFile(NonePIDFile):
- """
- Stub for L{PIDFile}.
- Tracks context manager entry/exit without doing anything.
- """
- def __init__(self):
- NonePIDFile.__init__(self)
- self.entered = False
- self.exited = False
- def __enter__(self):
- self.entered = True
- return self
- def __exit__(self, excType, excValue, traceback):
- self.exited = True
- class DummyExit(object):
- """
- Stub for L{exit} that remembers whether it's been called and, if it has,
- what arguments it was given.
- """
- def __init__(self):
- self.exited = False
- def __call__(self, status, message=None):
- assert not self.exited
- self.status = status
- self.message = message
- self.exited = True
- class DummyKill(object):
- """
- Stub for L{os.kill} that remembers whether it's been called and, if it has,
- what arguments it was given.
- """
- def __init__(self):
- self.calls = []
- def __call__(self, pid, sig):
- self.calls.append((pid, sig))
- class DummyStandardIO(object):
- """
- Stub for L{sys} which provides L{BytesIO} streams as stdout and stderr.
- """
- def __init__(self, stdout, stderr):
- self.stdout = stdout
- self.stderr = stderr
- class DummyWarningsModule(object):
- """
- Stub for L{warnings} which provides a C{showwarning} method that is a no-op.
- """
- def showwarning(*args, **kwargs):
- """
- Do nothing.
- @param args: ignored.
- @param kwargs: ignored.
- """
|