diff options
author | Peter Scheibel <scheibel1@llnl.gov> | 2024-11-11 10:13:31 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-11 10:13:31 -0800 |
commit | 9ed5e1de8e81eff9fb2d746305d146d5c4cd64f5 (patch) | |
tree | a00cc0658fde7b28b0dcb3019210c067fffd9a50 /lib | |
parent | 4eb7b998e842e3f3deb4ff73391128e7e75099ad (diff) | |
download | spack-9ed5e1de8e81eff9fb2d746305d146d5c4cd64f5.tar.gz spack-9ed5e1de8e81eff9fb2d746305d146d5c4cd64f5.tar.bz2 spack-9ed5e1de8e81eff9fb2d746305d146d5c4cd64f5.tar.xz spack-9ed5e1de8e81eff9fb2d746305d146d5c4cd64f5.zip |
Bugfix: `spack find -x` in environments (#46798)
This addresses part [1] of #46345
#44713 introduced a bug where all non-spec query parameters like date
ranges, -x, etc. were ignored when an env was active.
This fixes that issue and adds tests for it.
---------
Co-authored-by: Harmen Stoppels <me@harmenstoppels.nl>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/spack/cmd/__init__.py | 12 | ||||
-rw-r--r-- | lib/spack/spack/cmd/find.py | 90 | ||||
-rw-r--r-- | lib/spack/spack/cmd/modules/__init__.py | 13 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/find.py | 140 | ||||
-rw-r--r-- | lib/spack/spack/test/utilities.py | 32 |
5 files changed, 242 insertions, 45 deletions
diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index e9df5fc189..c0efd52521 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -545,6 +545,18 @@ class CommandNameError(spack.error.SpackError): super().__init__("{0} is not a permissible Spack command name.".format(name)) +class MultipleSpecsMatch(Exception): + """Raised when multiple specs match a constraint, in a context where + this is not allowed. + """ + + +class NoSpecMatches(Exception): + """Raised when no spec matches a constraint, in a context where + this is not allowed. + """ + + ######################################## # argparse types for argument validation ######################################## diff --git a/lib/spack/spack/cmd/find.py b/lib/spack/spack/cmd/find.py index 079c5bf4d3..c6930097ac 100644 --- a/lib/spack/spack/cmd/find.py +++ b/lib/spack/spack/cmd/find.py @@ -222,11 +222,9 @@ def make_env_decorator(env): def display_env(env, args, decorator, results): """Display extra find output when running in an environment. - Find in an environment outputs 2 or 3 sections: - - 1. Root specs - 2. Concretized roots (if asked for with -c) - 3. Installed specs + In an environment, `spack find` outputs a preliminary section + showing the root specs of the environment (this is in addition + to the section listing out specs matching the query parameters). """ tty.msg("In environment %s" % env.name) @@ -299,42 +297,70 @@ def display_env(env, args, decorator, results): print() -def find(parser, args): - env = ev.active_environment() - - if not env and args.only_roots: - tty.die("-r / --only-roots requires an active environment") - if not env and args.show_concretized: - tty.die("-c / --show-concretized requires an active environment") - +def _find_query(args, env): + q_args = query_arguments(args) + concretized_but_not_installed = list() if env: + all_env_specs = env.all_specs() if args.constraint: - init_specs = spack.cmd.parse_specs(args.constraint) - results = env.all_matching_specs(*init_specs) + init_specs = cmd.parse_specs(args.constraint) + env_specs = env.all_matching_specs(*init_specs) else: - results = env.all_specs() + env_specs = all_env_specs + + spec_hashes = set(x.dag_hash() for x in env_specs) + specs_meeting_q_args = set(spack.store.STORE.db.query(hashes=spec_hashes, **q_args)) + + results = list() + with spack.store.STORE.db.read_transaction(): + for spec in env_specs: + if not spec.installed: + concretized_but_not_installed.append(spec) + if spec in specs_meeting_q_args: + results.append(spec) else: - q_args = query_arguments(args) results = args.specs(**q_args) - decorator = make_env_decorator(env) if env else lambda s, f: f - # use groups by default except with format. if args.groups is None: args.groups = not args.format # Exit early with an error code if no package matches the constraint - if not results and args.constraint: - constraint_str = " ".join(str(s) for s in args.constraint_specs) - tty.die(f"No package matches the query: {constraint_str}") + if concretized_but_not_installed and args.show_concretized: + pass + elif results: + pass + elif args.constraint: + raise cmd.NoSpecMatches() # If tags have been specified on the command line, filter by tags if args.tags: packages_with_tags = spack.repo.PATH.packages_with_tags(*args.tags) results = [x for x in results if x.name in packages_with_tags] + concretized_but_not_installed = [ + x for x in concretized_but_not_installed if x.name in packages_with_tags + ] if args.loaded: - results = spack.cmd.filter_loaded_specs(results) + results = cmd.filter_loaded_specs(results) + + return results, concretized_but_not_installed + + +def find(parser, args): + env = ev.active_environment() + + if not env and args.only_roots: + tty.die("-r / --only-roots requires an active environment") + if not env and args.show_concretized: + tty.die("-c / --show-concretized requires an active environment") + + try: + results, concretized_but_not_installed = _find_query(args, env) + except cmd.NoSpecMatches: + # Note: this uses args.constraint vs. args.constraint_specs because + # the latter only exists if you call args.specs() + tty.die(f"No package matches the query: {' '.join(args.constraint)}") if args.install_status or args.show_concretized: status_fn = spack.spec.Spec.install_status @@ -345,14 +371,16 @@ def find(parser, args): if args.json: cmd.display_specs_as_json(results, deps=args.deps) else: + decorator = make_env_decorator(env) if env else lambda s, f: f + if not args.format: if env: display_env(env, args, decorator, results) if not args.only_roots: - display_results = results - if not args.show_concretized: - display_results = list(x for x in results if x.installed) + display_results = list(results) + if args.show_concretized: + display_results += concretized_but_not_installed cmd.display_specs( display_results, args, decorator=decorator, all_headers=True, status_fn=status_fn ) @@ -370,13 +398,9 @@ def find(parser, args): concretized_suffix += " (show with `spack find -c`)" pkg_type = "loaded" if args.loaded else "installed" - spack.cmd.print_how_many_pkgs( - list(x for x in results if x.installed), pkg_type, suffix=installed_suffix - ) + cmd.print_how_many_pkgs(results, pkg_type, suffix=installed_suffix) if env: - spack.cmd.print_how_many_pkgs( - list(x for x in results if not x.installed), - "concretized", - suffix=concretized_suffix, + cmd.print_how_many_pkgs( + concretized_but_not_installed, "concretized", suffix=concretized_suffix ) diff --git a/lib/spack/spack/cmd/modules/__init__.py b/lib/spack/spack/cmd/modules/__init__.py index 754813addc..013f4723db 100644 --- a/lib/spack/spack/cmd/modules/__init__.py +++ b/lib/spack/spack/cmd/modules/__init__.py @@ -19,6 +19,7 @@ import spack.error import spack.modules import spack.modules.common import spack.repo +from spack.cmd import MultipleSpecsMatch, NoSpecMatches from spack.cmd.common import arguments description = "manipulate module files" @@ -91,18 +92,6 @@ def add_loads_arguments(subparser): arguments.add_common_arguments(subparser, ["recurse_dependencies"]) -class MultipleSpecsMatch(Exception): - """Raised when multiple specs match a constraint, in a context where - this is not allowed. - """ - - -class NoSpecMatches(Exception): - """Raised when no spec matches a constraint, in a context where - this is not allowed. - """ - - def one_spec_or_raise(specs): """Ensures exactly one spec has been selected, or raises the appropriate exception. diff --git a/lib/spack/spack/test/cmd/find.py b/lib/spack/spack/test/cmd/find.py index d947362f18..779c6a942f 100644 --- a/lib/spack/spack/test/cmd/find.py +++ b/lib/spack/spack/test/cmd/find.py @@ -14,10 +14,13 @@ import pytest import spack.cmd as cmd import spack.cmd.find import spack.environment as ev +import spack.repo import spack.store import spack.user_environment as uenv from spack.main import SpackCommand from spack.spec import Spec +from spack.test.conftest import create_test_repo +from spack.test.utilities import SpackCommandArgs from spack.util.pattern import Bunch find = SpackCommand("find") @@ -453,3 +456,140 @@ def test_environment_with_version_range_in_compiler_doesnt_fail(tmp_path): with test_environment: output = find() assert "zlib%gcc@12.1.0" in output + + +_pkga = ( + "a0", + """\ +class A0(Package): + version("1.2") + version("1.1") + + depends_on("b0") + depends_on("c0") +""", +) + + +_pkgb = ( + "b0", + """\ +class B0(Package): + version("1.2") + version("1.1") +""", +) + + +_pkgc = ( + "c0", + """\ +class C0(Package): + version("1.2") + version("1.1") + + tags = ["tag0", "tag1"] +""", +) + + +_pkgd = ( + "d0", + """\ +class D0(Package): + version("1.2") + version("1.1") + + depends_on("c0") + depends_on("e0") +""", +) + + +_pkge = ( + "e0", + """\ +class E0(Package): + tags = ["tag1", "tag2"] + + version("1.2") + version("1.1") +""", +) + + +@pytest.fixture +def _create_test_repo(tmpdir, mutable_config): + r""" + a0 d0 + / \ / \ + b0 c0 e0 + """ + yield create_test_repo(tmpdir, [_pkga, _pkgb, _pkgc, _pkgd, _pkge]) + + +@pytest.fixture +def test_repo(_create_test_repo, monkeypatch, mock_stage): + with spack.repo.use_repositories(_create_test_repo) as mock_repo_path: + yield mock_repo_path + + +def test_find_concretized_not_installed( + mutable_mock_env_path, install_mockery, mock_fetch, test_repo, mock_archive +): + """Test queries against installs of specs against fake repo. + + Given A, B, C, D, E, create an environment and install A. + Add and concretize (but do not install) D. + Test a few queries after force uninstalling a dependency of A (but not + A itself). + """ + add = SpackCommand("add") + concretize = SpackCommand("concretize") + uninstall = SpackCommand("uninstall") + + def _query(_e, *args): + return spack.cmd.find._find_query(SpackCommandArgs("find")(*args), _e) + + def _nresults(_qresult): + return len(_qresult[0]), len(_qresult[1]) + + env("create", "test") + with ev.read("test") as e: + install("--fake", "--add", "a0") + + assert _nresults(_query(e)) == (3, 0) + assert _nresults(_query(e, "--explicit")) == (1, 0) + + add("d0") + concretize("--reuse") + + # At this point d0 should use existing c0, but d/e + # are not installed in the env + + # --explicit, --deprecated, --start-date, etc. are all + # filters on records, and therefore don't apply to + # concretized-but-not-installed results + assert _nresults(_query(e, "--explicit")) == (1, 2) + + assert _nresults(_query(e)) == (3, 2) + assert _nresults(_query(e, "-c", "d0")) == (0, 1) + + uninstall("-f", "-y", "b0") + + # b0 is now missing (it is not installed, but has an + # installed parent) + + assert _nresults(_query(e)) == (2, 3) + # b0 is "double-counted" here: it meets the --missing + # criteria, and also now qualifies as a + # concretized-but-not-installed spec + assert _nresults(_query(e, "--missing")) == (3, 3) + assert _nresults(_query(e, "--only-missing")) == (1, 3) + + # Tags are not attached to install records, so they + # can modify the concretized-but-not-installed results + + assert _nresults(_query(e, "--tag=tag0")) == (1, 0) + assert _nresults(_query(e, "--tag=tag1")) == (1, 1) + assert _nresults(_query(e, "--tag=tag2")) == (0, 1) diff --git a/lib/spack/spack/test/utilities.py b/lib/spack/spack/test/utilities.py new file mode 100644 index 0000000000..5e83db9da2 --- /dev/null +++ b/lib/spack/spack/test/utilities.py @@ -0,0 +1,32 @@ +# 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) + +"""Non-fixture utilities for test code. Must be imported. +""" +from spack.main import make_argument_parser + + +class SpackCommandArgs: + """Use this to get an Args object like what is passed into + a command. + + Useful for emulating args in unit tests that want to check + helper functions in Spack commands. Ensures that you get all + the default arg values established by the parser. + + Example usage:: + + install_args = SpackCommandArgs("install")("-v", "mpich") + """ + + def __init__(self, command_name): + self.parser = make_argument_parser() + self.command_name = command_name + + def __call__(self, *argv, **kwargs): + self.parser.add_command(self.command_name) + prepend = kwargs["global_args"] if "global_args" in kwargs else [] + args, unknown = self.parser.parse_known_args(prepend + [self.command_name] + list(argv)) + return args |