1# Copyright (c) 2005 Divmod, Inc.
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Tests for L{twisted.python.lockfile}.
7"""
8
9import os, errno
10
11from twisted.trial import unittest
12from twisted.python import lockfile
13from twisted.python.runtime import platform
14
15skipKill = None
16if platform.isWindows():
17    try:
18        from win32api import OpenProcess
19        import pywintypes
20    except ImportError:
21        skipKill = ("On windows, lockfile.kill is not implemented in the "
22                    "absence of win32api and/or pywintypes.")
23
24class UtilTests(unittest.TestCase):
25    """
26    Tests for the helper functions used to implement L{FilesystemLock}.
27    """
28    def test_symlinkEEXIST(self):
29        """
30        L{lockfile.symlink} raises L{OSError} with C{errno} set to L{EEXIST}
31        when an attempt is made to create a symlink which already exists.
32        """
33        name = self.mktemp()
34        lockfile.symlink('foo', name)
35        exc = self.assertRaises(OSError, lockfile.symlink, 'foo', name)
36        self.assertEqual(exc.errno, errno.EEXIST)
37
38
39    def test_symlinkEIOWindows(self):
40        """
41        L{lockfile.symlink} raises L{OSError} with C{errno} set to L{EIO} when
42        the underlying L{rename} call fails with L{EIO}.
43
44        Renaming a file on Windows may fail if the target of the rename is in
45        the process of being deleted (directory deletion appears not to be
46        atomic).
47        """
48        name = self.mktemp()
49        def fakeRename(src, dst):
50            raise IOError(errno.EIO, None)
51        self.patch(lockfile, 'rename', fakeRename)
52        exc = self.assertRaises(IOError, lockfile.symlink, name, "foo")
53        self.assertEqual(exc.errno, errno.EIO)
54    if not platform.isWindows():
55        test_symlinkEIOWindows.skip = (
56            "special rename EIO handling only necessary and correct on "
57            "Windows.")
58
59
60    def test_readlinkENOENT(self):
61        """
62        L{lockfile.readlink} raises L{OSError} with C{errno} set to L{ENOENT}
63        when an attempt is made to read a symlink which does not exist.
64        """
65        name = self.mktemp()
66        exc = self.assertRaises(OSError, lockfile.readlink, name)
67        self.assertEqual(exc.errno, errno.ENOENT)
68
69
70    def test_readlinkEACCESWindows(self):
71        """
72        L{lockfile.readlink} raises L{OSError} with C{errno} set to L{EACCES}
73        on Windows when the underlying file open attempt fails with C{EACCES}.
74
75        Opening a file on Windows may fail if the path is inside a directory
76        which is in the process of being deleted (directory deletion appears
77        not to be atomic).
78        """
79        name = self.mktemp()
80        def fakeOpen(path, mode):
81            raise IOError(errno.EACCES, None)
82        self.patch(lockfile, '_open', fakeOpen)
83        exc = self.assertRaises(IOError, lockfile.readlink, name)
84        self.assertEqual(exc.errno, errno.EACCES)
85    if not platform.isWindows():
86        test_readlinkEACCESWindows.skip = (
87            "special readlink EACCES handling only necessary and correct on "
88            "Windows.")
89
90
91    def test_kill(self):
92        """
93        L{lockfile.kill} returns without error if passed the PID of a
94        process which exists and signal C{0}.
95        """
96        lockfile.kill(os.getpid(), 0)
97    test_kill.skip = skipKill
98
99
100    def test_killESRCH(self):
101        """
102        L{lockfile.kill} raises L{OSError} with errno of L{ESRCH} if
103        passed a PID which does not correspond to any process.
104        """
105        # Hopefully there is no process with PID 2 ** 31 - 1
106        exc = self.assertRaises(OSError, lockfile.kill, 2 ** 31 - 1, 0)
107        self.assertEqual(exc.errno, errno.ESRCH)
108    test_killESRCH.skip = skipKill
109
110
111    def test_noKillCall(self):
112        """
113        Verify that when L{lockfile.kill} does end up as None (e.g. on Windows
114        without pywin32), it doesn't end up being called and raising a
115        L{TypeError}.
116        """
117        self.patch(lockfile, "kill", None)
118        fl = lockfile.FilesystemLock(self.mktemp())
119        fl.lock()
120        self.assertFalse(fl.lock())
121
122
123
124class LockingTestCase(unittest.TestCase):
125    def _symlinkErrorTest(self, errno):
126        def fakeSymlink(source, dest):
127            raise OSError(errno, None)
128        self.patch(lockfile, 'symlink', fakeSymlink)
129
130        lockf = self.mktemp()
131        lock = lockfile.FilesystemLock(lockf)
132        exc = self.assertRaises(OSError, lock.lock)
133        self.assertEqual(exc.errno, errno)
134
135
136    def test_symlinkError(self):
137        """
138        An exception raised by C{symlink} other than C{EEXIST} is passed up to
139        the caller of L{FilesystemLock.lock}.
140        """
141        self._symlinkErrorTest(errno.ENOSYS)
142
143
144    def test_symlinkErrorPOSIX(self):
145        """
146        An L{OSError} raised by C{symlink} on a POSIX platform with an errno of
147        C{EACCES} or C{EIO} is passed to the caller of L{FilesystemLock.lock}.
148
149        On POSIX, unlike on Windows, these are unexpected errors which cannot
150        be handled by L{FilesystemLock}.
151        """
152        self._symlinkErrorTest(errno.EACCES)
153        self._symlinkErrorTest(errno.EIO)
154    if platform.isWindows():
155        test_symlinkErrorPOSIX.skip = (
156            "POSIX-specific error propagation not expected on Windows.")
157
158
159    def test_cleanlyAcquire(self):
160        """
161        If the lock has never been held, it can be acquired and the C{clean}
162        and C{locked} attributes are set to C{True}.
163        """
164        lockf = self.mktemp()
165        lock = lockfile.FilesystemLock(lockf)
166        self.assertTrue(lock.lock())
167        self.assertTrue(lock.clean)
168        self.assertTrue(lock.locked)
169
170
171    def test_cleanlyRelease(self):
172        """
173        If a lock is released cleanly, it can be re-acquired and the C{clean}
174        and C{locked} attributes are set to C{True}.
175        """
176        lockf = self.mktemp()
177        lock = lockfile.FilesystemLock(lockf)
178        self.assertTrue(lock.lock())
179        lock.unlock()
180        self.assertFalse(lock.locked)
181
182        lock = lockfile.FilesystemLock(lockf)
183        self.assertTrue(lock.lock())
184        self.assertTrue(lock.clean)
185        self.assertTrue(lock.locked)
186
187
188    def test_cannotLockLocked(self):
189        """
190        If a lock is currently locked, it cannot be locked again.
191        """
192        lockf = self.mktemp()
193        firstLock = lockfile.FilesystemLock(lockf)
194        self.assertTrue(firstLock.lock())
195
196        secondLock = lockfile.FilesystemLock(lockf)
197        self.assertFalse(secondLock.lock())
198        self.assertFalse(secondLock.locked)
199
200
201    def test_uncleanlyAcquire(self):
202        """
203        If a lock was held by a process which no longer exists, it can be
204        acquired, the C{clean} attribute is set to C{False}, and the
205        C{locked} attribute is set to C{True}.
206        """
207        owner = 12345
208
209        def fakeKill(pid, signal):
210            if signal != 0:
211                raise OSError(errno.EPERM, None)
212            if pid == owner:
213                raise OSError(errno.ESRCH, None)
214
215        lockf = self.mktemp()
216        self.patch(lockfile, 'kill', fakeKill)
217        lockfile.symlink(str(owner), lockf)
218
219        lock = lockfile.FilesystemLock(lockf)
220        self.assertTrue(lock.lock())
221        self.assertFalse(lock.clean)
222        self.assertTrue(lock.locked)
223
224        self.assertEqual(lockfile.readlink(lockf), str(os.getpid()))
225
226
227    def test_lockReleasedBeforeCheck(self):
228        """
229        If the lock is initially held but then released before it can be
230        examined to determine if the process which held it still exists, it is
231        acquired and the C{clean} and C{locked} attributes are set to C{True}.
232        """
233        def fakeReadlink(name):
234            # Pretend to be another process releasing the lock.
235            lockfile.rmlink(lockf)
236            # Fall back to the real implementation of readlink.
237            readlinkPatch.restore()
238            return lockfile.readlink(name)
239        readlinkPatch = self.patch(lockfile, 'readlink', fakeReadlink)
240
241        def fakeKill(pid, signal):
242            if signal != 0:
243                raise OSError(errno.EPERM, None)
244            if pid == 43125:
245                raise OSError(errno.ESRCH, None)
246        self.patch(lockfile, 'kill', fakeKill)
247
248        lockf = self.mktemp()
249        lock = lockfile.FilesystemLock(lockf)
250        lockfile.symlink(str(43125), lockf)
251        self.assertTrue(lock.lock())
252        self.assertTrue(lock.clean)
253        self.assertTrue(lock.locked)
254
255
256    def test_lockReleasedDuringAcquireSymlink(self):
257        """
258        If the lock is released while an attempt is made to acquire
259        it, the lock attempt fails and C{FilesystemLock.lock} returns
260        C{False}.  This can happen on Windows when L{lockfile.symlink}
261        fails with L{IOError} of C{EIO} because another process is in
262        the middle of a call to L{os.rmdir} (implemented in terms of
263        RemoveDirectory) which is not atomic.
264        """
265        def fakeSymlink(src, dst):
266            # While another process id doing os.rmdir which the Windows
267            # implementation of rmlink does, a rename call will fail with EIO.
268            raise OSError(errno.EIO, None)
269
270        self.patch(lockfile, 'symlink', fakeSymlink)
271
272        lockf = self.mktemp()
273        lock = lockfile.FilesystemLock(lockf)
274        self.assertFalse(lock.lock())
275        self.assertFalse(lock.locked)
276    if not platform.isWindows():
277        test_lockReleasedDuringAcquireSymlink.skip = (
278            "special rename EIO handling only necessary and correct on "
279            "Windows.")
280
281
282    def test_lockReleasedDuringAcquireReadlink(self):
283        """
284        If the lock is initially held but is released while an attempt
285        is made to acquire it, the lock attempt fails and
286        L{FilesystemLock.lock} returns C{False}.
287        """
288        def fakeReadlink(name):
289            # While another process is doing os.rmdir which the
290            # Windows implementation of rmlink does, a readlink call
291            # will fail with EACCES.
292            raise IOError(errno.EACCES, None)
293        readlinkPatch = self.patch(lockfile, 'readlink', fakeReadlink)
294
295        lockf = self.mktemp()
296        lock = lockfile.FilesystemLock(lockf)
297        lockfile.symlink(str(43125), lockf)
298        self.assertFalse(lock.lock())
299        self.assertFalse(lock.locked)
300    if not platform.isWindows():
301        test_lockReleasedDuringAcquireReadlink.skip = (
302            "special readlink EACCES handling only necessary and correct on "
303            "Windows.")
304
305
306    def _readlinkErrorTest(self, exceptionType, errno):
307        def fakeReadlink(name):
308            raise exceptionType(errno, None)
309        self.patch(lockfile, 'readlink', fakeReadlink)
310
311        lockf = self.mktemp()
312
313        # Make it appear locked so it has to use readlink
314        lockfile.symlink(str(43125), lockf)
315
316        lock = lockfile.FilesystemLock(lockf)
317        exc = self.assertRaises(exceptionType, lock.lock)
318        self.assertEqual(exc.errno, errno)
319        self.assertFalse(lock.locked)
320
321
322    def test_readlinkError(self):
323        """
324        An exception raised by C{readlink} other than C{ENOENT} is passed up to
325        the caller of L{FilesystemLock.lock}.
326        """
327        self._readlinkErrorTest(OSError, errno.ENOSYS)
328        self._readlinkErrorTest(IOError, errno.ENOSYS)
329
330
331    def test_readlinkErrorPOSIX(self):
332        """
333        Any L{IOError} raised by C{readlink} on a POSIX platform passed to the
334        caller of L{FilesystemLock.lock}.
335
336        On POSIX, unlike on Windows, these are unexpected errors which cannot
337        be handled by L{FilesystemLock}.
338        """
339        self._readlinkErrorTest(IOError, errno.ENOSYS)
340        self._readlinkErrorTest(IOError, errno.EACCES)
341    if platform.isWindows():
342        test_readlinkErrorPOSIX.skip = (
343            "POSIX-specific error propagation not expected on Windows.")
344
345
346    def test_lockCleanedUpConcurrently(self):
347        """
348        If a second process cleans up the lock after a first one checks the
349        lock and finds that no process is holding it, the first process does
350        not fail when it tries to clean up the lock.
351        """
352        def fakeRmlink(name):
353            rmlinkPatch.restore()
354            # Pretend to be another process cleaning up the lock.
355            lockfile.rmlink(lockf)
356            # Fall back to the real implementation of rmlink.
357            return lockfile.rmlink(name)
358        rmlinkPatch = self.patch(lockfile, 'rmlink', fakeRmlink)
359
360        def fakeKill(pid, signal):
361            if signal != 0:
362                raise OSError(errno.EPERM, None)
363            if pid == 43125:
364                raise OSError(errno.ESRCH, None)
365        self.patch(lockfile, 'kill', fakeKill)
366
367        lockf = self.mktemp()
368        lock = lockfile.FilesystemLock(lockf)
369        lockfile.symlink(str(43125), lockf)
370        self.assertTrue(lock.lock())
371        self.assertTrue(lock.clean)
372        self.assertTrue(lock.locked)
373
374
375    def test_rmlinkError(self):
376        """
377        An exception raised by L{rmlink} other than C{ENOENT} is passed up
378        to the caller of L{FilesystemLock.lock}.
379        """
380        def fakeRmlink(name):
381            raise OSError(errno.ENOSYS, None)
382        self.patch(lockfile, 'rmlink', fakeRmlink)
383
384        def fakeKill(pid, signal):
385            if signal != 0:
386                raise OSError(errno.EPERM, None)
387            if pid == 43125:
388                raise OSError(errno.ESRCH, None)
389        self.patch(lockfile, 'kill', fakeKill)
390
391        lockf = self.mktemp()
392
393        # Make it appear locked so it has to use readlink
394        lockfile.symlink(str(43125), lockf)
395
396        lock = lockfile.FilesystemLock(lockf)
397        exc = self.assertRaises(OSError, lock.lock)
398        self.assertEqual(exc.errno, errno.ENOSYS)
399        self.assertFalse(lock.locked)
400
401
402    def test_killError(self):
403        """
404        If L{kill} raises an exception other than L{OSError} with errno set to
405        C{ESRCH}, the exception is passed up to the caller of
406        L{FilesystemLock.lock}.
407        """
408        def fakeKill(pid, signal):
409            raise OSError(errno.EPERM, None)
410        self.patch(lockfile, 'kill', fakeKill)
411
412        lockf = self.mktemp()
413
414        # Make it appear locked so it has to use readlink
415        lockfile.symlink(str(43125), lockf)
416
417        lock = lockfile.FilesystemLock(lockf)
418        exc = self.assertRaises(OSError, lock.lock)
419        self.assertEqual(exc.errno, errno.EPERM)
420        self.assertFalse(lock.locked)
421
422
423    def test_unlockOther(self):
424        """
425        L{FilesystemLock.unlock} raises L{ValueError} if called for a lock
426        which is held by a different process.
427        """
428        lockf = self.mktemp()
429        lockfile.symlink(str(os.getpid() + 1), lockf)
430        lock = lockfile.FilesystemLock(lockf)
431        self.assertRaises(ValueError, lock.unlock)
432
433
434    def test_isLocked(self):
435        """
436        L{isLocked} returns C{True} if the named lock is currently locked,
437        C{False} otherwise.
438        """
439        lockf = self.mktemp()
440        self.assertFalse(lockfile.isLocked(lockf))
441        lock = lockfile.FilesystemLock(lockf)
442        self.assertTrue(lock.lock())
443        self.assertTrue(lockfile.isLocked(lockf))
444        lock.unlock()
445        self.assertFalse(lockfile.isLocked(lockf))
446