Ausgabe der neuen DB Einträge

This commit is contained in:
hubobel 2022-01-02 21:50:48 +01:00
parent bad48e1627
commit cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions

View file

@ -0,0 +1,7 @@
# -*- test-case-name: twisted.application.runner.test -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Facilities for running a Twisted application.
"""

View file

@ -0,0 +1,138 @@
# -*- test-case-name: twisted.application.runner.test.test_exit -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
System exit support.
"""
from sys import stdout, stderr, exit as sysexit
from constantly import Values, ValueConstant
def exit(status, message=None):
"""
Exit the python interpreter with the given status and an optional message.
@param status: An exit status.
@type status: L{int} or L{ValueConstant} from L{ExitStatus}.
@param message: An options message to print.
@type status: L{str}
"""
if isinstance(status, ValueConstant):
code = status.value
else:
code = int(status)
if message:
if code == 0:
out = stdout
else:
out = stderr
out.write(message)
out.write("\n")
sysexit(code)
try:
import posix as Status
except ImportError:
class Status(object):
"""
Object to hang C{EX_*} values off of as a substitute for L{posix}.
"""
EX__BASE = 64
EX_OK = 0
EX_USAGE = EX__BASE
EX_DATAERR = EX__BASE + 1
EX_NOINPUT = EX__BASE + 2
EX_NOUSER = EX__BASE + 3
EX_NOHOST = EX__BASE + 4
EX_UNAVAILABLE = EX__BASE + 5
EX_SOFTWARE = EX__BASE + 6
EX_OSERR = EX__BASE + 7
EX_OSFILE = EX__BASE + 8
EX_CANTCREAT = EX__BASE + 9
EX_IOERR = EX__BASE + 10
EX_TEMPFAIL = EX__BASE + 11
EX_PROTOCOL = EX__BASE + 12
EX_NOPERM = EX__BASE + 13
EX_CONFIG = EX__BASE + 14
class ExitStatus(Values):
"""
Standard exit status codes for system programs.
@cvar EX_OK: Successful termination.
@type EX_OK: L{ValueConstant}
@cvar EX_USAGE: Command line usage error.
@type EX_USAGE: L{ValueConstant}
@cvar EX_DATAERR: Data format error.
@type EX_DATAERR: L{ValueConstant}
@cvar EX_NOINPUT: Cannot open input.
@type EX_NOINPUT: L{ValueConstant}
@cvar EX_NOUSER: Addressee unknown.
@type EX_NOUSER: L{ValueConstant}
@cvar EX_NOHOST: Host name unknown.
@type EX_NOHOST: L{ValueConstant}
@cvar EX_UNAVAILABLE: Service unavailable.
@type EX_UNAVAILABLE: L{ValueConstant}
@cvar EX_SOFTWARE: Internal software error.
@type EX_SOFTWARE: L{ValueConstant}
@cvar EX_OSERR: System error (e.g., can't fork).
@type EX_OSERR: L{ValueConstant}
@cvar EX_OSFILE: Critical OS file missing.
@type EX_OSFILE: L{ValueConstant}
@cvar EX_CANTCREAT: Can't create (user) output file.
@type EX_CANTCREAT: L{ValueConstant}
@cvar EX_IOERR: Input/output error.
@type EX_IOERR: L{ValueConstant}
@cvar EX_TEMPFAIL: Temporary failure; the user is invited to retry.
@type EX_TEMPFAIL: L{ValueConstant}
@cvar EX_PROTOCOL: Remote error in protocol.
@type EX_PROTOCOL: L{ValueConstant}
@cvar EX_NOPERM: Permission denied.
@type EX_NOPERM: L{ValueConstant}
@cvar EX_CONFIG: Configuration error.
@type EX_CONFIG: L{ValueConstant}
"""
EX_OK = ValueConstant(Status.EX_OK)
EX_USAGE = ValueConstant(Status.EX_USAGE)
EX_DATAERR = ValueConstant(Status.EX_DATAERR)
EX_NOINPUT = ValueConstant(Status.EX_NOINPUT)
EX_NOUSER = ValueConstant(Status.EX_NOUSER)
EX_NOHOST = ValueConstant(Status.EX_NOHOST)
EX_UNAVAILABLE = ValueConstant(Status.EX_UNAVAILABLE)
EX_SOFTWARE = ValueConstant(Status.EX_SOFTWARE)
EX_OSERR = ValueConstant(Status.EX_OSERR)
EX_OSFILE = ValueConstant(Status.EX_OSFILE)
EX_CANTCREAT = ValueConstant(Status.EX_CANTCREAT)
EX_IOERR = ValueConstant(Status.EX_IOERR)
EX_TEMPFAIL = ValueConstant(Status.EX_TEMPFAIL)
EX_PROTOCOL = ValueConstant(Status.EX_PROTOCOL)
EX_NOPERM = ValueConstant(Status.EX_NOPERM)
EX_CONFIG = ValueConstant(Status.EX_CONFIG)

