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