Fix broken spack find -u (#47102)

fixes #47101

The bug was introduced in #33495, where `spack find was not updated,
and wasn't caught by unit tests.

Now a Database can accept a custom predicate to select the installation
records. A unit test is added to prevent regressions. The weird convention
of having `any` as a default value has been replaced by the more commonly
used `None`.

Signed-off-by: Massimiliano Culpo <massimiliano.culpo@gmail.com>
This commit is contained in:
Massimiliano Culpo 2024-10-21 18:03:57 +02:00 committed by GitHub
parent 590be9bba1
commit 4187c57250
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 37 additions and 17 deletions

View File

@ -174,9 +174,9 @@ def query_arguments(args):
if (args.missing or args.only_missing) and not args.only_deprecated: if (args.missing or args.only_missing) and not args.only_deprecated:
installed.append(InstallStatuses.MISSING) installed.append(InstallStatuses.MISSING)
known = any predicate_fn = None
if args.unknown: if args.unknown:
known = False predicate_fn = lambda x: not spack.repo.PATH.exists(x.spec.name)
explicit = any explicit = any
if args.explicit: if args.explicit:
@ -184,7 +184,7 @@ def query_arguments(args):
if args.implicit: if args.implicit:
explicit = False explicit = False
q_args = {"installed": installed, "known": known, "explicit": explicit} q_args = {"installed": installed, "predicate_fn": predicate_fn, "explicit": explicit}
install_tree = args.install_tree install_tree = args.install_tree
upstreams = spack.config.get("upstreams", {}) upstreams = spack.config.get("upstreams", {})

View File

@ -378,7 +378,10 @@ def refresh(module_type, specs, args):
def modules_cmd(parser, args, module_type, callbacks=callbacks): def modules_cmd(parser, args, module_type, callbacks=callbacks):
# Qualifiers to be used when querying the db for specs # Qualifiers to be used when querying the db for specs
constraint_qualifiers = { constraint_qualifiers = {
"refresh": {"installed": True, "known": lambda x: not spack.repo.PATH.exists(x)} "refresh": {
"installed": True,
"predicate_fn": lambda x: spack.repo.PATH.exists(x.spec.name),
}
} }
query_args = constraint_qualifiers.get(args.subparser_name, {}) query_args = constraint_qualifiers.get(args.subparser_name, {})

View File

@ -299,12 +299,9 @@ def __reduce__(self):
database. If it is a spec, we'll evaluate database. If it is a spec, we'll evaluate
``spec.satisfies(query_spec)`` ``spec.satisfies(query_spec)``
known (bool or None): Specs that are "known" are those predicate_fn: optional predicate taking an InstallRecord as argument, and returning
for which Spack can locate a ``package.py`` file -- i.e., whether that record is selected for the query. It can be used to craft criteria
Spack "knows" how to install them. Specs that are unknown may that need some data for selection not provided by the Database itself.
represent packages that existed in a previous version of
Spack, but have since either changed their name or
been removed
installed (bool or InstallStatus or typing.Iterable or None): installed (bool or InstallStatus or typing.Iterable or None):
if ``True``, includes only installed if ``True``, includes only installed
@ -604,6 +601,9 @@ def _path(self, spec: "spack.spec.Spec") -> pathlib.Path:
return self.dir / f"{spec.name}-{spec.dag_hash()}" return self.dir / f"{spec.name}-{spec.dag_hash()}"
SelectType = Callable[[InstallRecord], bool]
class Database: class Database:
#: Fields written for each install record #: Fields written for each install record
record_fields: Tuple[str, ...] = DEFAULT_INSTALL_RECORD_FIELDS record_fields: Tuple[str, ...] = DEFAULT_INSTALL_RECORD_FIELDS
@ -1526,7 +1526,7 @@ def get_by_hash(self, dag_hash, default=None, installed=any):
def _query( def _query(
self, self,
query_spec=any, query_spec=any,
known=any, predicate_fn: Optional[SelectType] = None,
installed=True, installed=True,
explicit=any, explicit=any,
start_date=None, start_date=None,
@ -1534,7 +1534,7 @@ def _query(
hashes=None, hashes=None,
in_buildcache=any, in_buildcache=any,
origin=None, origin=None,
): ) -> List["spack.spec.Spec"]:
"""Run a query on the database.""" """Run a query on the database."""
# TODO: Specs are a lot like queries. Should there be a # TODO: Specs are a lot like queries. Should there be a
@ -1580,7 +1580,7 @@ def _query(
if explicit is not any and rec.explicit != explicit: if explicit is not any and rec.explicit != explicit:
continue continue
if known is not any and known(rec.spec.name): if predicate_fn is not None and not predicate_fn(rec):
continue continue
if start_date or end_date: if start_date or end_date:
@ -1665,14 +1665,14 @@ def query(self, *args, **kwargs):
query.__doc__ = "" query.__doc__ = ""
query.__doc__ += _QUERY_DOCSTRING query.__doc__ += _QUERY_DOCSTRING
def query_one(self, query_spec, known=any, installed=True): def query_one(self, query_spec, predicate_fn=None, installed=True):
"""Query for exactly one spec that matches the query spec. """Query for exactly one spec that matches the query spec.
Raises an assertion error if more than one spec matches the Raises an assertion error if more than one spec matches the
query. Returns None if no installed package matches. query. Returns None if no installed package matches.
""" """
concrete_specs = self.query(query_spec, known=known, installed=installed) concrete_specs = self.query(query_spec, predicate_fn=predicate_fn, installed=installed)
assert len(concrete_specs) <= 1 assert len(concrete_specs) <= 1
return concrete_specs[0] if concrete_specs else None return concrete_specs[0] if concrete_specs else None

View File

@ -70,10 +70,10 @@ def test_query_arguments():
q_args = query_arguments(args) q_args = query_arguments(args)
assert "installed" in q_args assert "installed" in q_args
assert "known" in q_args assert "predicate_fn" in q_args
assert "explicit" in q_args assert "explicit" in q_args
assert q_args["installed"] == ["installed"] assert q_args["installed"] == ["installed"]
assert q_args["known"] is any assert q_args["predicate_fn"] is None
assert q_args["explicit"] is any assert q_args["explicit"] is any
assert "start_date" in q_args assert "start_date" in q_args
assert "end_date" not in q_args assert "end_date" not in q_args

View File

@ -1181,3 +1181,20 @@ def test_reindex_with_upstreams(tmp_path, monkeypatch, mock_packages, config):
assert not reindexed_local_store.db.query_local("callpath") assert not reindexed_local_store.db.query_local("callpath")
assert reindexed_local_store.db.query("callpath") == [callpath] assert reindexed_local_store.db.query("callpath") == [callpath]
assert reindexed_local_store.db.query_local("mpileaks") == [mpileaks] assert reindexed_local_store.db.query_local("mpileaks") == [mpileaks]
@pytest.mark.regression("47101")
def test_query_with_predicate_fn(database):
all_specs = database.query()
# Name starts with a string
specs = database.query(predicate_fn=lambda x: x.spec.name.startswith("mpil"))
assert specs and all(x.name.startswith("mpil") for x in specs)
assert len(specs) < len(all_specs)
# Recipe is currently known/unknown
specs = database.query(predicate_fn=lambda x: spack.repo.PATH.exists(x.spec.name))
assert specs == all_specs
specs = database.query(predicate_fn=lambda x: not spack.repo.PATH.exists(x.spec.name))
assert not specs