View file

@ -0,0 +1,303 @@
# -*- test-case-name: twisted.application.runner.test.test_pidfile -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
PID file.
"""
import errno
from os import getpid, kill, name as SYSTEM_NAME
from zope.interface import Interface, implementer
from twisted.logger import Logger
class IPIDFile(Interface):
"""
Manages a file that remembers a process ID.
"""
def read():
"""
Read the process ID stored in this PID file.
@return: The contained process ID.
@rtype: L{int}
@raise NoPIDFound: If this PID file does not exist.
@raise EnvironmentError: If this PID file cannot be read.
@raise ValueError: If this PID file's content is invalid.
"""
def writeRunningPID():
"""
Store the PID of the current process in this PID file.
@raise EnvironmentError: If this PID file cannot be written.
"""
def remove():
"""
Remove this PID file.
@raise EnvironmentError: If this PID file cannot be removed.
"""
def isRunning():
"""
Determine whether there is a running process corresponding to the PID
in this PID file.
@return: True if this PID file contains a PID and a process with that
PID is currently running; false otherwise.
@rtype: L{bool}
@raise EnvironmentError: If this PID file cannot be read.
@raise InvalidPIDFileError: If this PID file's content is invalid.
@raise StalePIDFileError: If this PID file's content refers to a PID
for which there is no corresponding running process.
"""
def __enter__():
"""
Enter a context using this PIDFile.
Writes the PID file with the PID of the running process.
@raise AlreadyRunningError: A process corresponding to the PID in this
PID file is already running.
"""
def __exit__(excType, excValue, traceback):
"""
Exit a context using this PIDFile.
Removes the PID file.
"""
@implementer(IPIDFile)
class PIDFile(object):
"""
Concrete implementation of L{IPIDFile} based on C{IFilePath}.
This implementation is presently not supported on non-POSIX platforms.
Specifically, calling L{PIDFile.isRunning} will raise
L{NotImplementedError}.
"""
_log = Logger()
@staticmethod
def _format(pid):
"""
Format a PID file's content.
@param pid: A process ID.
@type pid: int
@return: Formatted PID file contents.
@rtype: L{bytes}
"""
return u"{}\n".format(int(pid)).encode("utf-8")
def __init__(self, filePath):
"""
@param filePath: The path to the PID file on disk.
@type filePath: L{IFilePath}
"""
self.filePath = filePath
def read(self):
pidString = b""
try:
with self.filePath.open() as fh:
for pidString in fh:
break
except OSError as e:
if e.errno == errno.ENOENT: # No such file
raise NoPIDFound("PID file does not exist")
raise
try:
return int(pidString)
except ValueError:
raise InvalidPIDFileError(
"non-integer PID value in PID file: {!r}".format(pidString)
)
def _write(self, pid):
"""
Store a PID in this PID file.
@param pid: A PID to store.
@type pid: L{int}
@raise EnvironmentError: If this PID file cannot be written.
"""
self.filePath.setContent(self._format(pid=pid))
def writeRunningPID(self):
self._write(getpid())
def remove(self):
self.filePath.remove()
def isRunning(self):
try:
pid = self.read()
except NoPIDFound:
return False
if SYSTEM_NAME == "posix":
return self._pidIsRunningPOSIX(pid)
else:
raise NotImplementedError(
"isRunning is not implemented on {}".format(SYSTEM_NAME)
)
@staticmethod
def _pidIsRunningPOSIX(pid):
"""
POSIX implementation for running process check.
Determine whether there is a running process corresponding to the given
PID.
@return: True if the given PID is currently running; false otherwise.
@rtype: L{bool}
@raise EnvironmentError: If this PID file cannot be read.
@raise InvalidPIDFileError: If this PID file's content is invalid.
@raise StalePIDFileError: If this PID file's content refers to a PID
for which there is no corresponding running process.
"""
try:
kill(pid, 0)
except OSError as e:
if e.errno == errno.ESRCH: # No such process
raise StalePIDFileError(
"PID file refers to non-existing process"
)
elif e.errno == errno.EPERM: # Not permitted to kill
return True
else:
raise
else:
return True
def __enter__(self):
try:
if self.isRunning():
raise AlreadyRunningError()
except StalePIDFileError:
self._log.info("Replacing stale PID file: {log_source}")
self.writeRunningPID()
return self
def __exit__(self, excType, excValue, traceback):
self.remove()
@implementer(IPIDFile)
class NonePIDFile(object):
"""
PID file implementation that does nothing.
This is meant to be used as a "active None" object in place of a PID file
when no PID file is desired.
"""
def __init__(self):
pass
def read(self):
raise NoPIDFound("PID file does not exist")
def _write(self, pid):
"""
Store a PID in this PID file.
@param pid: A PID to store.
@type pid: L{int}
@raise EnvironmentError: If this PID file cannot be written.
@note: This implementation always raises an L{EnvironmentError}.
"""
raise OSError(errno.EPERM, "Operation not permitted")
def writeRunningPID(self):
self._write(0)
def remove(self):
raise OSError(errno.ENOENT, "No such file or directory")
def isRunning(self):
return False
def __enter__(self):
return self
def __exit__(self, excType, excValue, traceback):
pass
nonePIDFile = NonePIDFile()
class AlreadyRunningError(Exception):
"""
Process is already running.
"""
class InvalidPIDFileError(Exception):
"""
PID file contents are invalid.
"""
class StalePIDFileError(Exception):
"""
PID file contents are valid, but there is no process with the referenced
PID.
"""
class NoPIDFound(Exception):
"""
No PID found in PID file.
"""

View file

@ -0,0 +1,185 @@
# -*- test-case-name: twisted.application.runner.test.test_runner -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Twisted application runner.
"""
from sys import stderr
from signal import SIGTERM
from os import kill
from attr import attrib, attrs, Factory
from twisted.logger import (
globalLogBeginner, textFileLogObserver,
FilteringLogObserver, LogLevelFilterPredicate,
LogLevel, Logger,
)
from ._exit import exit, ExitStatus
from ._pidfile import nonePIDFile, AlreadyRunningError, InvalidPIDFileError
@attrs(frozen=True)
class Runner(object):
"""
Twisted application runner.
@cvar _log: The logger attached to this class.
@type _log: L{Logger}
@ivar _reactor: The reactor to start and run the application in.
@type _reactor: L{IReactorCore}
@ivar _pidFile: The file to store the running process ID in.
@type _pidFile: L{IPIDFile}
@ivar _kill: Whether this runner should kill an existing running
instance of the application.
@type _kill: L{bool}
@ivar _defaultLogLevel: The default log level to start the logging
system with.
@type _defaultLogLevel: L{constantly.NamedConstant} from L{LogLevel}
@ivar _logFile: A file stream to write logging output to.
@type _logFile: writable file-like object
@ivar _fileLogObserverFactory: A factory for the file log observer to
use when starting the logging system.
@type _pidFile: callable that takes a single writable file-like object
argument and returns a L{twisted.logger.FileLogObserver}
@ivar _whenRunning: Hook to call after the reactor is running;
this is where the application code that relies on the reactor gets
called.
@type _whenRunning: callable that takes the keyword arguments specified
by C{whenRunningArguments}
@ivar _whenRunningArguments: Keyword arguments to pass to
C{whenRunning} when it is called.
@type _whenRunningArguments: L{dict}
@ivar _reactorExited: Hook to call after the reactor exits.
@type _reactorExited: callable that takes the keyword arguments
specified by C{reactorExitedArguments}
@ivar _reactorExitedArguments: Keyword arguments to pass to
C{reactorExited} when it is called.
@type _reactorExitedArguments: L{dict}
"""
_log = Logger()
_reactor = attrib()
_pidFile = attrib(default=nonePIDFile)
_kill = attrib(default=False)
_defaultLogLevel = attrib(default=LogLevel.info)
_logFile = attrib(default=stderr)
_fileLogObserverFactory = attrib(default=textFileLogObserver)
_whenRunning = attrib(default=lambda **_: None)
_whenRunningArguments = attrib(default=Factory(dict))
_reactorExited = attrib(default=lambda **_: None)
_reactorExitedArguments = attrib(default=Factory(dict))
def run(self):
"""
Run this command.
"""
pidFile = self._pidFile
self.killIfRequested()
try:
with pidFile:
self.startLogging()
self.startReactor()
self.reactorExited()
except AlreadyRunningError:
exit(ExitStatus.EX_CONFIG, "Already running.")
return # When testing, patched exit doesn't exit
def killIfRequested(self):
"""
If C{self._kill} is true, attempt to kill a running instance of the
application.
"""
pidFile = self._pidFile
if self._kill:
if pidFile is nonePIDFile:
exit(ExitStatus.EX_USAGE, "No PID file specified.")
return # When testing, patched exit doesn't exit
try:
pid = pidFile.read()
except EnvironmentError:
exit(ExitStatus.EX_IOERR, "Unable to read PID file.")
return # When testing, patched exit doesn't exit
except InvalidPIDFileError:
exit(ExitStatus.EX_DATAERR, "Invalid PID file.")
return # When testing, patched exit doesn't exit
self.startLogging()
self._log.info("Terminating process: {pid}", pid=pid)
kill(pid, SIGTERM)
exit(ExitStatus.EX_OK)
return # When testing, patched exit doesn't exit
def startLogging(self):
"""
Start the L{twisted.logger} logging system.
"""
logFile = self._logFile
fileLogObserverFactory = self._fileLogObserverFactory
fileLogObserver = fileLogObserverFactory(logFile)
logLevelPredicate = LogLevelFilterPredicate(
defaultLogLevel=self._defaultLogLevel
)
filteringObserver = FilteringLogObserver(
fileLogObserver, [logLevelPredicate]
)
globalLogBeginner.beginLoggingTo([filteringObserver])
def startReactor(self):
"""
Register C{self._whenRunning} with the reactor so that it is called
once the reactor is running, then start the reactor.
"""
self._reactor.callWhenRunning(self.whenRunning)
self._log.info("Starting reactor...")
self._reactor.run()
def whenRunning(self):
"""
Call C{self._whenRunning} with C{self._whenRunningArguments}.
@note: This method is called after the reactor starts running.
"""
self._whenRunning(**self._whenRunningArguments)
def reactorExited(self):
"""
Call C{self._reactorExited} with C{self._reactorExitedArguments}.
@note: This method is called after the reactor exits.
"""
self._reactorExited(**self._reactorExitedArguments)

