3434import os
3535import re
3636import shutil
37+ import signal
3738import stat
3839import sys
3940import tempfile
@@ -1415,7 +1416,7 @@ def test_module_only(self):
14151416 self .assertTrue (os .path .exists (os .path .join (self .test_installpath , 'software' , 'toy' , '0.0-deps' , 'bin' )))
14161417 modtxt = read_file (toy_mod )
14171418 self .assertTrue (re .search ("set root %s" % prefix , modtxt ))
1418- self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' ))), 1 )
1419+ self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' ))), 2 )
14191420 self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' , 'toy' ))), 1 )
14201421
14211422 # install (only) additional module under a hierarchical MNS
@@ -1430,7 +1431,7 @@ def test_module_only(self):
14301431 # existing install is reused
14311432 modtxt2 = read_file (toy_core_mod )
14321433 self .assertTrue (re .search ("set root %s" % prefix , modtxt2 ))
1433- self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' ))), 2 )
1434+ self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' ))), 3 )
14341435 self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' , 'toy' ))), 1 )
14351436
14361437 # make sure load statements for dependencies are included
@@ -1441,7 +1442,7 @@ def test_module_only(self):
14411442 os .remove (toy_core_mod )
14421443
14431444 # test installing (only) additional module in Lua syntax (if Lmod is available)
1444- lmod_abspath = which ('lmod' )
1445+ lmod_abspath = os . environ . get ( 'LMOD_CMD' ) or which ('lmod' )
14451446 if lmod_abspath is not None :
14461447 args = common_args [:- 1 ] + [
14471448 '--allow-modules-tool-mismatch' ,
@@ -1455,7 +1456,7 @@ def test_module_only(self):
14551456 # existing install is reused
14561457 modtxt3 = read_file (toy_mod + '.lua' )
14571458 self .assertTrue (re .search ('local root = "%s"' % prefix , modtxt3 ))
1458- self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' ))), 2 )
1459+ self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' ))), 3 )
14591460 self .assertEqual (len (os .listdir (os .path .join (self .test_installpath , 'software' , 'toy' ))), 1 )
14601461
14611462 # make sure load statements for dependencies are included
@@ -2057,7 +2058,7 @@ def test_toy_modaltsoftname(self):
20572058 self .assertTrue (os .path .exists (os .path .join (modules_path , 'yot' , yot_name )))
20582059
20592060 # only subdirectories for software should be created
2060- self .assertEqual (os .listdir (software_path ), ['toy' ] )
2061+ self .assertEqual (sorted ( os .listdir (software_path )), sorted ( ['toy' , '.locks' ]) )
20612062 self .assertEqual (sorted (os .listdir (os .path .join (software_path , 'toy' ))), ['0.0-one' , '0.0-two' ])
20622063
20632064 # only subdirectories for modules with alternative names should be created
@@ -2516,6 +2517,95 @@ def test_toy_ghost_installdir(self):
25162517
25172518 self .assertFalse (os .path .exists (toy_installdir ))
25182519
2520+ def test_toy_build_lock (self ):
2521+ """Test toy installation when a lock is already in place."""
2522+
2523+ locks_dir = os .path .join (self .test_installpath , 'software' , '.locks' )
2524+ toy_installdir = os .path .join (self .test_installpath , 'software' , 'toy' , '0.0' )
2525+ toy_lock_fn = toy_installdir .replace (os .path .sep , '_' ) + '.lock'
2526+
2527+ toy_lock_path = os .path .join (locks_dir , toy_lock_fn )
2528+ mkdir (toy_lock_path , parents = True )
2529+
2530+ error_pattern = "Lock .*_software_toy_0.0.lock already exists, aborting!"
2531+ self .assertErrorRegex (EasyBuildError , error_pattern , self .test_toy_build , raise_error = True , verbose = False )
2532+
2533+ locks_dir = os .path .join (self .test_prefix , 'locks' )
2534+
2535+ # no lock in place, so installation proceeds as normal
2536+ extra_args = ['--locks-dir=%s' % locks_dir ]
2537+ self .test_toy_build (extra_args = extra_args , verify = True , raise_error = True )
2538+
2539+ # put lock in place in custom locks dir, try again
2540+ toy_lock_path = os .path .join (locks_dir , toy_lock_fn )
2541+ mkdir (toy_lock_path , parents = True )
2542+ self .assertErrorRegex (EasyBuildError , error_pattern , self .test_toy_build ,
2543+ extra_args = extra_args , raise_error = True , verbose = False )
2544+
2545+ # also test use of --ignore-locks
2546+ self .test_toy_build (extra_args = extra_args + ['--ignore-locks' ], verify = True , raise_error = True )
2547+
2548+ # define a context manager that remove a lock after a while, so we can check the use of --wait-for-lock
2549+ class remove_lock_after :
2550+ def __init__ (self , seconds , lock_fp ):
2551+ self .seconds = seconds
2552+ self .lock_fp = lock_fp
2553+
2554+ def remove_lock (self , * args ):
2555+ remove_dir (self .lock_fp )
2556+
2557+ def __enter__ (self ):
2558+ signal .signal (signal .SIGALRM , self .remove_lock )
2559+ signal .alarm (self .seconds )
2560+
2561+ def __exit__ (self , type , value , traceback ):
2562+ pass
2563+
2564+ # wait for lock to be removed, with 1 second interval of checking
2565+ extra_args .append ('--wait-on-lock=1' )
2566+
2567+ wait_regex = re .compile ("^== lock .*_software_toy_0.0.lock exists, waiting 1 seconds" , re .M )
2568+ ok_regex = re .compile ("^== COMPLETED: Installation ended successfully" , re .M )
2569+
2570+ self .assertTrue (os .path .exists (toy_lock_path ))
2571+
2572+ # use context manager to remove lock after 3 seconds
2573+ with remove_lock_after (3 , toy_lock_path ):
2574+ self .mock_stderr (True )
2575+ self .mock_stdout (True )
2576+ self .test_toy_build (extra_args = extra_args , verify = False , raise_error = True , testing = False )
2577+ stderr , stdout = self .get_stderr (), self .get_stdout ()
2578+ self .mock_stderr (False )
2579+ self .mock_stdout (False )
2580+
2581+ self .assertEqual (stderr , '' )
2582+
2583+ wait_matches = wait_regex .findall (stdout )
2584+ # we can't rely on an exact number of 'waiting' messages, so let's go with a range...
2585+ self .assertTrue (len (wait_matches ) in range (2 , 5 ))
2586+
2587+ self .assertTrue (ok_regex .search (stdout ), "Pattern '%s' found in: %s" % (ok_regex .pattern , stdout ))
2588+
2589+ # when there is no lock in place, --wait-on-lock has no impact
2590+ self .assertFalse (os .path .exists (toy_lock_path ))
2591+ self .mock_stderr (True )
2592+ self .mock_stdout (True )
2593+ self .test_toy_build (extra_args = extra_args , verify = False , raise_error = True , testing = False )
2594+ stderr , stdout = self .get_stderr (), self .get_stdout ()
2595+ self .mock_stderr (False )
2596+ self .mock_stdout (False )
2597+
2598+ self .assertEqual (stderr , '' )
2599+ self .assertTrue (ok_regex .search (stdout ), "Pattern '%s' found in: %s" % (ok_regex .pattern , stdout ))
2600+ self .assertFalse (wait_regex .search (stdout ), "Pattern '%s' not found in: %s" % (wait_regex .pattern , stdout ))
2601+
2602+ # check for clean error on creation of lock
2603+ extra_args = ['--locks-dir=/' ]
2604+ error_pattern = r"Failed to create lock /.*_software_toy_0.0.lock:.* "
2605+ error_pattern += r"(Read-only file system|Permission denied)"
2606+ self .assertErrorRegex (EasyBuildError , error_pattern , self .test_toy_build ,
2607+ extra_args = extra_args , raise_error = True , verbose = False )
2608+
25192609
25202610def suite ():
25212611 """ return all the tests in this file """
0 commit comments