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.
This commit is contained in:
Harmen Stoppels 2024-07-08 14:53:51 +02:00 committed by Harmen Stoppels
parent 9f035ca030
commit 8a9c501030
2 changed files with 30 additions and 18 deletions

View file

@ -4164,29 +4164,21 @@ def __getitem__(self, name: str):
csv = query_parameters.pop().strip() csv = query_parameters.pop().strip()
query_parameters = re.split(r"\s*,\s*", csv) 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( order = lambda: itertools.chain(
self.traverse(deptype="link"), self.traverse_edges(deptype=dt.LINK, order="breadth", cover="edges"),
self.dependencies(deptype=dt.BUILD | dt.RUN | dt.TEST), self.edges_to_dependencies(depflag=dt.BUILD | dt.RUN | dt.TEST),
self.traverse(), # fall back to a full search 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: try:
child: Spec = next( child: Spec = next(
itertools.chain( e.spec
# Regular specs for e in itertools.chain(
(x for x in order() if x.name == name), (e for e in order() if e.spec.name == name or name in e.virtuals),
( # for historical reasons
x (e for e in order() if e.spec.concrete and e.spec.package.provides(name)),
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)),
) )
) )
except StopIteration: except StopIteration:

View file

@ -1105,3 +1105,23 @@ def test_indexing_prefers_direct_or_transitive_link_deps():
# Ensure that the full DAG is still searched # Ensure that the full DAG is still searched
assert root["a2"] 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"