1212import subprocess
1313import sys
1414import tempfile
15+ import threading
1516import time
1617from unittest import SkipTest , mock
1718
@@ -418,9 +419,11 @@ def test_it_should_dashify(self):
418419 self .assertEqual ("this-is-my-argument" , dashify ("this_is_my_argument" ))
419420 self .assertEqual ("foo" , dashify ("foo" ))
420421
421- def test_lock_file (self ):
422+ @ddt .data ("my-lock-file" , "my-lock-file-\u0394 " , "\u0394 /my-lock-file" , "\U0001f680 /my-lock-file" )
423+ def test_lock_file (self , filename ):
422424 with tempfile .TemporaryDirectory () as tdir :
423- my_file = os .path .join (tdir , "my-lock-file" )
425+ my_file = os .path .join (tdir , filename )
426+ os .makedirs (os .path .dirname (my_file ), exist_ok = True )
424427 lock_file = LockFile (my_file )
425428 assert not lock_file ._has_lock ()
426429 # Release lock we don't have - fine.
@@ -429,6 +432,7 @@ def test_lock_file(self):
429432 # Get lock.
430433 lock_file ._obtain_lock_or_raise ()
431434 assert lock_file ._has_lock ()
435+ assert os .path .isfile (my_file + ".lock" )
432436
433437 # Concurrent access.
434438 other_lock_file = LockFile (my_file )
@@ -437,6 +441,7 @@ def test_lock_file(self):
437441
438442 lock_file ._release_lock ()
439443 assert not lock_file ._has_lock ()
444+ assert not os .path .exists (my_file + ".lock" )
440445
441446 other_lock_file ._obtain_lock_or_raise ()
442447 self .assertRaises (IOError , lock_file ._obtain_lock_or_raise )
@@ -446,6 +451,67 @@ def test_lock_file(self):
446451 lock_file ._obtain_lock_or_raise ()
447452 lock_file ._release_lock ()
448453
454+ def test_lock_file_rejects_embedded_nul (self ):
455+ with tempfile .TemporaryDirectory () as tdir :
456+ my_file = os .path .join (tdir , "my-lock-file" )
457+ lock_file = LockFile (my_file + "\0 suffix" )
458+ self .assertRaises (ValueError , lock_file ._obtain_lock_or_raise )
459+ assert not lock_file ._has_lock ()
460+ assert not os .path .exists (my_file )
461+
462+ @ddt .data (False , True )
463+ @requires_symlinks
464+ def test_lock_file_does_not_follow_a_symlink (self , target_exists ):
465+ with tempfile .TemporaryDirectory () as tdir :
466+ my_file = os .path .join (tdir , "my-lock-file" )
467+ outside = os .path .join (tdir , "outside-the-lock" )
468+ content = b"Do not modify the symlink target."
469+ if target_exists :
470+ with open (outside , "wb" ) as stream :
471+ stream .write (content )
472+ os .symlink (outside , my_file + ".lock" )
473+
474+ lock_file = LockFile (my_file )
475+ self .assertRaises (IOError , lock_file ._obtain_lock_or_raise )
476+ assert not lock_file ._has_lock ()
477+ lock_file ._release_lock ()
478+ assert os .path .islink (my_file + ".lock" )
479+ if target_exists :
480+ with open (outside , "rb" ) as stream :
481+ self .assertEqual (stream .read (), content )
482+ else :
483+ assert not os .path .exists (outside )
484+
485+ def test_lock_file_is_obtained_by_a_single_holder (self ):
486+ with tempfile .TemporaryDirectory () as tdir :
487+ my_file = os .path .join (tdir , "my-lock-file" )
488+ racers = 8
489+ at_the_line = threading .Barrier (racers )
490+ holders = []
491+ guard = threading .Lock ()
492+
493+ def obtain ():
494+ lock_file = LockFile (my_file )
495+ at_the_line .wait ()
496+ try :
497+ lock_file ._obtain_lock_or_raise ()
498+ except OSError :
499+ return
500+ with guard :
501+ holders .append (lock_file )
502+
503+ threads = [threading .Thread (target = obtain ) for _ in range (racers )]
504+ for thread in threads :
505+ thread .start ()
506+ for thread in threads :
507+ thread .join ()
508+
509+ try :
510+ self .assertEqual (1 , len (holders ))
511+ finally :
512+ for lock_file in holders :
513+ lock_file ._release_lock ()
514+
449515 def test_blocking_lock_file (self ):
450516 with tempfile .TemporaryDirectory () as tdir :
451517 my_file = os .path .join (tdir , "my-lock-file" )
0 commit comments