521 lines
22 KiB
Python
521 lines
22 KiB
Python
# Copyright 2013-2023 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
|