test_pidfile.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for L{twisted.application.runner._pidfile}.
  5. """
  6. from functools import wraps
  7. import errno
  8. from os import getpid, name as SYSTEM_NAME
  9. from io import BytesIO
  10. from zope.interface import implementer
  11. from zope.interface.verify import verifyObject
  12. from twisted.python.filepath import IFilePath
  13. from twisted.python.runtime import platform
  14. from ...runner import _pidfile
  15. from .._pidfile import (
  16. IPIDFile, PIDFile, NonePIDFile,
  17. AlreadyRunningError, InvalidPIDFileError, StalePIDFileError,
  18. NoPIDFound,
  19. )
  20. import twisted.trial.unittest
  21. from twisted.trial.unittest import SkipTest
  22. def ifPlatformSupported(f):
  23. """
  24. Decorator for tests that are not expected to work on all platforms.
  25. Calling L{PIDFile.isRunning} currently raises L{NotImplementedError} on
  26. non-POSIX platforms.
  27. On an unsupported platform, we expect to see any test that calls
  28. L{PIDFile.isRunning} to raise either L{NotImplementedError}, L{SkipTest},
  29. or C{self.failureException}.
  30. (C{self.failureException} may occur in a test that checks for a specific
  31. exception but it gets NotImplementedError instead.)
  32. @param f: The test method to decorate.
  33. @type f: method
  34. @return: The wrapped callable.
  35. """
  36. @wraps(f)
  37. def wrapper(self, *args, **kwargs):
  38. supported = platform.getType() == "posix"
  39. if supported:
  40. return f(self, *args, **kwargs)
  41. else:
  42. e = self.assertRaises(
  43. (NotImplementedError, SkipTest, self.failureException),
  44. f, self, *args, **kwargs
  45. )
  46. if isinstance(e, NotImplementedError):
  47. self.assertTrue(
  48. str(e).startswith("isRunning is not implemented on ")
  49. )
  50. return wrapper
  51. class PIDFileTests(twisted.trial.unittest.TestCase):
  52. """
  53. Tests for L{PIDFile}.
  54. """
  55. def test_interface(self):
  56. """
  57. L{PIDFile} conforms to L{IPIDFile}.
  58. """
  59. pidFile = PIDFile(DummyFilePath())
  60. verifyObject(IPIDFile, pidFile)
  61. def test_formatWithPID(self):
  62. """
  63. L{PIDFile._format} returns the expected format when given a PID.
  64. """
  65. self.assertEqual(PIDFile._format(pid=1337), b"1337\n")
  66. def test_readWithPID(self):
  67. """
  68. L{PIDFile.read} returns the PID from the given file path.
  69. """
  70. pid = 1337
  71. pidFile = PIDFile(DummyFilePath(PIDFile._format(pid=pid)))
  72. self.assertEqual(pid, pidFile.read())
  73. def test_readEmptyPID(self):
  74. """
  75. L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
  76. path.
  77. """
  78. pidValue = b""
  79. pidFile = PIDFile(DummyFilePath(b""))
  80. e = self.assertRaises(InvalidPIDFileError, pidFile.read)
  81. self.assertEqual(
  82. str(e),
  83. "non-integer PID value in PID file: {!r}".format(pidValue)
  84. )
  85. def test_readWithBogusPID(self):
  86. """
  87. L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
  88. path.
  89. """
  90. pidValue = b"$foo!"
  91. pidFile = PIDFile(DummyFilePath(pidValue))
  92. e = self.assertRaises(InvalidPIDFileError, pidFile.read)
  93. self.assertEqual(
  94. str(e),
  95. "non-integer PID value in PID file: {!r}".format(pidValue)
  96. )
  97. def test_readDoesntExist(self):
  98. """
  99. L{PIDFile.read} raises L{NoPIDFound} when given a non-existing file
  100. path.
  101. """
  102. pidFile = PIDFile(DummyFilePath())
  103. e = self.assertRaises(NoPIDFound, pidFile.read)
  104. self.assertEqual(str(e), "PID file does not exist")
  105. def test_readOpenRaisesOSErrorNotENOENT(self):
  106. """
  107. L{PIDFile.read} re-raises L{OSError} if the associated C{errno} is
  108. anything other than L{errno.ENOENT}.
  109. """
  110. def oops(mode="r"):
  111. raise OSError(errno.EIO, "I/O error")
  112. self.patch(DummyFilePath, "open", oops)
  113. pidFile = PIDFile(DummyFilePath())
  114. error = self.assertRaises(OSError, pidFile.read)
  115. self.assertEqual(error.errno, errno.EIO)
  116. def test_writePID(self):
  117. """
  118. L{PIDFile._write} stores the given PID.
  119. """
  120. pid = 1995
  121. pidFile = PIDFile(DummyFilePath())
  122. pidFile._write(pid)
  123. self.assertEqual(pidFile.read(), pid)
  124. def test_writePIDInvalid(self):
  125. """
  126. L{PIDFile._write} raises L{ValueError} when given an invalid PID.
  127. """
  128. pidFile = PIDFile(DummyFilePath())
  129. self.assertRaises(ValueError, pidFile._write, u"burp")
  130. def test_writeRunningPID(self):
  131. """
  132. L{PIDFile.writeRunningPID} stores the PID for the current process.
  133. """
  134. pidFile = PIDFile(DummyFilePath())
  135. pidFile.writeRunningPID()
  136. self.assertEqual(pidFile.read(), getpid())
  137. def test_remove(self):
  138. """
  139. L{PIDFile.remove} removes the PID file.
  140. """
  141. pidFile = PIDFile(DummyFilePath(b""))
  142. self.assertTrue(pidFile.filePath.exists())
  143. pidFile.remove()
  144. self.assertFalse(pidFile.filePath.exists())
  145. @ifPlatformSupported
  146. def test_isRunningDoesExist(self):
  147. """
  148. L{PIDFile.isRunning} returns true for a process that does exist.
  149. """
  150. pidFile = PIDFile(DummyFilePath())
  151. pidFile._write(1337)
  152. def kill(pid, signal):
  153. return # Don't actually kill anything
  154. self.patch(_pidfile, "kill", kill)
  155. self.assertTrue(pidFile.isRunning())
  156. @ifPlatformSupported
  157. def test_isRunningThis(self):
  158. """
  159. L{PIDFile.isRunning} returns true for this process (which is running).
  160. @note: This differs from L{PIDFileTests.test_isRunningDoesExist} in
  161. that it actually invokes the C{kill} system call, which is useful for
  162. testing of our chosen method for probing the existence of a process.
  163. """
  164. pidFile = PIDFile(DummyFilePath())
  165. pidFile.writeRunningPID()
  166. self.assertTrue(pidFile.isRunning())
  167. @ifPlatformSupported
  168. def test_isRunningDoesNotExist(self):
  169. """
  170. L{PIDFile.isRunning} raises L{StalePIDFileError} for a process that
  171. does not exist (errno=ESRCH).
  172. """
  173. pidFile = PIDFile(DummyFilePath())
  174. pidFile._write(1337)
  175. def kill(pid, signal):
  176. raise OSError(errno.ESRCH, "No such process")
  177. self.patch(_pidfile, "kill", kill)
  178. self.assertRaises(StalePIDFileError, pidFile.isRunning)
  179. @ifPlatformSupported
  180. def test_isRunningNotAllowed(self):
  181. """
  182. L{PIDFile.isRunning} returns true for a process that we are not allowed
  183. to kill (errno=EPERM).
  184. """
  185. pidFile = PIDFile(DummyFilePath())
  186. pidFile._write(1337)
  187. def kill(pid, signal):
  188. raise OSError(errno.EPERM, "Operation not permitted")
  189. self.patch(_pidfile, "kill", kill)
  190. self.assertTrue(pidFile.isRunning())
  191. @ifPlatformSupported
  192. def test_isRunningInit(self):
  193. """
  194. L{PIDFile.isRunning} returns true for a process that we are not allowed
  195. to kill (errno=EPERM).
  196. @note: This differs from L{PIDFileTests.test_isRunningNotAllowed} in
  197. that it actually invokes the C{kill} system call, which is useful for
  198. testing of our chosen method for probing the existence of a process
  199. that we are not allowed to kill.
  200. @note: In this case, we try killing C{init}, which is process #1 on
  201. POSIX systems, so this test is not portable. C{init} should always be
  202. running and should not be killable by non-root users.
  203. """
  204. if SYSTEM_NAME != "posix":
  205. raise SkipTest("This test assumes POSIX")
  206. pidFile = PIDFile(DummyFilePath())
  207. pidFile._write(1) # PID 1 is init on POSIX systems
  208. self.assertTrue(pidFile.isRunning())
  209. @ifPlatformSupported
  210. def test_isRunningUnknownErrno(self):
  211. """
  212. L{PIDFile.isRunning} re-raises L{OSError} if the attached C{errno}
  213. value from L{os.kill} is not an expected one.
  214. """
  215. pidFile = PIDFile(DummyFilePath())
  216. pidFile.writeRunningPID()
  217. def kill(pid, signal):
  218. raise OSError(errno.EEXIST, "File exists")
  219. self.patch(_pidfile, "kill", kill)
  220. self.assertRaises(OSError, pidFile.isRunning)
  221. def test_isRunningNoPIDFile(self):
  222. """
  223. L{PIDFile.isRunning} returns false if the PID file doesn't exist.
  224. """
  225. pidFile = PIDFile(DummyFilePath())
  226. self.assertFalse(pidFile.isRunning())
  227. def test_contextManager(self):
  228. """
  229. When used as a context manager, a L{PIDFile} will store the current pid
  230. on entry, then removes the PID file on exit.
  231. """
  232. pidFile = PIDFile(DummyFilePath())
  233. self.assertFalse(pidFile.filePath.exists())
  234. with pidFile:
  235. self.assertTrue(pidFile.filePath.exists())
  236. self.assertEqual(pidFile.read(), getpid())
  237. self.assertFalse(pidFile.filePath.exists())
  238. @ifPlatformSupported
  239. def test_contextManagerDoesntExist(self):
  240. """
  241. When used as a context manager, a L{PIDFile} will replace the
  242. underlying PIDFile rather than raising L{AlreadyRunningError} if the
  243. contained PID file exists but refers to a non-running PID.
  244. """
  245. pidFile = PIDFile(DummyFilePath())
  246. pidFile._write(1337)
  247. def kill(pid, signal):
  248. raise OSError(errno.ESRCH, "No such process")
  249. self.patch(_pidfile, "kill", kill)
  250. e = self.assertRaises(StalePIDFileError, pidFile.isRunning)
  251. self.assertEqual(str(e), "PID file refers to non-existing process")
  252. with pidFile:
  253. self.assertEqual(pidFile.read(), getpid())
  254. @ifPlatformSupported
  255. def test_contextManagerAlreadyRunning(self):
  256. """
  257. When used as a context manager, a L{PIDFile} will raise
  258. L{AlreadyRunningError} if the there is already a running process with
  259. the contained PID.
  260. """
  261. pidFile = PIDFile(DummyFilePath())
  262. pidFile._write(1337)
  263. def kill(pid, signal):
  264. return # Don't actually kill anything
  265. self.patch(_pidfile, "kill", kill)
  266. self.assertTrue(pidFile.isRunning())
  267. self.assertRaises(AlreadyRunningError, pidFile.__enter__)
  268. class NonePIDFileTests(twisted.trial.unittest.TestCase):
  269. """
  270. Tests for L{NonePIDFile}.
  271. """
  272. def test_interface(self):
  273. """
  274. L{NonePIDFile} conforms to L{IPIDFile}.
  275. """
  276. pidFile = NonePIDFile()
  277. verifyObject(IPIDFile, pidFile)
  278. def test_read(self):
  279. """
  280. L{NonePIDFile.read} raises L{NoPIDFound}.
  281. """
  282. pidFile = NonePIDFile()
  283. e = self.assertRaises(NoPIDFound, pidFile.read)
  284. self.assertEqual(str(e), "PID file does not exist")
  285. def test_write(self):
  286. """
  287. L{NonePIDFile._write} raises L{OSError} with an errno of L{errno.EPERM}.
  288. """
  289. pidFile = NonePIDFile()
  290. error = self.assertRaises(OSError, pidFile._write, 0)
  291. self.assertEqual(error.errno, errno.EPERM)
  292. def test_writeRunningPID(self):
  293. """
  294. L{NonePIDFile.writeRunningPID} raises L{OSError} with an errno of
  295. L{errno.EPERM}.
  296. """
  297. pidFile = NonePIDFile()
  298. error = self.assertRaises(OSError, pidFile.writeRunningPID)
  299. self.assertEqual(error.errno, errno.EPERM)
  300. def test_remove(self):
  301. """
  302. L{NonePIDFile.remove} raises L{OSError} with an errno of L{errno.EPERM}.
  303. """
  304. pidFile = NonePIDFile()
  305. error = self.assertRaises(OSError, pidFile.remove)
  306. self.assertEqual(error.errno, errno.ENOENT)
  307. def test_isRunning(self):
  308. """
  309. L{NonePIDFile.isRunning} returns L{False}.
  310. """
  311. pidFile = NonePIDFile()
  312. self.assertEqual(pidFile.isRunning(), False)
  313. def test_contextManager(self):
  314. """
  315. When used as a context manager, a L{NonePIDFile} doesn't raise, despite
  316. not existing.
  317. """
  318. pidFile = NonePIDFile()
  319. with pidFile:
  320. pass
  321. @implementer(IFilePath)
  322. class DummyFilePath(object):
  323. """
  324. In-memory L{IFilePath}.
  325. """
  326. def __init__(self, content=None):
  327. self.setContent(content)
  328. def open(self, mode="r"):
  329. if not self._exists:
  330. raise OSError(errno.ENOENT, "No such file or directory")
  331. return BytesIO(self.getContent())
  332. def setContent(self, content):
  333. self._exists = content is not None
  334. self._content = content
  335. def getContent(self):
  336. return self._content
  337. def remove(self):
  338. self.setContent(None)
  339. def exists(self):
  340. return self._exists