View file

@ -0,0 +1,7 @@
# -*- test-case-name: twisted.application.runner.test -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.runner}.
"""

View file

@ -0,0 +1,104 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.runner._exit}.
"""
from twisted.python.compat import NativeStringIO
from ...runner import _exit
from .._exit import exit, ExitStatus
import twisted.trial.unittest
class ExitTests(twisted.trial.unittest.TestCase):
"""
Tests for L{exit}.
"""
def setUp(self):
self.exit = DummyExit()
self.patch(_exit, "sysexit", self.exit)
def test_exitStatusInt(self):
"""
L{exit} given an L{int} status code will pass it to L{sys.exit}.
"""
status = 1234
exit(status)
self.assertEqual(self.exit.arg, status)
def test_exitStatusStringNotInt(self):
"""
L{exit} given a L{str} status code that isn't a string integer raises
L{ValueError}.
"""
self.assertRaises(ValueError, exit, "foo")
def test_exitStatusStringInt(self):
"""
L{exit} given a L{str} status code that is a string integer passes the
corresponding L{int} to L{sys.exit}.
"""
exit("1234")
self.assertEqual(self.exit.arg, 1234)
def test_exitConstant(self):
"""
L{exit} given a L{ValueConstant} status code passes the corresponding
value to L{sys.exit}.
"""
status = ExitStatus.EX_CONFIG
exit(status)
self.assertEqual(self.exit.arg, status.value)
def test_exitMessageZero(self):
"""
L{exit} given a status code of zero (C{0}) writes the given message to
standard output.
"""
out = NativeStringIO()
self.patch(_exit, "stdout", out)
message = "Hello, world."
exit(0, message)
self.assertEqual(out.getvalue(), message + "\n")
def test_exitMessageNonZero(self):
"""
L{exit} given a non-zero status code writes the given message to
standard error.
"""
out = NativeStringIO()
self.patch(_exit, "stderr", out)
message = "Hello, world."
exit(64, message)
self.assertEqual(out.getvalue(), message + "\n")
class DummyExit(object):
"""
Stub for L{sys.exit} that remembers whether it's been called and, if it
has, what argument it was given.
"""
def __init__(self):
self.exited = False
def __call__(self, arg=None):
assert not self.exited
self.arg = arg
self.exited = True

