diff --git a/lib/spack/spack/test/cmd/external.py b/lib/spack/spack/test/cmd/external.py index a4fb3fa823d..060e578d557 100644 --- a/lib/spack/spack/test/cmd/external.py +++ b/lib/spack/spack/test/cmd/external.py @@ -18,6 +18,8 @@ from spack.main import SpackCommand from spack.spec import Spec +pytestmark = [pytest.mark.usefixtures("mock_packages")] + @pytest.fixture def executables_found(monkeypatch): @@ -67,13 +69,24 @@ def test_get_executables(working_env, mock_executable): # TODO: this test should be made to work, but in the meantime it is # causing intermittent (spurious) CI failures on all PRs @pytest.mark.not_on_windows("Test fails intermittently on Windows") -def test_find_external_cmd_not_buildable(mutable_config, working_env, mock_executable): +def test_find_external_cmd_not_buildable( + mutable_config, working_env, mock_executable, monkeypatch +): """When the user invokes 'spack external find --not-buildable', the config for any package where Spack finds an external version should be marked as not buildable. """ - cmake_path1 = mock_executable("cmake", output="echo cmake version 1.foo") - os.environ["PATH"] = os.pathsep.join([os.path.dirname(cmake_path1)]) + version = "1.foo" + + @classmethod + def _determine_version(cls, exe): + return version + + cmake_cls = spack.repo.PATH.get_pkg_class("cmake") + monkeypatch.setattr(cmake_cls, "determine_version", _determine_version) + + cmake_path = mock_executable("cmake", output=f"echo cmake version {version}") + os.environ["PATH"] = str(cmake_path.parent) external("find", "--not-buildable", "cmake") pkgs_cfg = spack.config.get("packages") assert "cmake" in pkgs_cfg @@ -89,10 +102,12 @@ def test_find_external_cmd_not_buildable(mutable_config, working_env, mock_execu ["detectable"], [], [ + "builtin.mock.cmake", "builtin.mock.find-externals1", "builtin.mock.gcc", - "builtin.mock.llvm", "builtin.mock.intel-oneapi-compilers", + "builtin.mock.llvm", + "builtin.mock.mpich", ], ), # find --all --exclude find-externals1 @@ -100,26 +115,38 @@ def test_find_external_cmd_not_buildable(mutable_config, working_env, mock_execu None, ["detectable"], ["builtin.mock.find-externals1"], - ["builtin.mock.gcc", "builtin.mock.llvm", "builtin.mock.intel-oneapi-compilers"], + [ + "builtin.mock.cmake", + "builtin.mock.gcc", + "builtin.mock.intel-oneapi-compilers", + "builtin.mock.llvm", + "builtin.mock.mpich", + ], ), ( None, ["detectable"], ["find-externals1"], - ["builtin.mock.gcc", "builtin.mock.llvm", "builtin.mock.intel-oneapi-compilers"], + [ + "builtin.mock.cmake", + "builtin.mock.gcc", + "builtin.mock.intel-oneapi-compilers", + "builtin.mock.llvm", + "builtin.mock.mpich", + ], ), - # find cmake (and cmake is not detectable) - (["cmake"], ["detectable"], [], []), + # find hwloc (and mock hwloc is not detectable) + (["hwloc"], ["detectable"], [], []), ], ) -def test_package_selection(names, tags, exclude, expected, mutable_mock_repo): +def test_package_selection(names, tags, exclude, expected): """Tests various cases of selecting packages""" # In the mock repo we only have 'find-externals1' that is detectable result = spack.cmd.external.packages_to_search_for(names=names, tags=tags, exclude=exclude) assert set(result) == set(expected) -def test_find_external_no_manifest(mutable_config, working_env, mutable_mock_repo, monkeypatch): +def test_find_external_no_manifest(mutable_config, working_env, monkeypatch): """The user runs 'spack external find'; the default path for storing manifest files does not exist. Ensure that the command does not fail. @@ -132,7 +159,7 @@ def test_find_external_no_manifest(mutable_config, working_env, mutable_mock_rep def test_find_external_empty_default_manifest_dir( - mutable_config, working_env, mutable_mock_repo, tmpdir, monkeypatch + mutable_config, working_env, tmpdir, monkeypatch ): """The user runs 'spack external find'; the default path for storing manifest files exists but is empty. Ensure that the command does not @@ -147,7 +174,7 @@ def test_find_external_empty_default_manifest_dir( @pytest.mark.not_on_windows("Can't chmod on Windows") @pytest.mark.skipif(getuid() == 0, reason="user is root") def test_find_external_manifest_with_bad_permissions( - mutable_config, working_env, mutable_mock_repo, tmpdir, monkeypatch + mutable_config, working_env, tmpdir, monkeypatch ): """The user runs 'spack external find'; the default path for storing manifest files exists but with insufficient permissions. Check that @@ -167,7 +194,7 @@ def test_find_external_manifest_with_bad_permissions( os.chmod(test_manifest_file_path, 0o700) -def test_find_external_manifest_failure(mutable_config, mutable_mock_repo, tmpdir, monkeypatch): +def test_find_external_manifest_failure(mutable_config, tmpdir, monkeypatch): """The user runs 'spack external find'; the manifest parsing fails with some exception. Ensure that the command still succeeds (i.e. moves on to other external detection mechanisms). @@ -187,7 +214,7 @@ def fail(): assert "Skipping manifest and continuing" in output -def test_find_external_merge(mutable_config, mutable_mock_repo, tmp_path): +def test_find_external_merge(mutable_config, tmp_path): """Checks that 'spack find external' doesn't overwrite an existing spec in packages.yaml.""" pkgs_cfg_init = { "find-externals1": { @@ -213,7 +240,7 @@ def test_find_external_merge(mutable_config, mutable_mock_repo, tmp_path): assert {"spec": "find-externals1@1.2", "prefix": "/x/y2"} in pkg_externals -def test_list_detectable_packages(mutable_config, mutable_mock_repo): +def test_list_detectable_packages(mutable_config): external("list") assert external.returncode == 0 @@ -259,13 +286,23 @@ def test_new_entries_are_reported_correctly(mock_executable, mutable_config, mon @pytest.mark.parametrize("command_args", [("-t", "build-tools"), ("-t", "build-tools", "cmake")]) +@pytest.mark.not_on_windows("the test uses bash scripts") def test_use_tags_for_detection(command_args, mock_executable, mutable_config, monkeypatch): + versions = {"cmake": "3.19.1", "openssl": "2.8.3"} + + @classmethod + def _determine_version(cls, exe): + return versions[os.path.basename(exe)] + + cmake_cls = spack.repo.PATH.get_pkg_class("cmake") + monkeypatch.setattr(cmake_cls, "determine_version", _determine_version) + # Prepare an environment to detect a fake cmake - cmake_exe = mock_executable("cmake", output="echo cmake version 3.19.1") + cmake_exe = mock_executable("cmake", output=f"echo cmake version {versions['cmake']}") prefix = os.path.dirname(cmake_exe) monkeypatch.setenv("PATH", prefix) - openssl_exe = mock_executable("openssl", output="OpenSSL 2.8.3") + openssl_exe = mock_executable("openssl", output=f"OpenSSL {versions['openssl']}") prefix = os.path.dirname(openssl_exe) monkeypatch.setenv("PATH", prefix) @@ -282,6 +319,16 @@ def test_failures_in_scanning_do_not_result_in_an_error( mock_executable, monkeypatch, mutable_config ): """Tests that scanning paths with wrong permissions, won't cause `external find` to error.""" + versions = {"first": "3.19.1", "second": "3.23.3"} + + @classmethod + def _determine_version(cls, exe): + bin_parent = os.path.dirname(exe).split(os.sep)[-2] + return versions[bin_parent] + + cmake_cls = spack.repo.PATH.get_pkg_class("cmake") + monkeypatch.setattr(cmake_cls, "determine_version", _determine_version) + cmake_exe1 = mock_executable( "cmake", output="echo cmake version 3.19.1", subdir=("first", "bin") ) @@ -299,21 +346,30 @@ def test_failures_in_scanning_do_not_result_in_an_error( assert external.returncode == 0 assert "The following specs have been" in output assert "cmake" in output - assert "3.23.3" in output - assert "3.19.1" not in output + for vers in versions.values(): + assert vers in output def test_detect_virtuals(mock_executable, mutable_config, monkeypatch): """Test whether external find --not-buildable sets virtuals as non-buildable (unless user config sets them to buildable)""" - mpich = mock_executable("mpichversion", output="echo MPICH Version: 4.0.2") + version = "4.0.2" + + @classmethod + def _determine_version(cls, exe): + return version + + cmake_cls = spack.repo.PATH.get_pkg_class("mpich") + monkeypatch.setattr(cmake_cls, "determine_version", _determine_version) + + mpich = mock_executable("mpichversion", output=f"echo MPICH Version: {version}") prefix = os.path.dirname(mpich) external("find", "--path", prefix, "--not-buildable", "mpich") # Check that mpich was correctly detected mpich = mutable_config.get("packages:mpich") assert mpich["buildable"] is False - assert Spec(mpich["externals"][0]["spec"]).satisfies("mpich@4.0.2") + assert Spec(mpich["externals"][0]["spec"]).satisfies(f"mpich@{version}") # Check that the virtual package mpi was marked as non-buildable assert mutable_config.get("packages:mpi:buildable") is False diff --git a/var/spack/test_repos/builtin.mock/packages/cmake/package.py b/var/spack/test_repos/builtin.mock/packages/cmake/package.py index d33ec83a5fe..b0f40fe6b5a 100644 --- a/var/spack/test_repos/builtin.mock/packages/cmake/package.py +++ b/var/spack/test_repos/builtin.mock/packages/cmake/package.py @@ -21,6 +21,7 @@ class Cmake(Package): url = "https://cmake.org/files/v3.4/cmake-3.4.3.tar.gz" tags = ["build-tools"] + executables = ["^cmake[0-9]*$"] depends_on("c", type="build") depends_on("cxx", type="build") @@ -36,6 +37,12 @@ class Cmake(Package): url="https://cmake.org/files/v3.4/cmake-3.4.3.tar.gz", ) + @classmethod + def determine_version(cls, exe): + output = Executable(exe)("--version", output=str, error=str) + match = re.search(r"cmake.*version\s+(\S+)", output) + return match.group(1) if match else None + def setup_build_environment(self, env: EnvironmentModifications) -> None: spack_cc # Ensure spack module-scope variable is avaiable env.set("for_install", "for_install") diff --git a/var/spack/test_repos/builtin.mock/packages/mpich/package.py b/var/spack/test_repos/builtin.mock/packages/mpich/package.py index a6a0e049d08..2b242b56d5c 100644 --- a/var/spack/test_repos/builtin.mock/packages/mpich/package.py +++ b/var/spack/test_repos/builtin.mock/packages/mpich/package.py @@ -12,6 +12,7 @@ class Mpich(Package): list_depth = 2 tags = ["tag1", "tag2"] + executables = ["^mpichversion$"] variant("debug", default=False, description="Compile MPICH with debug flags.") @@ -30,6 +31,12 @@ class Mpich(Package): depends_on("cxx", type="build") depends_on("fortran", type="build") + @classmethod + def determine_version(cls, exe): + output = Executable(exe)(output=str, error=str) + match = re.search(r"MPICH Version:\s+(\S+)", output) + return match.group(1) if match else None + def install(self, spec, prefix): touch(prefix.mpich)