# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other # Spack Project Developers. See the top-level COPYRIGHT file for details. # # SPDX-License-Identifier: (Apache-2.0 OR MIT) import os import pytest import spack.environment as ev import spack.main import spack.modules.lmod import spack.spec mpich_spec_string = "mpich@3.0.4" mpileaks_spec_string = "mpileaks" libdwarf_spec_string = "libdwarf arch=x64-linux" install = spack.main.SpackCommand("install") #: Class of the writer tested in this module writer_cls = spack.modules.lmod.LmodModulefileWriter pytestmark = [ pytest.mark.not_on_windows("does not run on windows"), pytest.mark.usefixtures("mock_modules_root"), ] @pytest.fixture(params=["clang@=12.0.0", "gcc@=10.2.1"]) def compiler(request): return request.param @pytest.fixture( params=[ ("mpich@3.0.4", ("mpi",)), ("mpich@3.0.1", []), ("openblas@0.2.15", ("blas",)), ("openblas-with-lapack@0.2.15", ("blas", "lapack")), ("mpileaks@2.3", ("mpi",)), ("mpileaks@2.1", []), ] ) def provider(request): return request.param @pytest.mark.usefixtures("config", "mock_packages") class TestLmod: @pytest.mark.regression("37788") @pytest.mark.parametrize("modules_config", ["core_compilers", "core_compilers_at_equal"]) def test_layout_for_specs_compiled_with_core_compilers( self, modules_config, module_configuration, factory ): """Tests that specs compiled with core compilers are in the 'Core' folder. Also tests that we can use both ``compiler@version`` and ``compiler@=version`` to specify a core compiler. """ module_configuration(modules_config) module, spec = factory("libelf%clang@12.0.0") assert "Core" in module.layout.available_path_parts def test_file_layout(self, compiler, provider, factory, module_configuration): """Tests the layout of files in the hierarchy is the one expected.""" module_configuration("complex_hierarchy") spec_string, services = provider module, spec = factory(spec_string + "%" + compiler) layout = module.layout # Check that the services provided are in the hierarchy for s in services: assert s in layout.conf.hierarchy_tokens # Check that the compiler part of the path has no hash and that it # is transformed to r"Core" if the compiler is listed among core # compilers # Check that specs listed as core_specs are transformed to "Core" if compiler == "clang@=12.0.0" or spec_string == "mpich@3.0.1": assert "Core" in layout.available_path_parts else: assert compiler.replace("@=", "/") in layout.available_path_parts # Check that the provider part instead has always an hash even if # hash has been disallowed in the configuration file path_parts = layout.available_path_parts service_part = spec_string.replace("@", "/") service_part = "-".join([service_part, layout.spec.dag_hash(length=7)]) if "mpileaks" in spec_string: # It's a user, not a provider, so create the provider string service_part = layout.spec["mpi"].format("{name}/{version}-{hash:7}") else: # Only relevant for providers, not users, of virtuals assert service_part in path_parts # Check that multi-providers have repetitions in path parts repetitions = len([x for x in path_parts if service_part == x]) if spec_string == "openblas-with-lapack@0.2.15": assert repetitions == 2 elif spec_string == "mpileaks@2.1": assert repetitions == 0 else: assert repetitions == 1 def test_compilers_provided_different_name(self, factory, module_configuration): module_configuration("complex_hierarchy") module, spec = factory("intel-oneapi-compilers%clang@3.3") provides = module.conf.provides assert "compiler" in provides assert provides["compiler"] == spack.spec.CompilerSpec("oneapi@=3.0") def test_simple_case(self, modulefile_content, module_configuration): """Tests the generation of a simple Lua module file.""" module_configuration("autoload_direct") content = modulefile_content(mpich_spec_string) assert "-- -*- lua -*-" in content assert "whatis([[Name : mpich]])" in content assert "whatis([[Version : 3.0.4]])" in content assert 'family("mpi")' in content def test_autoload_direct(self, modulefile_content, module_configuration): """Tests the automatic loading of direct dependencies.""" module_configuration("autoload_direct") content = modulefile_content(mpileaks_spec_string) assert len([x for x in content if "depends_on(" in x]) == 2 def test_autoload_all(self, modulefile_content, module_configuration): """Tests the automatic loading of all dependencies.""" module_configuration("autoload_all") content = modulefile_content(mpileaks_spec_string) assert len([x for x in content if "depends_on(" in x]) == 5 def test_alter_environment(self, modulefile_content, module_configuration): """Tests modifications to run-time environment.""" module_configuration("alter_environment") content = modulefile_content("mpileaks platform=test target=x86_64") assert len([x for x in content if x.startswith('prepend_path("CMAKE_PREFIX_PATH"')]) == 0 assert len([x for x in content if 'setenv("FOO", "foo")' in x]) == 1 assert len([x for x in content if 'unsetenv("BAR")' in x]) == 1 content = modulefile_content("libdwarf platform=test target=core2") assert len([x for x in content if x.startswith('prepend_path("CMAKE_PREFIX_PATH"')]) == 0 assert len([x for x in content if 'setenv("FOO", "foo")' in x]) == 0 assert len([x for x in content if 'unsetenv("BAR")' in x]) == 0 def test_prepend_path_separator(self, modulefile_content, module_configuration): """Tests that we can use custom delimiters to manipulate path lists.""" module_configuration("module_path_separator") content = modulefile_content("module-path-separator") assert len([x for x in content if 'append_path("COLON", "foo", ":")' in x]) == 1 assert len([x for x in content if 'prepend_path("COLON", "foo", ":")' in x]) == 1 assert len([x for x in content if 'remove_path("COLON", "foo", ":")' in x]) == 1 assert len([x for x in content if 'append_path("SEMICOLON", "bar", ";")' in x]) == 1 assert len([x for x in content if 'prepend_path("SEMICOLON", "bar", ";")' in x]) == 1 assert len([x for x in content if 'remove_path("SEMICOLON", "bar", ";")' in x]) == 1 assert len([x for x in content if 'append_path("SPACE", "qux", " ")' in x]) == 1 assert len([x for x in content if 'remove_path("SPACE", "qux", " ")' in x]) == 1 @pytest.mark.regression("11355") def test_manpath_setup(self, modulefile_content, module_configuration): """Tests specific setup of MANPATH environment variable.""" module_configuration("autoload_direct") # no manpath set by module content = modulefile_content("mpileaks") assert len([x for x in content if 'append_path("MANPATH", "", ":")' in x]) == 0 # manpath set by module with prepend_path content = modulefile_content("module-manpath-prepend") assert ( len([x for x in content if 'prepend_path("MANPATH", "/path/to/man", ":")' in x]) == 1 ) assert ( len([x for x in content if 'prepend_path("MANPATH", "/path/to/share/man", ":")' in x]) == 1 ) assert len([x for x in content if 'append_path("MANPATH", "", ":")' in x]) == 1 # manpath set by module with append_path content = modulefile_content("module-manpath-append") assert len([x for x in content if 'append_path("MANPATH", "/path/to/man", ":")' in x]) == 1 assert len([x for x in content if 'append_path("MANPATH", "", ":")' in x]) == 1 # manpath set by module with setenv content = modulefile_content("module-manpath-setenv") assert len([x for x in content if 'setenv("MANPATH", "/path/to/man")' in x]) == 1 assert len([x for x in content if 'append_path("MANPATH", "", ":")' in x]) == 0 @pytest.mark.regression("29578") def test_setenv_raw_value(self, modulefile_content, module_configuration): """Tests that we can set environment variable value without formatting it.""" module_configuration("autoload_direct") content = modulefile_content("module-setenv-raw") assert len([x for x in content if 'setenv("FOO", "{{name}}, {name}, {{}}, {}")' in x]) == 1 def test_help_message(self, modulefile_content, module_configuration): """Tests the generation of module help message.""" module_configuration("autoload_direct") content = modulefile_content("mpileaks target=core2") help_msg = ( "help([[Name : mpileaks]])" "help([[Version: 2.3]])" "help([[Target : core2]])" "help()" "help([[Mpileaks is a mock package that passes audits]])" ) assert help_msg in "".join(content) content = modulefile_content("libdwarf target=core2") help_msg = ( "help([[Name : libdwarf]])" "help([[Version: 20130729]])" "help([[Target : core2]])" "depends_on(" ) assert help_msg in "".join(content) content = modulefile_content("module-long-help target=core2") help_msg = ( "help([[Name : module-long-help]])" "help([[Version: 1.0]])" "help([[Target : core2]])" "help()" "help([[Package to test long description message generated in modulefile." "Message too long is wrapped over multiple lines.]])" ) assert help_msg in "".join(content) def test_exclude(self, modulefile_content, module_configuration): """Tests excluding the generation of selected modules.""" module_configuration("exclude") content = modulefile_content(mpileaks_spec_string) assert len([x for x in content if "depends_on(" in x]) == 1 def test_no_hash(self, factory, module_configuration): """Makes sure that virtual providers (in the hierarchy) always include a hash. Make sure that the module file for the spec does not include a hash if hash_length is 0. """ module_configuration("no_hash") module, spec = factory(mpileaks_spec_string) path = module.layout.filename mpi_spec = spec["mpi"] mpi_element = "{0}/{1}-{2}/".format( mpi_spec.name, mpi_spec.version, mpi_spec.dag_hash(length=7) ) assert mpi_element in path mpileaks_spec = spec mpileaks_element = "{0}/{1}.lua".format(mpileaks_spec.name, mpileaks_spec.version) assert path.endswith(mpileaks_element) def test_no_core_compilers(self, factory, module_configuration): """Ensures that missing 'core_compilers' in the configuration file raises the right exception. """ # In this case we miss the entry completely module_configuration("missing_core_compilers") module, spec = factory(mpileaks_spec_string) with pytest.raises(spack.modules.lmod.CoreCompilersNotFoundError): module.write() # Here we have an empty list module_configuration("core_compilers_empty") module, spec = factory(mpileaks_spec_string) with pytest.raises(spack.modules.lmod.CoreCompilersNotFoundError): module.write() def test_non_virtual_in_hierarchy(self, factory, module_configuration): """Ensures that if a non-virtual is in hierarchy, an exception will be raised. """ module_configuration("non_virtual_in_hierarchy") module, spec = factory(mpileaks_spec_string) with pytest.raises(spack.modules.lmod.NonVirtualInHierarchyError): module.write() def test_conflicts(self, modulefile_content, module_configuration): """Tests adding conflicts to the module.""" # This configuration has no error, so check the conflicts directives # are there module_configuration("conflicts") content = modulefile_content("mpileaks") assert len([x for x in content if x.startswith("conflict")]) == 2 assert len([x for x in content if x == 'conflict("mpileaks")']) == 1 assert len([x for x in content if x == 'conflict("intel/14.0.1")']) == 1 def test_inconsistent_conflict_in_modules_yaml(self, modulefile_content, module_configuration): """Tests inconsistent conflict definition in `modules.yaml`.""" # This configuration is inconsistent, check an error is raised module_configuration("wrong_conflicts") with pytest.raises(spack.modules.common.ModulesError): modulefile_content("mpileaks") def test_override_template_in_package(self, modulefile_content, module_configuration): """Tests overriding a template from and attribute in the package.""" module_configuration("autoload_direct") content = modulefile_content("override-module-templates") assert "Override successful!" in content def test_override_template_in_modules_yaml(self, modulefile_content, module_configuration): """Tests overriding a template from `modules.yaml`""" module_configuration("override_template") content = modulefile_content("override-module-templates") assert "Override even better!" in content content = modulefile_content("mpileaks target=x86_64") assert "Override even better!" in content @pytest.mark.usefixtures("config") def test_external_configure_args(self, factory): # If this package is detected as an external, its configure option line # in the module file starts with 'unknown' writer, spec = factory("externaltool") assert "unknown" in writer.context.configure_options def test_guess_core_compilers(self, factory, module_configuration, monkeypatch): """Check that we can guess core compilers.""" # In this case we miss the entry completely module_configuration("missing_core_compilers") # Our mock paths must be detected as system paths monkeypatch.setattr(spack.util.environment, "SYSTEM_DIRS", ["/path/to"]) # We don't want to really write into user configuration # when running tests def no_op_set(*args, **kwargs): pass monkeypatch.setattr(spack.config, "set", no_op_set) # Assert we have core compilers now writer, _ = factory(mpileaks_spec_string) assert writer.conf.core_compilers @pytest.mark.parametrize( "spec_str", ["mpileaks target=nocona", "mpileaks target=core2", "mpileaks target=x86_64"] ) @pytest.mark.regression("13005") def test_only_generic_microarchitectures_in_root( self, spec_str, factory, module_configuration ): module_configuration("complex_hierarchy") writer, spec = factory(spec_str) assert str(spec.target.family) in writer.layout.arch_dirname if spec.target.family != spec.target: assert str(spec.target) not in writer.layout.arch_dirname def test_projections_specific(self, factory, module_configuration): """Tests reading the correct naming scheme.""" # This configuration has no error, so check the conflicts directives # are there module_configuration("projections") # Test we read the expected configuration for the naming scheme writer, _ = factory("mpileaks") expected = {"all": "{name}/v{version}", "mpileaks": "{name}-mpiprojection"} assert writer.conf.projections == expected projection = writer.spec.format(writer.conf.projections["mpileaks"]) assert projection in writer.layout.use_name def test_projections_all(self, factory, module_configuration): """Tests reading the correct naming scheme.""" # This configuration has no error, so check the conflicts directives # are there module_configuration("projections") # Test we read the expected configuration for the naming scheme writer, _ = factory("libelf") expected = {"all": "{name}/v{version}", "mpileaks": "{name}-mpiprojection"} assert writer.conf.projections == expected projection = writer.spec.format(writer.conf.projections["all"]) assert projection in writer.layout.use_name def test_modules_relative_to_view( self, tmpdir, modulefile_content, module_configuration, install_mockery, mock_fetch ): with ev.create_in_dir(str(tmpdir), with_view=True) as e: module_configuration("with_view") install("--add", "cmake") spec = spack.spec.Spec("cmake").concretized() content = modulefile_content("cmake") expected = e.default_view.get_projection_for_spec(spec) # Rather than parse all lines, ensure all prefixes in the content # point to the right one assert any(expected in line for line in content) assert not any(spec.prefix in line for line in content) def test_modules_no_arch(self, factory, module_configuration): module_configuration("no_arch") module, spec = factory(mpileaks_spec_string) path = module.layout.filename assert str(spec.os) not in path def test_hide_implicits(self, module_configuration, temporary_store): """Tests the addition and removal of hide command in modulerc.""" module_configuration("hide_implicits") spec = spack.spec.Spec("mpileaks@2.3").concretized() # mpileaks is defined as implicit, thus hide command should appear in modulerc writer = writer_cls(spec, "default", False) writer.write() assert os.path.exists(writer.layout.modulerc) with open(writer.layout.modulerc) as f: content = [line.strip() for line in f.readlines()] hide_implicit_mpileaks = f'hide_version("{writer.layout.use_name}")' assert len([x for x in content if hide_implicit_mpileaks == x]) == 1 # The direct dependencies are all implicitly installed, and they should all be hidden, # except for mpich, which is provider for mpi, which is in the hierarchy, and therefore # can't be hidden. All other hidden modules should have a 7 character hash (the config # hash_length = 0 only applies to exposed modules). with open(writer.layout.filename) as f: depends_statements = [line.strip() for line in f.readlines() if "depends_on" in line] for dep in spec.dependencies(deptype=("link", "run")): if dep.satisfies("mpi"): assert not any(dep.dag_hash(7) in line for line in depends_statements) else: assert any(dep.dag_hash(7) in line for line in depends_statements) # when mpileaks becomes explicit, its file name changes (hash_length = 0), meaning an # extra module file is created; the old one still exists and remains hidden. writer = writer_cls(spec, "default", True) writer.write() assert os.path.exists(writer.layout.modulerc) with open(writer.layout.modulerc) as f: content = [line.strip() for line in f.readlines()] assert hide_implicit_mpileaks in content # old, implicit mpileaks is still hidden assert f'hide_version("{writer.layout.use_name}")' not in content # after removing both the implicit and explicit module, the modulerc file would be empty # and should be removed. writer_cls(spec, "default", False).remove() writer_cls(spec, "default", True).remove() assert not os.path.exists(writer.layout.modulerc) assert not os.path.exists(writer.layout.filename) # implicit module is removed writer = writer_cls(spec, "default", False) writer.write() assert os.path.exists(writer.layout.filename) assert os.path.exists(writer.layout.modulerc) writer.remove() assert not os.path.exists(writer.layout.modulerc) assert not os.path.exists(writer.layout.filename) # three versions of mpileaks are implicit writer = writer_cls(spec, "default", False) writer.write(overwrite=True) spec_alt1 = spack.spec.Spec("mpileaks@2.2").concretized() spec_alt2 = spack.spec.Spec("mpileaks@2.1").concretized() writer_alt1 = writer_cls(spec_alt1, "default", False) writer_alt1.write(overwrite=True) writer_alt2 = writer_cls(spec_alt2, "default", False) writer_alt2.write(overwrite=True) assert os.path.exists(writer.layout.modulerc) with open(writer.layout.modulerc) as f: content = [line.strip() for line in f.readlines()] hide_cmd = f'hide_version("{writer.layout.use_name}")' hide_cmd_alt1 = f'hide_version("{writer_alt1.layout.use_name}")' hide_cmd_alt2 = f'hide_version("{writer_alt2.layout.use_name}")' assert len([x for x in content if hide_cmd == x]) == 1 assert len([x for x in content if hide_cmd_alt1 == x]) == 1 assert len([x for x in content if hide_cmd_alt2 == x]) == 1 # one version is removed writer_alt1.remove() assert os.path.exists(writer.layout.modulerc) with open(writer.layout.modulerc) as f: content = [line.strip() for line in f.readlines()] assert len([x for x in content if hide_cmd == x]) == 1 assert len([x for x in content if hide_cmd_alt1 == x]) == 0 assert len([x for x in content if hide_cmd_alt2 == x]) == 1