diff --git a/payu/envmod.py b/payu/envmod.py index cfa13e13..2b7c98f3 100644 --- a/payu/envmod.py +++ b/payu/envmod.py @@ -159,24 +159,8 @@ def setup_user_modules(user_modules, user_modulepaths): previous_path = os.environ.get('PATH', '') for modulefile in user_modules: - # Check module exists and there is not multiple available - output = run_module_cmd("avail --terse", modulefile).stderr - - # Extract out the modulefiles available - strip out lines like: - # /apps/Modules/modulefiles: - modules = [line for line in output.strip().splitlines() - if not (line.startswith('/') and line.endswith(':'))] - - # Modules are used for finding model executable paths - so check - # for unique module - if len(modules) > 1: - raise ValueError( - f"There are multiple modules available for {modulefile}:\n" + - f"{output}\n{MULTIPLE_MODULES_HELP}") - elif len(modules) == 0: - raise ValueError( - f"Module is not found: {modulefile}\n{MODULE_NOT_FOUND_HELP}" - ) + # Check modulefile exists and is unique or has an exact match + check_modulefile(modulefile) # Load module module('load', modulefile) @@ -191,6 +175,31 @@ def setup_user_modules(user_modules, user_modulepaths): return (loaded_modules, paths) +def check_modulefile(modulefile: str) -> None: + """Given a modulefile, check if modulefile exists, and there is + a unique modulefile available - e.g. if it's version is specified""" + output = run_module_cmd("avail --terse", modulefile).stderr + + # Extract out the modulefiles available - strip out lines like: + # /apps/Modules/modulefiles: + modules_avail = [line for line in output.strip().splitlines() + if not (line.startswith('/') and line.endswith(':'))] + + # Remove () from end of modulefiles if they exist, e.g. (default) + modules_avail = [mod.rsplit('(', 1)[0] for mod in modules_avail] + + # Modules are used for finding model executable paths - so check + # for unique module, or an exact match for the modulefile name + if len(modules_avail) > 1 and modules_avail.count(modulefile) != 1: + raise ValueError( + f"There are multiple modules available for {modulefile}:\n" + + f"{output}\n{MULTIPLE_MODULES_HELP}") + elif len(modules_avail) == 0: + raise ValueError( + f"Module is not found: {modulefile}\n{MODULE_NOT_FOUND_HELP}" + ) + + def run_module_cmd(subcommand, *args): """Wrapper around subprocess module command that captures output""" modulecmd = f"{os.environ['MODULESHOME']}/bin/modulecmd bash" diff --git a/test/test_envmod.py b/test/test_envmod.py new file mode 100644 index 00000000..b911d6e6 --- /dev/null +++ b/test/test_envmod.py @@ -0,0 +1,105 @@ +import pytest +from unittest.mock import patch + +from payu.envmod import check_modulefile + + +@patch('payu.envmod.run_module_cmd') +def test_check_modulefile_unique(mock_module_cmd): + # Mock module avail command + mock_module_cmd.return_value.stderr = """/path/to/modulefiles: +test-module/1.0.0 +""" + # Test runs without an error + check_modulefile('test-module/1.0.0') + + +@patch('payu.envmod.run_module_cmd') +def test_check_modulefile_without_version(mock_module_cmd): + # Mock module avail command + mock_module_cmd.return_value.stderr = """/path/to/modulefiles: +test-module/1.0.0 +test-module/2.0.0 +test-module/3.0.1 +""" + + # Expect an error raised + with pytest.raises(ValueError) as exc_info: + check_modulefile('test-module') + exc_info.value.startswith( + "There are multiple modules available for test-module" + ) + + # Mock module avail command use debug in name + mock_module_cmd.return_value.stderr = """/path/to/modulefiles: +test-module/1.0.0 +test-module/1.0.0-debug +""" + + # Expect an error raised + with pytest.raises(ValueError) as exc_info: + check_modulefile('test-module') + exc_info.value.startswith( + "There are multiple modules available for test-module" + ) + + +@patch('payu.envmod.run_module_cmd') +def test_check_modulefile_exact_match(mock_module_cmd): + # Mock module avail command + mock_module_cmd.return_value.stderr = """/path/to/modulefiles: +test-module/1.0.0 +test-module/1.0.0-debug +""" + + # Test runs without an error + check_modulefile('test-module/1.0.0') + + +@patch('payu.envmod.run_module_cmd') +def test_check_modulefile_exact_match_with_symbolic_version(mock_module_cmd): + # Mock module avail command + mock_module_cmd.return_value.stderr = """/path/to/modulefiles: +test-module/1.0.0(default) +test-module/1.0.0-debug +""" + + # Test runs without an error + check_modulefile('test-module/1.0.0') + + # Rerun test with another symbolic version/alias other than default + mock_module_cmd.return_value.stderr = """/path/to/modulefiles: +test-module/1.0.0(some_symbolic_name_or_alias) +test-module/1.0.0-debug +""" + + # Test runs without an error + check_modulefile('test-module/1.0.0') + + +@patch('payu.envmod.run_module_cmd') +def test_check_modulefile_multiple_modules(mock_module_cmd): + # Mock module avail command + mock_module_cmd.return_value.stderr = """/path/to/modulefiles: +test-module/1.0.0 +/another/module/path: +test-module/1.0.0 +""" + + # Expect an error raised + with pytest.raises(ValueError) as exc_info: + check_modulefile('test-module/1.0.0') + exc_info.value.startswith( + "There are multiple modules available for test-module" + ) + + +@patch('payu.envmod.run_module_cmd') +def test_check_modulefile_no_modules_found(mock_module_cmd): + # Mock module avail command + mock_module_cmd.return_value.stderr = "" + + # Expect an error raised + with pytest.raises(ValueError) as exc_info: + check_modulefile('test-module/1.0.0') + exc_info.value.startswith("Module is not found: test-module")