# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ Tests for the inotify-alike implementation L{allmydata.watchdog}. """ # Note: See https://twistedmatrix.com/trac/ticket/8915 for a proposal # to avoid all of this duplicated code from Twisted. from twisted.internet import defer, reactor from twisted.python import filepath, runtime from allmydata.frontends.magic_folder import get_inotify_module from .common import ( AsyncTestCase, skipIf, ) inotify = get_inotify_module() @skipIf(runtime.platformType == "win32", "inotify does not yet work on windows") class INotifyTests(AsyncTestCase): """ Define all the tests for the basic functionality exposed by L{inotify.INotify}. """ def setUp(self): self.dirname = filepath.FilePath(self.mktemp()) self.dirname.createDirectory() self.inotify = inotify.INotify() self.inotify.startReading() self.addCleanup(self.inotify.stopReading) return super(INotifyTests, self).setUp() def _notificationTest(self, mask, operation, expectedPath=None): """ Test notification from some filesystem operation. @param mask: The event mask to use when setting up the watch. @param operation: A function which will be called with the name of a file in the watched directory and which should trigger the event. @param expectedPath: Optionally, the name of the path which is expected to come back in the notification event; this will also be passed to C{operation} (primarily useful when the operation is being done to the directory itself, not a file in it). @return: A L{Deferred} which fires successfully when the expected event has been received or fails otherwise. """ if expectedPath is None: expectedPath = self.dirname.child("foo.bar") notified = defer.Deferred() def cbNotified(result): (watch, filename, events) = result self.assertEqual(filename.asBytesMode(), expectedPath.asBytesMode()) self.assertTrue(events & mask) self.inotify.ignore(self.dirname) notified.addCallback(cbNotified) def notify_event(*args): notified.callback(args) self.inotify.watch( self.dirname, mask=mask, callbacks=[notify_event]) operation(expectedPath) return notified def test_modify(self): """ Writing to a file in a monitored directory sends an C{inotify.IN_MODIFY} event to the callback. """ def operation(path): with path.open("w") as fObj: fObj.write(b'foo') return self._notificationTest(inotify.IN_MODIFY, operation) def test_attrib(self): """ Changing the metadata of a file in a monitored directory sends an C{inotify.IN_ATTRIB} event to the callback. """ def operation(path): # Create the file. path.touch() # Modify the file's attributes. path.touch() return self._notificationTest(inotify.IN_ATTRIB, operation) def test_closeWrite(self): """ Closing a file which was open for writing in a monitored directory sends an C{inotify.IN_CLOSE_WRITE} event to the callback. """ def operation(path): path.open("w").close() return self._notificationTest(inotify.IN_CLOSE_WRITE, operation) def test_delete(self): """ Deleting a file in a monitored directory sends an C{inotify.IN_DELETE} event to the callback. """ expectedPath = self.dirname.child("foo.bar") expectedPath.touch() notified = defer.Deferred() def cbNotified(result): (watch, filename, events) = result self.assertEqual(filename.asBytesMode(), expectedPath.asBytesMode()) self.assertTrue(events & inotify.IN_DELETE) notified.addCallback(cbNotified) self.inotify.watch( self.dirname, mask=inotify.IN_DELETE, callbacks=[lambda *args: notified.callback(args)]) expectedPath.remove() return notified def test_humanReadableMask(self): """ L{inotify.humanReadableMask} translates all the possible event masks to a human readable string. """ for mask, value in inotify._FLAG_TO_HUMAN: self.assertEqual(inotify.humanReadableMask(mask)[0], value) checkMask = ( inotify.IN_CLOSE_WRITE | inotify.IN_ACCESS | inotify.IN_OPEN) self.assertEqual( set(inotify.humanReadableMask(checkMask)), set(['close_write', 'access', 'open'])) def test_noAutoAddSubdirectory(self): """ L{inotify.INotify.watch} with autoAdd==False will stop inotify from watching subdirectories created under the watched one. """ def _callback(wp, fp, mask): # We are notified before we actually process new # directories, so we need to defer this check. def _(): try: self.assertFalse(self.inotify._isWatched(subdir)) d.callback(None) except Exception: d.errback() reactor.callLater(0, _) checkMask = inotify.IN_ISDIR | inotify.IN_CREATE self.inotify.watch( self.dirname, mask=checkMask, autoAdd=False, callbacks=[_callback]) subdir = self.dirname.child('test') d = defer.Deferred() subdir.createDirectory() return d