From 8a9c501030961edef5340ba8b4c6239ed0572dd9 Mon Sep 17 00:00:00 2001 From: Harmen Stoppels Date: Mon, 8 Jul 2024 14:53:51 +0200 Subject: [PATCH] spec.py: fix __getitem__ looking outside of dag (#45090) `Spec.__getitem__` queries dependent edges, which almost always point to nodes outside the sub-dag considered. It should only ever look at edges being traversed. --- lib/spack/spack/spec.py | 28 ++++++++++------------------ lib/spack/spack/test/spec_dag.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index 27c762bd40..5b7ad3e881 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -4164,29 +4164,21 @@ def __getitem__(self, name: str): csv = query_parameters.pop().strip() query_parameters = re.split(r"\s*,\s*", csv) - # In some cases a package appears multiple times in the same DAG for *distinct* - # specs. For example, a build-type dependency may itself depend on a package - # the current spec depends on, but their specs may differ. Therefore we iterate - # in an order here that prioritizes the build, test and runtime dependencies; - # only when we don't find the package do we consider the full DAG. order = lambda: itertools.chain( - self.traverse(deptype="link"), - self.dependencies(deptype=dt.BUILD | dt.RUN | dt.TEST), - self.traverse(), # fall back to a full search + self.traverse_edges(deptype=dt.LINK, order="breadth", cover="edges"), + self.edges_to_dependencies(depflag=dt.BUILD | dt.RUN | dt.TEST), + self.traverse_edges(deptype=dt.ALL, order="breadth", cover="edges"), ) + # Consider runtime dependencies and direct build/test deps before transitive dependencies, + # and prefer matches closest to the root. try: child: Spec = next( - itertools.chain( - # Regular specs - (x for x in order() if x.name == name), - ( - x - for x in order() - if (not x.virtual) - and any(name in edge.virtuals for edge in x.edges_from_dependents()) - ), - (x for x in order() if (not x.virtual) and x.package.provides(name)), + e.spec + for e in itertools.chain( + (e for e in order() if e.spec.name == name or name in e.virtuals), + # for historical reasons + (e for e in order() if e.spec.concrete and e.spec.package.provides(name)), ) ) except StopIteration: diff --git a/lib/spack/spack/test/spec_dag.py b/lib/spack/spack/test/spec_dag.py index da85be6d1c..39e9ff66a1 100644 --- a/lib/spack/spack/test/spec_dag.py +++ b/lib/spack/spack/test/spec_dag.py @@ -1105,3 +1105,23 @@ def test_indexing_prefers_direct_or_transitive_link_deps(): # Ensure that the full DAG is still searched assert root["a2"] + + +def test_getitem_sticks_to_subdag(): + """Test that indexing on Spec by virtual does not traverse outside the dag, which happens in + the unlikely case someone would rewrite __getitem__ in terms of edges_from_dependents instead + of edges_to_dependencies.""" + x, y, z = Spec("x"), Spec("y"), Spec("z") + x.add_dependency_edge(z, depflag=dt.LINK, virtuals=("virtual",)) + y.add_dependency_edge(z, depflag=dt.LINK, virtuals=()) + assert x["virtual"].name == "z" + with pytest.raises(KeyError): + y["virtual"] + + +def test_getitem_finds_transitive_virtual(): + x, y, z = Spec("x"), Spec("y"), Spec("z") + x.add_dependency_edge(z, depflag=dt.LINK, virtuals=()) + x.add_dependency_edge(y, depflag=dt.LINK, virtuals=()) + y.add_dependency_edge(z, depflag=dt.LINK, virtuals=("virtual",)) + assert x["virtual"].name == "z"