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