1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
5 Tests for the inotify wrapper in L{twisted.internet.inotify}.
8 from twisted.internet import defer, reactor
9 from twisted.python import filepath, runtime
10 from twisted.trial import unittest
13 from twisted.python import _inotify
17 from twisted.internet import inotify
21 class TestINotify(unittest.TestCase):
23 Define all the tests for the basic functionality exposed by
26 if not runtime.platform.supportsINotify():
27 skip = "This platform doesn't support INotify."
30 self.dirname = filepath.FilePath(self.mktemp())
31 self.dirname.createDirectory()
32 self.inotify = inotify.INotify()
33 self.inotify.startReading()
34 self.addCleanup(self.inotify.loseConnection)
37 def test_initializationErrors(self):
39 L{inotify.INotify} emits a C{RuntimeError} when initialized
40 in an environment that doesn't support inotify as we expect it.
42 We just try to raise an exception for every possible case in
43 the for loop in L{inotify.INotify._inotify__init__}.
47 raise inotify.INotifyError()
48 self.patch(inotify.INotify, '_inotify', FakeINotify())
49 self.assertRaises(inotify.INotifyError, inotify.INotify)
52 def _notificationTest(self, mask, operation, expectedPath=None):
54 Test notification from some filesystem operation.
56 @param mask: The event mask to use when setting up the watch.
58 @param operation: A function which will be called with the
59 name of a file in the watched directory and which should
62 @param expectedPath: Optionally, the name of the path which is
63 expected to come back in the notification event; this will
64 also be passed to C{operation} (primarily useful when the
65 operation is being done to the directory itself, not a
68 @return: A L{Deferred} which fires successfully when the
69 expected event has been received or fails otherwise.
71 if expectedPath is None:
72 expectedPath = self.dirname.child("foo.bar")
73 notified = defer.Deferred()
74 def cbNotified((watch, filename, events)):
75 self.assertEqual(filename, expectedPath)
76 self.assertTrue(events & mask)
77 notified.addCallback(cbNotified)
80 self.dirname, mask=mask,
81 callbacks=[lambda *args: notified.callback(args)])
82 operation(expectedPath)
86 def test_access(self):
88 Reading from a file in a monitored directory sends an
89 C{inotify.IN_ACCESS} event to the callback.
92 path.setContent("foo")
95 return self._notificationTest(inotify.IN_ACCESS, operation)
98 def test_modify(self):
100 Writing to a file in a monitored directory sends an
101 C{inotify.IN_MODIFY} event to the callback.
104 fObj = path.open("w")
108 return self._notificationTest(inotify.IN_MODIFY, operation)
111 def test_attrib(self):
113 Changing the metadata of a a file in a monitored directory
114 sends an C{inotify.IN_ATTRIB} event to the callback.
120 return self._notificationTest(inotify.IN_ATTRIB, operation)
123 def test_closeWrite(self):
125 Closing a file which was open for writing in a monitored
126 directory sends an C{inotify.IN_CLOSE_WRITE} event to the
130 fObj = path.open("w")
133 return self._notificationTest(inotify.IN_CLOSE_WRITE, operation)
136 def test_closeNoWrite(self):
138 Closing a file which was open for reading but not writing in a
139 monitored directory sends an C{inotify.IN_CLOSE_NOWRITE} event
144 fObj = path.open("r")
147 return self._notificationTest(inotify.IN_CLOSE_NOWRITE, operation)
152 Opening a file in a monitored directory sends an
153 C{inotify.IN_OPEN} event to the callback.
156 fObj = path.open("w")
159 return self._notificationTest(inotify.IN_OPEN, operation)
162 def test_movedFrom(self):
164 Moving a file out of a monitored directory sends an
165 C{inotify.IN_MOVED_FROM} event to the callback.
168 fObj = path.open("w")
170 path.moveTo(filepath.FilePath(self.mktemp()))
172 return self._notificationTest(inotify.IN_MOVED_FROM, operation)
175 def test_movedTo(self):
177 Moving a file into a monitored directory sends an
178 C{inotify.IN_MOVED_TO} event to the callback.
181 p = filepath.FilePath(self.mktemp())
185 return self._notificationTest(inotify.IN_MOVED_TO, operation)
188 def test_create(self):
190 Creating a file in a monitored directory sends an
191 C{inotify.IN_CREATE} event to the callback.
194 fObj = path.open("w")
197 return self._notificationTest(inotify.IN_CREATE, operation)
200 def test_delete(self):
202 Deleting a file in a monitored directory sends an
203 C{inotify.IN_DELETE} event to the callback.
209 return self._notificationTest(inotify.IN_DELETE, operation)
212 def test_deleteSelf(self):
214 Deleting the monitored directory itself sends an
215 C{inotify.IN_DELETE_SELF} event to the callback.
220 return self._notificationTest(
221 inotify.IN_DELETE_SELF, operation, expectedPath=self.dirname)
224 def test_moveSelf(self):
226 Renaming the monitored directory itself sends an
227 C{inotify.IN_MOVE_SELF} event to the callback.
230 path.moveTo(filepath.FilePath(self.mktemp()))
232 return self._notificationTest(
233 inotify.IN_MOVE_SELF, operation, expectedPath=self.dirname)
236 def test_simpleSubdirectoryAutoAdd(self):
238 L{inotify.INotify} when initialized with autoAdd==True adds
239 also adds the created subdirectories to the watchlist.
241 def _callback(wp, filename, mask):
242 # We are notified before we actually process new
243 # directories, so we need to defer this check.
246 self.assertTrue(self.inotify._isWatched(subdir))
250 reactor.callLater(0, _)
252 checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
254 self.dirname, mask=checkMask, autoAdd=True,
255 callbacks=[_callback])
256 subdir = self.dirname.child('test')
258 subdir.createDirectory()
262 def test_simpleDeleteDirectory(self):
264 L{inotify.INotify} removes a directory from the watchlist when
265 it's removed from the filesystem.
268 def _callback(wp, filename, mask):
269 # We are notified before we actually process new
270 # directories, so we need to defer this check.
273 self.assertTrue(self.inotify._isWatched(subdir))
278 # second call, we have just removed the subdir
280 self.assertTrue(not self.inotify._isWatched(subdir))
286 # first call, it's the create subdir
287 calls.append(filename)
288 reactor.callLater(0, _)
291 reactor.callLater(0, _eb)
293 checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
295 self.dirname, mask=checkMask, autoAdd=True,
296 callbacks=[_callback])
297 subdir = self.dirname.child('test')
299 subdir.createDirectory()
303 def test_ignoreDirectory(self):
305 L{inotify.INotify.ignore} removes a directory from the watchlist
307 self.inotify.watch(self.dirname, autoAdd=True)
308 self.assertTrue(self.inotify._isWatched(self.dirname))
309 self.inotify.ignore(self.dirname)
310 self.assertFalse(self.inotify._isWatched(self.dirname))
313 def test_humanReadableMask(self):
315 L{inotify.humaReadableMask} translates all the possible event
316 masks to a human readable string.
318 for mask, value in inotify._FLAG_TO_HUMAN:
319 self.assertEqual(inotify.humanReadableMask(mask)[0], value)
322 inotify.IN_CLOSE_WRITE | inotify.IN_ACCESS | inotify.IN_OPEN)
324 set(inotify.humanReadableMask(checkMask)),
325 set(['close_write', 'access', 'open']))
328 def test_recursiveWatch(self):
330 L{inotify.INotify.watch} with recursive==True will add all the
331 subdirectories under the given path to the watchlist.
333 subdir = self.dirname.child('test')
334 subdir2 = subdir.child('test2')
335 subdir3 = subdir2.child('test3')
337 dirs = [subdir, subdir2, subdir3]
338 self.inotify.watch(self.dirname, recursive=True)
339 # let's even call this twice so that we test that nothing breaks
340 self.inotify.watch(self.dirname, recursive=True)
342 self.assertTrue(self.inotify._isWatched(d))
345 def test_connectionLostError(self):
347 L{inotify.INotify.connectionLost} if there's a problem while closing
348 the fd shouldn't raise the exception but should log the error
351 in_ = inotify.INotify()
354 self.flushLoggedErrors()
356 def test_noAutoAddSubdirectory(self):
358 L{inotify.INotify.watch} with autoAdd==False will stop inotify
359 from watching subdirectories created under the watched one.
361 def _callback(wp, fp, mask):
362 # We are notified before we actually process new
363 # directories, so we need to defer this check.
366 self.assertFalse(self.inotify._isWatched(subdir.path))
370 reactor.callLater(0, _)
372 checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
374 self.dirname, mask=checkMask, autoAdd=False,
375 callbacks=[_callback])
376 subdir = self.dirname.child('test')
378 subdir.createDirectory()
382 def test_seriesOfWatchAndIgnore(self):
384 L{inotify.INotify} will watch a filepath for events even if the same
385 path is repeatedly added/removed/re-added to the watchpoints.
387 expectedPath = self.dirname.child("foo.bar2")
390 notified = defer.Deferred()
391 def cbNotified((ignored, filename, events)):
392 self.assertEqual(filename, expectedPath)
393 self.assertTrue(events & inotify.IN_DELETE_SELF)
396 notified.callback(args)
398 # Watch, ignore, watch again to get into the state being tested.
399 self.assertTrue(self.inotify.watch(expectedPath, callbacks=[callIt]))
400 self.inotify.ignore(expectedPath)
403 expectedPath, mask=inotify.IN_DELETE_SELF, callbacks=[callIt]))
405 notified.addCallback(cbNotified)
407 # Apparently in kernel version < 2.6.25, inofify has a bug in the way
408 # similar events are coalesced. So, be sure to generate a different
409 # event here than the touch() at the top of this method might have
411 expectedPath.remove()
416 def test_ignoreFilePath(self):
418 L{inotify.INotify} will ignore a filepath after it has been removed from
421 expectedPath = self.dirname.child("foo.bar2")
423 expectedPath2 = self.dirname.child("foo.bar3")
424 expectedPath2.touch()
426 notified = defer.Deferred()
427 def cbNotified((ignored, filename, events)):
428 self.assertEqual(filename, expectedPath2)
429 self.assertTrue(events & inotify.IN_DELETE_SELF)
432 notified.callback(args)
436 expectedPath, inotify.IN_DELETE_SELF, callbacks=[callIt]))
437 notified.addCallback(cbNotified)
441 expectedPath2, inotify.IN_DELETE_SELF, callbacks=[callIt]))
443 self.inotify.ignore(expectedPath)
445 expectedPath.remove()
446 expectedPath2.remove()
451 def test_ignoreNonWatchedFile(self):
453 L{inotify.INotify} will raise KeyError if a non-watched filepath is
456 expectedPath = self.dirname.child("foo.ignored")
459 self.assertRaises(KeyError, self.inotify.ignore, expectedPath)
462 def test_complexSubdirectoryAutoAdd(self):
464 L{inotify.INotify} with autoAdd==True for a watched path
465 generates events for every file or directory already present
466 in a newly created subdirectory under the watched one.
468 This tests that we solve a race condition in inotify even though
469 we may generate duplicate events.
472 def _callback(wp, filename, mask):
476 self.assertTrue(self.inotify._isWatched(subdir))
477 self.assertTrue(self.inotify._isWatched(subdir2))
478 self.assertTrue(self.inotify._isWatched(subdir3))
479 created = someFiles + [subdir, subdir2, subdir3]
480 self.assertEqual(len(calls), len(created))
481 self.assertEqual(calls, set(created))
487 checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
489 self.dirname, mask=checkMask, autoAdd=True,
490 callbacks=[_callback])
491 subdir = self.dirname.child('test')
492 subdir2 = subdir.child('test2')
493 subdir3 = subdir2.child('test3')
497 someFiles = [subdir.child('file1.dat'),
498 subdir2.child('file2.dat'),
499 subdir3.child('file3.dat')]
500 # Add some files in pretty much all the directories so that we
501 # see that we process all of them.
502 for i, filename in enumerate(someFiles):
503 filename.setContent(filename.path)