test_inotify.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for the inotify wrapper in L{twisted.internet.inotify}.
  5. """
  6. import sys
  7. from twisted.internet import defer, reactor
  8. from twisted.python import filepath, runtime
  9. from twisted.python.reflect import requireModule
  10. from twisted.trial import unittest
  11. if requireModule('twisted.python._inotify') is not None:
  12. from twisted.internet import inotify
  13. else:
  14. inotify = None
  15. class INotifyTests(unittest.TestCase):
  16. """
  17. Define all the tests for the basic functionality exposed by
  18. L{inotify.INotify}.
  19. """
  20. if not runtime.platform.supportsINotify():
  21. skip = "This platform doesn't support INotify."
  22. def setUp(self):
  23. self.dirname = filepath.FilePath(self.mktemp())
  24. self.dirname.createDirectory()
  25. self.inotify = inotify.INotify()
  26. self.inotify.startReading()
  27. self.addCleanup(self.inotify.loseConnection)
  28. def test_initializationErrors(self):
  29. """
  30. L{inotify.INotify} emits a C{RuntimeError} when initialized
  31. in an environment that doesn't support inotify as we expect it.
  32. We just try to raise an exception for every possible case in
  33. the for loop in L{inotify.INotify._inotify__init__}.
  34. """
  35. class FakeINotify:
  36. def init(self):
  37. raise inotify.INotifyError()
  38. self.patch(inotify.INotify, '_inotify', FakeINotify())
  39. self.assertRaises(inotify.INotifyError, inotify.INotify)
  40. def _notificationTest(self, mask, operation, expectedPath=None):
  41. """
  42. Test notification from some filesystem operation.
  43. @param mask: The event mask to use when setting up the watch.
  44. @param operation: A function which will be called with the
  45. name of a file in the watched directory and which should
  46. trigger the event.
  47. @param expectedPath: Optionally, the name of the path which is
  48. expected to come back in the notification event; this will
  49. also be passed to C{operation} (primarily useful when the
  50. operation is being done to the directory itself, not a
  51. file in it).
  52. @return: A L{Deferred} which fires successfully when the
  53. expected event has been received or fails otherwise.
  54. """
  55. if expectedPath is None:
  56. expectedPath = self.dirname.child("foo.bar")
  57. notified = defer.Deferred()
  58. def cbNotified(result):
  59. (watch, filename, events) = result
  60. self.assertEqual(filename.asBytesMode(), expectedPath.asBytesMode())
  61. self.assertTrue(events & mask)
  62. notified.addCallback(cbNotified)
  63. self.inotify.watch(
  64. self.dirname, mask=mask,
  65. callbacks=[lambda *args: notified.callback(args)])
  66. operation(expectedPath)
  67. return notified
  68. def test_access(self):
  69. """
  70. Reading from a file in a monitored directory sends an
  71. C{inotify.IN_ACCESS} event to the callback.
  72. """
  73. def operation(path):
  74. path.setContent(b"foo")
  75. path.getContent()
  76. return self._notificationTest(inotify.IN_ACCESS, operation)
  77. def test_modify(self):
  78. """
  79. Writing to a file in a monitored directory sends an
  80. C{inotify.IN_MODIFY} event to the callback.
  81. """
  82. def operation(path):
  83. with path.open("w") as fObj:
  84. fObj.write(b'foo')
  85. return self._notificationTest(inotify.IN_MODIFY, operation)
  86. def test_attrib(self):
  87. """
  88. Changing the metadata of a file in a monitored directory
  89. sends an C{inotify.IN_ATTRIB} event to the callback.
  90. """
  91. def operation(path):
  92. path.touch()
  93. path.touch()
  94. return self._notificationTest(inotify.IN_ATTRIB, operation)
  95. def test_closeWrite(self):
  96. """
  97. Closing a file which was open for writing in a monitored
  98. directory sends an C{inotify.IN_CLOSE_WRITE} event to the
  99. callback.
  100. """
  101. def operation(path):
  102. path.open("w").close()
  103. return self._notificationTest(inotify.IN_CLOSE_WRITE, operation)
  104. def test_closeNoWrite(self):
  105. """
  106. Closing a file which was open for reading but not writing in a
  107. monitored directory sends an C{inotify.IN_CLOSE_NOWRITE} event
  108. to the callback.
  109. """
  110. def operation(path):
  111. path.touch()
  112. path.open("r").close()
  113. return self._notificationTest(inotify.IN_CLOSE_NOWRITE, operation)
  114. def test_open(self):
  115. """
  116. Opening a file in a monitored directory sends an
  117. C{inotify.IN_OPEN} event to the callback.
  118. """
  119. def operation(path):
  120. path.open("w").close()
  121. return self._notificationTest(inotify.IN_OPEN, operation)
  122. def test_movedFrom(self):
  123. """
  124. Moving a file out of a monitored directory sends an
  125. C{inotify.IN_MOVED_FROM} event to the callback.
  126. """
  127. def operation(path):
  128. path.open("w").close()
  129. path.moveTo(filepath.FilePath(self.mktemp()))
  130. return self._notificationTest(inotify.IN_MOVED_FROM, operation)
  131. def test_movedTo(self):
  132. """
  133. Moving a file into a monitored directory sends an
  134. C{inotify.IN_MOVED_TO} event to the callback.
  135. """
  136. def operation(path):
  137. p = filepath.FilePath(self.mktemp())
  138. p.touch()
  139. p.moveTo(path)
  140. return self._notificationTest(inotify.IN_MOVED_TO, operation)
  141. def test_create(self):
  142. """
  143. Creating a file in a monitored directory sends an
  144. C{inotify.IN_CREATE} event to the callback.
  145. """
  146. def operation(path):
  147. path.open("w").close()
  148. return self._notificationTest(inotify.IN_CREATE, operation)
  149. def test_delete(self):
  150. """
  151. Deleting a file in a monitored directory sends an
  152. C{inotify.IN_DELETE} event to the callback.
  153. """
  154. def operation(path):
  155. path.touch()
  156. path.remove()
  157. return self._notificationTest(inotify.IN_DELETE, operation)
  158. def test_deleteSelf(self):
  159. """
  160. Deleting the monitored directory itself sends an
  161. C{inotify.IN_DELETE_SELF} event to the callback.
  162. """
  163. def operation(path):
  164. path.remove()
  165. return self._notificationTest(
  166. inotify.IN_DELETE_SELF, operation, expectedPath=self.dirname)
  167. def test_moveSelf(self):
  168. """
  169. Renaming the monitored directory itself sends an
  170. C{inotify.IN_MOVE_SELF} event to the callback.
  171. """
  172. def operation(path):
  173. path.moveTo(filepath.FilePath(self.mktemp()))
  174. return self._notificationTest(
  175. inotify.IN_MOVE_SELF, operation, expectedPath=self.dirname)
  176. def test_simpleSubdirectoryAutoAdd(self):
  177. """
  178. L{inotify.INotify} when initialized with autoAdd==True adds
  179. also adds the created subdirectories to the watchlist.
  180. """
  181. def _callback(wp, filename, mask):
  182. # We are notified before we actually process new
  183. # directories, so we need to defer this check.
  184. def _():
  185. try:
  186. self.assertTrue(self.inotify._isWatched(subdir))
  187. d.callback(None)
  188. except Exception:
  189. d.errback()
  190. reactor.callLater(0, _)
  191. checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
  192. self.inotify.watch(
  193. self.dirname, mask=checkMask, autoAdd=True,
  194. callbacks=[_callback])
  195. subdir = self.dirname.child('test')
  196. d = defer.Deferred()
  197. subdir.createDirectory()
  198. return d
  199. def test_simpleDeleteDirectory(self):
  200. """
  201. L{inotify.INotify} removes a directory from the watchlist when
  202. it's removed from the filesystem.
  203. """
  204. calls = []
  205. def _callback(wp, filename, mask):
  206. # We are notified before we actually process new
  207. # directories, so we need to defer this check.
  208. def _():
  209. try:
  210. self.assertTrue(self.inotify._isWatched(subdir))
  211. subdir.remove()
  212. except Exception:
  213. d.errback()
  214. def _eb():
  215. # second call, we have just removed the subdir
  216. try:
  217. self.assertFalse(self.inotify._isWatched(subdir))
  218. d.callback(None)
  219. except Exception:
  220. d.errback()
  221. if not calls:
  222. # first call, it's the create subdir
  223. calls.append(filename)
  224. reactor.callLater(0, _)
  225. else:
  226. reactor.callLater(0, _eb)
  227. checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
  228. self.inotify.watch(
  229. self.dirname, mask=checkMask, autoAdd=True,
  230. callbacks=[_callback])
  231. subdir = self.dirname.child('test')
  232. d = defer.Deferred()
  233. subdir.createDirectory()
  234. return d
  235. def test_ignoreDirectory(self):
  236. """
  237. L{inotify.INotify.ignore} removes a directory from the watchlist
  238. """
  239. self.inotify.watch(self.dirname, autoAdd=True)
  240. self.assertTrue(self.inotify._isWatched(self.dirname))
  241. self.inotify.ignore(self.dirname)
  242. self.assertFalse(self.inotify._isWatched(self.dirname))
  243. def test_humanReadableMask(self):
  244. """
  245. L{inotify.humaReadableMask} translates all the possible event
  246. masks to a human readable string.
  247. """
  248. for mask, value in inotify._FLAG_TO_HUMAN:
  249. self.assertEqual(inotify.humanReadableMask(mask)[0], value)
  250. checkMask = (
  251. inotify.IN_CLOSE_WRITE | inotify.IN_ACCESS | inotify.IN_OPEN)
  252. self.assertEqual(
  253. set(inotify.humanReadableMask(checkMask)),
  254. set(['close_write', 'access', 'open']))
  255. def test_recursiveWatch(self):
  256. """
  257. L{inotify.INotify.watch} with recursive==True will add all the
  258. subdirectories under the given path to the watchlist.
  259. """
  260. subdir = self.dirname.child('test')
  261. subdir2 = subdir.child('test2')
  262. subdir3 = subdir2.child('test3')
  263. subdir3.makedirs()
  264. dirs = [subdir, subdir2, subdir3]
  265. self.inotify.watch(self.dirname, recursive=True)
  266. # let's even call this twice so that we test that nothing breaks
  267. self.inotify.watch(self.dirname, recursive=True)
  268. for d in dirs:
  269. self.assertTrue(self.inotify._isWatched(d))
  270. def test_connectionLostError(self):
  271. """
  272. L{inotify.INotify.connectionLost} if there's a problem while closing
  273. the fd shouldn't raise the exception but should log the error
  274. """
  275. import os
  276. in_ = inotify.INotify()
  277. os.close(in_._fd)
  278. in_.loseConnection()
  279. self.flushLoggedErrors()
  280. def test_noAutoAddSubdirectory(self):
  281. """
  282. L{inotify.INotify.watch} with autoAdd==False will stop inotify
  283. from watching subdirectories created under the watched one.
  284. """
  285. def _callback(wp, fp, mask):
  286. # We are notified before we actually process new
  287. # directories, so we need to defer this check.
  288. def _():
  289. try:
  290. self.assertFalse(self.inotify._isWatched(subdir))
  291. d.callback(None)
  292. except Exception:
  293. d.errback()
  294. reactor.callLater(0, _)
  295. checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
  296. self.inotify.watch(
  297. self.dirname, mask=checkMask, autoAdd=False,
  298. callbacks=[_callback])
  299. subdir = self.dirname.child('test')
  300. d = defer.Deferred()
  301. subdir.createDirectory()
  302. return d
  303. def test_seriesOfWatchAndIgnore(self):
  304. """
  305. L{inotify.INotify} will watch a filepath for events even if the same
  306. path is repeatedly added/removed/re-added to the watchpoints.
  307. """
  308. expectedPath = self.dirname.child("foo.bar2")
  309. expectedPath.touch()
  310. notified = defer.Deferred()
  311. def cbNotified(result):
  312. (ignored, filename, events) = result
  313. self.assertEqual(filename.asBytesMode(), expectedPath.asBytesMode())
  314. self.assertTrue(events & inotify.IN_DELETE_SELF)
  315. def callIt(*args):
  316. notified.callback(args)
  317. # Watch, ignore, watch again to get into the state being tested.
  318. self.assertTrue(self.inotify.watch(expectedPath, callbacks=[callIt]))
  319. self.inotify.ignore(expectedPath)
  320. self.assertTrue(
  321. self.inotify.watch(
  322. expectedPath, mask=inotify.IN_DELETE_SELF, callbacks=[callIt]))
  323. notified.addCallback(cbNotified)
  324. # Apparently in kernel version < 2.6.25, inofify has a bug in the way
  325. # similar events are coalesced. So, be sure to generate a different
  326. # event here than the touch() at the top of this method might have
  327. # generated.
  328. expectedPath.remove()
  329. return notified
  330. def test_ignoreFilePath(self):
  331. """
  332. L{inotify.INotify} will ignore a filepath after it has been removed from
  333. the watch list.
  334. """
  335. expectedPath = self.dirname.child("foo.bar2")
  336. expectedPath.touch()
  337. expectedPath2 = self.dirname.child("foo.bar3")
  338. expectedPath2.touch()
  339. notified = defer.Deferred()
  340. def cbNotified(result):
  341. (ignored, filename, events) = result
  342. self.assertEqual(filename.asBytesMode(), expectedPath2.asBytesMode())
  343. self.assertTrue(events & inotify.IN_DELETE_SELF)
  344. def callIt(*args):
  345. notified.callback(args)
  346. self.assertTrue(
  347. self.inotify.watch(
  348. expectedPath, inotify.IN_DELETE_SELF, callbacks=[callIt]))
  349. notified.addCallback(cbNotified)
  350. self.assertTrue(
  351. self.inotify.watch(
  352. expectedPath2, inotify.IN_DELETE_SELF, callbacks=[callIt]))
  353. self.inotify.ignore(expectedPath)
  354. expectedPath.remove()
  355. expectedPath2.remove()
  356. return notified
  357. def test_ignoreNonWatchedFile(self):
  358. """
  359. L{inotify.INotify} will raise KeyError if a non-watched filepath is
  360. ignored.
  361. """
  362. expectedPath = self.dirname.child("foo.ignored")
  363. expectedPath.touch()
  364. self.assertRaises(KeyError, self.inotify.ignore, expectedPath)
  365. def test_complexSubdirectoryAutoAdd(self):
  366. """
  367. L{inotify.INotify} with autoAdd==True for a watched path
  368. generates events for every file or directory already present
  369. in a newly created subdirectory under the watched one.
  370. This tests that we solve a race condition in inotify even though
  371. we may generate duplicate events.
  372. """
  373. calls = set()
  374. def _callback(wp, filename, mask):
  375. calls.add(filename)
  376. if len(calls) == 6:
  377. try:
  378. self.assertTrue(self.inotify._isWatched(subdir))
  379. self.assertTrue(self.inotify._isWatched(subdir2))
  380. self.assertTrue(self.inotify._isWatched(subdir3))
  381. created = someFiles + [subdir, subdir2, subdir3]
  382. created = {f.asBytesMode() for f in created}
  383. self.assertEqual(len(calls), len(created))
  384. self.assertEqual(calls, created)
  385. except Exception:
  386. d.errback()
  387. else:
  388. d.callback(None)
  389. checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
  390. self.inotify.watch(
  391. self.dirname, mask=checkMask, autoAdd=True,
  392. callbacks=[_callback])
  393. subdir = self.dirname.child('test')
  394. subdir2 = subdir.child('test2')
  395. subdir3 = subdir2.child('test3')
  396. d = defer.Deferred()
  397. subdir3.makedirs()
  398. someFiles = [subdir.child('file1.dat'),
  399. subdir2.child('file2.dat'),
  400. subdir3.child('file3.dat')]
  401. # Add some files in pretty much all the directories so that we
  402. # see that we process all of them.
  403. for i, filename in enumerate(someFiles):
  404. filename.setContent(
  405. filename.path.encode(sys.getfilesystemencoding()))
  406. return d