test_runner.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for L{twisted.application.runner._runner}.
  5. """
  6. from signal import SIGTERM
  7. from io import BytesIO
  8. import errno
  9. from twisted.logger import (
  10. LogLevel, LogPublisher, LogBeginner,
  11. FileLogObserver, FilteringLogObserver, LogLevelFilterPredicate,
  12. )
  13. from ...runner import _runner
  14. from .._exit import ExitStatus
  15. from .._pidfile import PIDFile, NonePIDFile
  16. from .._runner import Runner
  17. from .test_pidfile import DummyFilePath
  18. from .mockreactor import MockReactor
  19. import twisted.trial.unittest
  20. class RunnerTests(twisted.trial.unittest.TestCase):
  21. """
  22. Tests for L{Runner}.
  23. """
  24. def setUp(self):
  25. # Patch exit and kill so we can capture usage and prevent actual exits
  26. # and kills.
  27. self.exit = DummyExit()
  28. self.kill = DummyKill()
  29. self.patch(_runner, "exit", self.exit)
  30. self.patch(_runner, "kill", self.kill)
  31. # Patch getpid so we get a known result
  32. self.pid = 1337
  33. self.pidFileContent = u"{}\n".format(self.pid).encode("utf-8")
  34. # Patch globalLogBeginner so that we aren't trying to install multiple
  35. # global log observers.
  36. self.stdout = BytesIO()
  37. self.stderr = BytesIO()
  38. self.stdio = DummyStandardIO(self.stdout, self.stderr)
  39. self.warnings = DummyWarningsModule()
  40. self.globalLogPublisher = LogPublisher()
  41. self.globalLogBeginner = LogBeginner(
  42. self.globalLogPublisher,
  43. self.stdio.stderr, self.stdio,
  44. self.warnings,
  45. )
  46. self.patch(_runner, "stderr", self.stderr)
  47. self.patch(_runner, "globalLogBeginner", self.globalLogBeginner)
  48. def test_runInOrder(self):
  49. """
  50. L{Runner.run} calls the expected methods in order.
  51. """
  52. runner = DummyRunner({})
  53. runner.run()
  54. self.assertEqual(
  55. runner.calledMethods,
  56. [
  57. "killIfRequested",
  58. "startLogging",
  59. "startReactor",
  60. "reactorExited",
  61. ]
  62. )
  63. def test_runUsesPIDFile(self):
  64. """
  65. L{Runner.run} uses the provided PID file.
  66. """
  67. pidFile = DummyPIDFile()
  68. runner = DummyRunner(pidFile=pidFile)
  69. self.assertFalse(pidFile.entered)
  70. self.assertFalse(pidFile.exited)
  71. runner.run()
  72. self.assertTrue(pidFile.entered)
  73. self.assertTrue(pidFile.exited)
  74. def test_runAlreadyRunning(self):
  75. """
  76. L{Runner.run} exits with L{ExitStatus.EX_USAGE} and the expected
  77. message if a process is already running that corresponds to the given
  78. PID file.
  79. """
  80. pidFile = PIDFile(DummyFilePath(self.pidFileContent))
  81. pidFile.isRunning = lambda: True
  82. runner = DummyRunner(pidFile=pidFile)
  83. runner.run()
  84. self.assertEqual(self.exit.status, ExitStatus.EX_CONFIG)
  85. self.assertEqual(self.exit.message, "Already running.")
  86. def test_killNotRequested(self):
  87. """
  88. L{Runner.killIfRequested} when C{kill} is false doesn't exit and
  89. doesn't indiscriminately murder anyone.
  90. """
  91. runner = Runner({})
  92. runner.killIfRequested()
  93. self.assertEqual(self.kill.calls, [])
  94. self.assertFalse(self.exit.exited)
  95. def test_killRequestedWithoutPIDFile(self):
  96. """
  97. L{Runner.killIfRequested} when C{kill} is true but C{pidFile} is
  98. L{nonePIDFile} exits with L{ExitStatus.EX_USAGE} and the expected
  99. message; and also doesn't indiscriminately murder anyone.
  100. """
  101. runner = Runner(kill=True)
  102. runner.killIfRequested()
  103. self.assertEqual(self.kill.calls, [])
  104. self.assertEqual(self.exit.status, ExitStatus.EX_USAGE)
  105. self.assertEqual(self.exit.message, "No PID file specified.")
  106. def test_killRequestedWithPIDFile(self):
  107. """
  108. L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
  109. performs a targeted killing of the appropriate process.
  110. """
  111. pidFile = PIDFile(DummyFilePath(self.pidFileContent))
  112. runner = Runner(kill=True, pidFile=pidFile)
  113. runner.killIfRequested()
  114. self.assertEqual(self.kill.calls, [(self.pid, SIGTERM)])
  115. self.assertEqual(self.exit.status, ExitStatus.EX_OK)
  116. self.assertIdentical(self.exit.message, None)
  117. def test_killRequestedWithPIDFileCantRead(self):
  118. """
  119. L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
  120. that it can't read exits with L{ExitStatus.EX_IOERR}.
  121. """
  122. pidFile = PIDFile(DummyFilePath(None))
  123. def read():
  124. raise OSError(errno.EACCES, "Permission denied")
  125. pidFile.read = read
  126. runner = Runner(kill=True, pidFile=pidFile)
  127. runner.killIfRequested()
  128. self.assertEqual(self.exit.status, ExitStatus.EX_IOERR)
  129. self.assertEqual(self.exit.message, "Unable to read PID file.")
  130. def test_killRequestedWithPIDFileEmpty(self):
  131. """
  132. L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
  133. containing no value exits with L{ExitStatus.EX_DATAERR}.
  134. """
  135. pidFile = PIDFile(DummyFilePath(b""))
  136. runner = Runner(kill=True, pidFile=pidFile)
  137. runner.killIfRequested()
  138. self.assertEqual(self.exit.status, ExitStatus.EX_DATAERR)
  139. self.assertEqual(self.exit.message, "Invalid PID file.")
  140. def test_killRequestedWithPIDFileNotAnInt(self):
  141. """
  142. L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
  143. containing a non-integer value exits with L{ExitStatus.EX_DATAERR}.
  144. """
  145. pidFile = PIDFile(DummyFilePath(b"** totally not a number, dude **"))
  146. runner = Runner(kill=True, pidFile=pidFile)
  147. runner.killIfRequested()
  148. self.assertEqual(self.exit.status, ExitStatus.EX_DATAERR)
  149. self.assertEqual(self.exit.message, "Invalid PID file.")
  150. def test_startLogging(self):
  151. """
  152. L{Runner.startLogging} sets up a filtering observer with a log level
  153. predicate set to the given log level that contains a file observer of
  154. the given type which writes to the given file.
  155. """
  156. logFile = BytesIO()
  157. # Patch the log beginner so that we don't try to start the already
  158. # running (started by trial) logging system.
  159. class LogBeginner(object):
  160. def beginLoggingTo(self, observers):
  161. LogBeginner.observers = observers
  162. self.patch(_runner, "globalLogBeginner", LogBeginner())
  163. # Patch FilteringLogObserver so we can capture its arguments
  164. class MockFilteringLogObserver(FilteringLogObserver):
  165. def __init__(
  166. self, observer, predicates,
  167. negativeObserver=lambda event: None
  168. ):
  169. MockFilteringLogObserver.observer = observer
  170. MockFilteringLogObserver.predicates = predicates
  171. FilteringLogObserver.__init__(
  172. self, observer, predicates, negativeObserver
  173. )
  174. self.patch(_runner, "FilteringLogObserver", MockFilteringLogObserver)
  175. # Patch FileLogObserver so we can capture its arguments
  176. class MockFileLogObserver(FileLogObserver):
  177. def __init__(self, outFile):
  178. MockFileLogObserver.outFile = outFile
  179. FileLogObserver.__init__(self, outFile, str)
  180. # Start logging
  181. runner = Runner(
  182. defaultLogLevel=LogLevel.critical,
  183. logFile=logFile,
  184. fileLogObserverFactory=MockFileLogObserver,
  185. )
  186. runner.startLogging()
  187. # Check for a filtering observer
  188. self.assertEqual(len(LogBeginner.observers), 1)
  189. self.assertIsInstance(LogBeginner.observers[0], FilteringLogObserver)
  190. # Check log level predicate with the correct default log level
  191. self.assertEqual(len(MockFilteringLogObserver.predicates), 1)
  192. self.assertIsInstance(
  193. MockFilteringLogObserver.predicates[0],
  194. LogLevelFilterPredicate
  195. )
  196. self.assertIdentical(
  197. MockFilteringLogObserver.predicates[0].defaultLogLevel,
  198. LogLevel.critical
  199. )
  200. # Check for a file observer attached to the filtering observer
  201. self.assertIsInstance(
  202. MockFilteringLogObserver.observer, MockFileLogObserver
  203. )
  204. # Check for the file we gave it
  205. self.assertIdentical(
  206. MockFilteringLogObserver.observer.outFile, logFile
  207. )
  208. def test_startReactorWithoutReactor(self):
  209. """
  210. L{Runner.startReactor} without the C{reactor} argument runs the default
  211. reactor.
  212. """
  213. reactor = MockReactor(self)
  214. self.patch(_runner, "defaultReactor", reactor)
  215. runner = Runner()
  216. runner.startReactor()
  217. self.assertTrue(reactor.hasInstalled)
  218. self.assertTrue(reactor.hasRun)
  219. def test_startReactorWithReactor(self):
  220. """
  221. L{Runner.startReactor} with the C{reactor} argument runs the given
  222. reactor.
  223. """
  224. reactor = MockReactor(self)
  225. runner = Runner(reactor=reactor)
  226. runner.startReactor()
  227. self.assertTrue(reactor.hasRun)
  228. def test_startReactorWhenRunning(self):
  229. """
  230. L{Runner.startReactor} ensures that C{whenRunning} is called with
  231. C{whenRunningArguments} when the reactor is running.
  232. """
  233. self._testHook("whenRunning", "startReactor")
  234. def test_whenRunningWithArguments(self):
  235. """
  236. L{Runner.whenRunning} calls C{whenRunning} with
  237. C{whenRunningArguments}.
  238. """
  239. self._testHook("whenRunning")
  240. def test_reactorExitedWithArguments(self):
  241. """
  242. L{Runner.whenRunning} calls C{reactorExited} with
  243. C{reactorExitedArguments}.
  244. """
  245. self._testHook("reactorExited")
  246. def _testHook(self, methodName, callerName=None):
  247. """
  248. Verify that the named hook is run with the expected arguments as
  249. specified by the arguments used to create the L{Runner}, when the
  250. specified caller is invoked.
  251. @param methodName: The name of the hook to verify.
  252. @type methodName: L{str}
  253. @param callerName: The name of the method that is expected to cause the
  254. hook to be called.
  255. If C{None}, use the L{Runner} method with the same name as the
  256. hook.
  257. @type callerName: L{str}
  258. """
  259. if callerName is None:
  260. callerName = methodName
  261. arguments = dict(a=object(), b=object(), c=object())
  262. argumentsSeen = []
  263. def hook(**arguments):
  264. argumentsSeen.append(arguments)
  265. runnerArguments = {
  266. methodName: hook,
  267. "{}Arguments".format(methodName): arguments.copy(),
  268. }
  269. runner = Runner(reactor=MockReactor(self), **runnerArguments)
  270. hookCaller = getattr(runner, callerName)
  271. hookCaller()
  272. self.assertEqual(len(argumentsSeen), 1)
  273. self.assertEqual(argumentsSeen[0], arguments)
  274. class DummyRunner(Runner):
  275. """
  276. Stub for L{Runner}.
  277. Keep track of calls to some methods without actually doing anything.
  278. """
  279. def __init__(self, *args, **kwargs):
  280. Runner.__init__(self, *args, **kwargs)
  281. self.calledMethods = []
  282. def killIfRequested(self):
  283. self.calledMethods.append("killIfRequested")
  284. def startLogging(self):
  285. self.calledMethods.append("startLogging")
  286. def startReactor(self):
  287. self.calledMethods.append("startReactor")
  288. def reactorExited(self):
  289. self.calledMethods.append("reactorExited")
  290. class DummyPIDFile(NonePIDFile):
  291. """
  292. Stub for L{PIDFile}.
  293. Tracks context manager entry/exit without doing anything.
  294. """
  295. def __init__(self):
  296. NonePIDFile.__init__(self)
  297. self.entered = False
  298. self.exited = False
  299. def __enter__(self):
  300. self.entered = True
  301. return self
  302. def __exit__(self, excType, excValue, traceback):
  303. self.exited = True
  304. class DummyExit(object):
  305. """
  306. Stub for L{exit} that remembers whether it's been called and, if it has,
  307. what arguments it was given.
  308. """
  309. def __init__(self):
  310. self.exited = False
  311. def __call__(self, status, message=None):
  312. assert not self.exited
  313. self.status = status
  314. self.message = message
  315. self.exited = True
  316. class DummyKill(object):
  317. """
  318. Stub for L{os.kill} that remembers whether it's been called and, if it has,
  319. what arguments it was given.
  320. """
  321. def __init__(self):
  322. self.calls = []
  323. def __call__(self, pid, sig):
  324. self.calls.append((pid, sig))
  325. class DummyStandardIO(object):
  326. """
  327. Stub for L{sys} which provides L{BytesIO} streams as stdout and stderr.
  328. """
  329. def __init__(self, stdout, stderr):
  330. self.stdout = stdout
  331. self.stderr = stderr
  332. class DummyWarningsModule(object):
  333. """
  334. Stub for L{warnings} which provides a C{showwarning} method that is a no-op.
  335. """
  336. def showwarning(*args, **kwargs):
  337. """
  338. Do nothing.
  339. @param args: ignored.
  340. @param kwargs: ignored.
  341. """