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