View file

@ -0,0 +1,476 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.application.runner._pidfile}.
"""
from functools import wraps
import errno
from os import getpid, name as SYSTEM_NAME
from io import BytesIO
from zope.interface import implementer
from zope.interface.verify import verifyObject
from twisted.python.filepath import IFilePath
from twisted.python.runtime import platform
from ...runner import _pidfile
from .._pidfile import (
IPIDFile, PIDFile, NonePIDFile,
AlreadyRunningError, InvalidPIDFileError, StalePIDFileError,
NoPIDFound,
)
import twisted.trial.unittest
from twisted.trial.unittest import SkipTest
def ifPlatformSupported(f):
"""
Decorator for tests that are not expected to work on all platforms.
Calling L{PIDFile.isRunning} currently raises L{NotImplementedError} on
non-POSIX platforms.
On an unsupported platform, we expect to see any test that calls
L{PIDFile.isRunning} to raise either L{NotImplementedError}, L{SkipTest},
or C{self.failureException}.
(C{self.failureException} may occur in a test that checks for a specific
exception but it gets NotImplementedError instead.)
@param f: The test method to decorate.
@type f: method
@return: The wrapped callable.
"""
@wraps(f)
def wrapper(self, *args, **kwargs):
supported = platform.getType() == "posix"
if supported:
return f(self, *args, **kwargs)
else:
e = self.assertRaises(
(NotImplementedError, SkipTest, self.failureException),
f, self, *args, **kwargs
)
if isinstance(e, NotImplementedError):
self.assertTrue(
str(e).startswith("isRunning is not implemented on ")
)
return wrapper
class PIDFileTests(twisted.trial.unittest.TestCase):
"""
Tests for L{PIDFile}.
"""
def test_interface(self):
"""
L{PIDFile} conforms to L{IPIDFile}.
"""
pidFile = PIDFile(DummyFilePath())
verifyObject(IPIDFile, pidFile)
def test_formatWithPID(self):
"""
L{PIDFile._format} returns the expected format when given a PID.
"""
self.assertEqual(PIDFile._format(pid=1337), b"1337\n")
def test_readWithPID(self):
"""
L{PIDFile.read} returns the PID from the given file path.
"""
pid = 1337
pidFile = PIDFile(DummyFilePath(PIDFile._format(pid=pid)))
self.assertEqual(pid, pidFile.read())
def test_readEmptyPID(self):
"""
L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
path.
"""
pidValue = b""
pidFile = PIDFile(DummyFilePath(b""))
e = self.assertRaises(InvalidPIDFileError, pidFile.read)
self.assertEqual(
str(e),
"non-integer PID value in PID file: {!r}".format(pidValue)
)
def test_readWithBogusPID(self):
"""
L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
path.
"""
pidValue = b"$foo!"
pidFile = PIDFile(DummyFilePath(pidValue))
e = self.assertRaises(InvalidPIDFileError, pidFile.read)
self.assertEqual(
str(e),
"non-integer PID value in PID file: {!r}".format(pidValue)
)
def test_readDoesntExist(self):
"""
L{PIDFile.read} raises L{NoPIDFound} when given a non-existing file
path.
"""
pidFile = PIDFile(DummyFilePath())
e = self.assertRaises(NoPIDFound, pidFile.read)
self.assertEqual(str(e), "PID file does not exist")
def test_readOpenRaisesOSErrorNotENOENT(self):
"""
L{PIDFile.read} re-raises L{OSError} if the associated C{errno} is
anything other than L{errno.ENOENT}.
"""
def oops(mode="r"):
raise OSError(errno.EIO, "I/O error")
self.patch(DummyFilePath, "open", oops)
pidFile = PIDFile(DummyFilePath())
error = self.assertRaises(OSError, pidFile.read)
self.assertEqual(error.errno, errno.EIO)
def test_writePID(self):
"""
L{PIDFile._write} stores the given PID.
"""
pid = 1995
pidFile = PIDFile(DummyFilePath())
pidFile._write(pid)
self.assertEqual(pidFile.read(), pid)
def test_writePIDInvalid(self):
"""
L{PIDFile._write} raises L{ValueError} when given an invalid PID.
"""
pidFile = PIDFile(DummyFilePath())
self.assertRaises(ValueError, pidFile._write, u"burp")
def test_writeRunningPID(self):
"""
L{PIDFile.writeRunningPID} stores the PID for the current process.
"""
pidFile = PIDFile(DummyFilePath())
pidFile.writeRunningPID()
self.assertEqual(pidFile.read(), getpid())
def test_remove(self):
"""
L{PIDFile.remove} removes the PID file.
"""
pidFile = PIDFile(DummyFilePath(b""))
self.assertTrue(pidFile.filePath.exists())
pidFile.remove()
self.assertFalse(pidFile.filePath.exists())
@ifPlatformSupported
def test_isRunningDoesExist(self):
"""
L{PIDFile.isRunning} returns true for a process that does exist.
"""
pidFile = PIDFile(DummyFilePath())
pidFile._write(1337)
def kill(pid, signal):
return # Don't actually kill anything
self.patch(_pidfile, "kill", kill)
self.assertTrue(pidFile.isRunning())
@ifPlatformSupported
def test_isRunningThis(self):
"""
L{PIDFile.isRunning} returns true for this process (which is running).
@note: This differs from L{PIDFileTests.test_isRunningDoesExist} in
that it actually invokes the C{kill} system call, which is useful for
testing of our chosen method for probing the existence of a process.
"""
pidFile = PIDFile(DummyFilePath())
pidFile.writeRunningPID()
self.assertTrue(pidFile.isRunning())
@ifPlatformSupported
def test_isRunningDoesNotExist(self):
"""
L{PIDFile.isRunning} raises L{StalePIDFileError} for a process that
does not exist (errno=ESRCH).
"""
pidFile = PIDFile(DummyFilePath())
pidFile._write(1337)
def kill(pid, signal):
raise OSError(errno.ESRCH, "No such process")
self.patch(_pidfile, "kill", kill)
self.assertRaises(StalePIDFileError, pidFile.isRunning)
@ifPlatformSupported
def test_isRunningNotAllowed(self):
"""
L{PIDFile.isRunning} returns true for a process that we are not allowed
to kill (errno=EPERM).
"""
pidFile = PIDFile(DummyFilePath())
pidFile._write(1337)
def kill(pid, signal):
raise OSError(errno.EPERM, "Operation not permitted")
self.patch(_pidfile, "kill", kill)
self.assertTrue(pidFile.isRunning())
@ifPlatformSupported
def test_isRunningInit(self):
"""
L{PIDFile.isRunning} returns true for a process that we are not allowed
to kill (errno=EPERM).
@note: This differs from L{PIDFileTests.test_isRunningNotAllowed} in
that it actually invokes the C{kill} system call, which is useful for
testing of our chosen method for probing the existence of a process
that we are not allowed to kill.
@note: In this case, we try killing C{init}, which is process #1 on
POSIX systems, so this test is not portable. C{init} should always be
running and should not be killable by non-root users.
"""
if SYSTEM_NAME != "posix":
raise SkipTest("This test assumes POSIX")
pidFile = PIDFile(DummyFilePath())
pidFile._write(1) # PID 1 is init on POSIX systems
self.assertTrue(pidFile.isRunning())
@ifPlatformSupported
def test_isRunningUnknownErrno(self):
"""
L{PIDFile.isRunning} re-raises L{OSError} if the attached C{errno}
value from L{os.kill} is not an expected one.
"""
pidFile = PIDFile(DummyFilePath())
pidFile.writeRunningPID()
def kill(pid, signal):
raise OSError(errno.EEXIST, "File exists")
self.patch(_pidfile, "kill", kill)
self.assertRaises(OSError, pidFile.isRunning)
def test_isRunningNoPIDFile(self):
"""
L{PIDFile.isRunning} returns false if the PID file doesn't exist.
"""
pidFile = PIDFile(DummyFilePath())
self.assertFalse(pidFile.isRunning())
def test_contextManager(self):
"""
When used as a context manager, a L{PIDFile} will store the current pid
on entry, then removes the PID file on exit.
"""
pidFile = PIDFile(DummyFilePath())
self.assertFalse(pidFile.filePath.exists())
with pidFile:
self.assertTrue(pidFile.filePath.exists())
self.assertEqual(pidFile.read(), getpid())
self.assertFalse(pidFile.filePath.exists())
@ifPlatformSupported
def test_contextManagerDoesntExist(self):
"""
When used as a context manager, a L{PIDFile} will replace the
underlying PIDFile rather than raising L{AlreadyRunningError} if the
contained PID file exists but refers to a non-running PID.
"""
pidFile = PIDFile(DummyFilePath())
pidFile._write(1337)
def kill(pid, signal):
raise OSError(errno.ESRCH, "No such process")
self.patch(_pidfile, "kill", kill)
e = self.assertRaises(StalePIDFileError, pidFile.isRunning)
self.assertEqual(str(e), "PID file refers to non-existing process")
with pidFile:
self.assertEqual(pidFile.read(), getpid())
@ifPlatformSupported
def test_contextManagerAlreadyRunning(self):
"""
When used as a context manager, a L{PIDFile} will raise
L{AlreadyRunningError} if the there is already a running process with
the contained PID.
"""
pidFile = PIDFile(DummyFilePath())
pidFile._write(1337)
def kill(pid, signal):
return # Don't actually kill anything
self.patch(_pidfile, "kill", kill)
self.assertTrue(pidFile.isRunning())
self.assertRaises(AlreadyRunningError, pidFile.__enter__)
class NonePIDFileTests(twisted.trial.unittest.TestCase):
"""
Tests for L{NonePIDFile}.
"""
def test_interface(self):
"""
L{NonePIDFile} conforms to L{IPIDFile}.
"""
pidFile = NonePIDFile()
verifyObject(IPIDFile, pidFile)
def test_read(self):
"""
L{NonePIDFile.read} raises L{NoPIDFound}.
"""
pidFile = NonePIDFile()
e = self.assertRaises(NoPIDFound, pidFile.read)
self.assertEqual(str(e), "PID file does not exist")
def test_write(self):
"""
L{NonePIDFile._write} raises L{OSError} with an errno of L{errno.EPERM}.
"""
pidFile = NonePIDFile()
error = self.assertRaises(OSError, pidFile._write, 0)
self.assertEqual(error.errno, errno.EPERM)
def test_writeRunningPID(self):
"""
L{NonePIDFile.writeRunningPID} raises L{OSError} with an errno of
L{errno.EPERM}.
"""
pidFile = NonePIDFile()
error = self.assertRaises(OSError, pidFile.writeRunningPID)
self.assertEqual(error.errno, errno.EPERM)
def test_remove(self):
"""
L{NonePIDFile.remove} raises L{OSError} with an errno of L{errno.EPERM}.
"""
pidFile = NonePIDFile()
error = self.assertRaises(OSError, pidFile.remove)
self.assertEqual(error.errno, errno.ENOENT)
def test_isRunning(self):
"""
L{NonePIDFile.isRunning} returns L{False}.
"""
pidFile = NonePIDFile()
self.assertEqual(pidFile.isRunning(), False)
def test_contextManager(self):
"""
When used as a context manager, a L{NonePIDFile} doesn't raise, despite
not existing.
"""
pidFile = NonePIDFile()
with pidFile:
pass
@implementer(IFilePath)
class DummyFilePath(object):
"""
In-memory L{IFilePath}.
"""
def __init__(self, content=None):
self.setContent(content)
def open(self, mode="r"):
if not self._exists:
raise OSError(errno.ENOENT, "No such file or directory")
return BytesIO(self.getContent())
def setContent(self, content):
self._exists = content is not None
self._content = content
def getContent(self):
return self._content
def remove(self):
self.setContent(None)
def exists(self):
return self._exists

View file

@ -0,0 +1,460 @@
# 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 attr import attrib, attrs, Factory
from twisted.logger import (
LogLevel, LogPublisher, LogBeginner,
FileLogObserver, FilteringLogObserver, LogLevelFilterPredicate,
)
from twisted.internet.testing import MemoryReactor
from ...runner import _runner
from .._exit import ExitStatus
from .._pidfile import PIDFile, NonePIDFile
from .._runner import Runner
from .test_pidfile import DummyFilePath
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(reactor=MemoryReactor())
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 = Runner(reactor=MemoryReactor(), 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 = Runner(reactor=MemoryReactor(), 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(reactor=MemoryReactor())
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(reactor=MemoryReactor(), 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(reactor=MemoryReactor(), 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(reactor=MemoryReactor(), 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(reactor=MemoryReactor(), 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(reactor=MemoryReactor(), 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(
reactor=MemoryReactor(),
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_startReactorWithReactor(self):
"""
L{Runner.startReactor} with the C{reactor} argument runs the given
reactor.
"""
reactor = MemoryReactor()
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=MemoryReactor(), **runnerArguments)
hookCaller = getattr(runner, callerName)
hookCaller()
self.assertEqual(len(argumentsSeen), 1)
self.assertEqual(argumentsSeen[0], arguments)
@attrs(frozen=True)
class DummyRunner(Runner):
"""
Stub for L{Runner}.
Keep track of calls to some methods without actually doing anything.
"""
calledMethods = attrib(default=Factory(list))
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.
"""