# Copyright 2013-2023 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)
"""
These tests check Spec DAG operations using dummy packages.
"""
import pytest
import spack.deptypes as dt
import spack.error
import spack.package_base
import spack.parser
import spack.repo
import spack.util.hash as hashutil
from spack.dependency import Dependency
from spack.spec import Spec
def check_links(spec_to_check):
for spec in spec_to_check.traverse():
for dependent in spec.dependents():
assert dependent.edges_to_dependencies(name=spec.name)
for dependency in spec.dependencies():
assert dependency.edges_from_dependents(name=spec.name)
@pytest.fixture()
def saved_deps():
"""Returns a dictionary to save the dependencies."""
return {}
@pytest.fixture()
def set_dependency(saved_deps, monkeypatch):
"""Returns a function that alters the dependency information
for a package in the ``saved_deps`` fixture.
"""
def _mock(pkg_name, spec):
"""Alters dependence information for a package.
Adds a dependency on <spec> to pkg. Use this to mock up constraints.
"""
spec = Spec(spec)
# Save original dependencies before making any changes.
pkg_cls = spack.repo.PATH.get_pkg_class(pkg_name)
if pkg_name not in saved_deps:
saved_deps[pkg_name] = (pkg_cls, pkg_cls.dependencies.copy())
cond = Spec(pkg_cls.name)
dependency = Dependency(pkg_cls, spec)
monkeypatch.setitem(pkg_cls.dependencies, spec.name, {cond: dependency})
return _mock
@pytest.mark.usefixtures("config")
def test_test_deptype(tmpdir):
"""Ensure that test-only dependencies are only included for specified
packages in the following spec DAG::
w
/|
x y
|
z
w->y deptypes are (link, build), w->x and y->z deptypes are (test)
"""
builder = spack.repo.MockRepositoryBuilder(tmpdir)
builder.add_package("x")
builder.add_package("z")
builder.add_package("y", dependencies=[("z", "test", None)])
builder.add_package("w", dependencies=[("x", "test", None), ("y", None, None)])
with spack.repo.use_repositories(builder.root):
spec = Spec("w").concretized(tests=("w",))
assert "x" in spec
assert "z" not in spec
@pytest.mark.usefixtures("config")
@pytest.mark.only_clingo("fails with the original concretizer and full hashes")
def test_installed_deps(monkeypatch, mock_packages):
"""Ensure that concrete specs and their build deps don't constrain solves.
Preinstall a package ``c`` that has a constrained build dependency on ``d``, then
install ``a`` and ensure that neither:
* ``c``'s package constraints, nor
* the concrete ``c``'s build dependencies
constrain ``a``'s dependency on ``d``.
"""
# see installed-deps-[abcde] test packages.
# a
# / \
# b c b --> d build/link
# |\ /| b --> e build/link
# |/ \| c --> d build
# d e c --> e build/link
#
a, b, c, d, e = ["installed-deps-%s" % s for s in "abcde"]
# install C, which will force d's version to be 2
# BUT d is only a build dependency of C, so it won't constrain
# link/run dependents of C when C is depended on as an existing
# (concrete) installation.
c_spec = Spec(c)
c_spec.concretize()
assert c_spec[d].version == spack.version.Version("2")
installed_names = [s.name for s in c_spec.traverse()]
def _mock_installed(self):
return self.name in installed_names
monkeypatch.setattr(Spec, "installed", _mock_installed)
# install A, which depends on B, C, D, and E, and force A to
# use the installed C. It should *not* force A to use the installed D
# *if* we're doing a fresh installation.
a_spec = Spec(a)
a_spec._add_dependency(c_spec, depflag=dt.BUILD | dt.LINK, virtuals=())
a_spec.concretize()
assert spack.version.Version("2") == a_spec[c][d].version
assert spack.version.Version("2") == a_spec[e].version
assert spack.version.Version("3") == a_spec[b][d].version
assert spack.version.Version("3") == a_spec[d].version
@pytest.mark.usefixtures("config")
def test_specify_preinstalled_dep(tmpdir, monkeypatch):
"""Specify the use of a preinstalled package during concretization with a
transitive dependency that is only supplied by the preinstalled package.
"""
builder = spack.repo.MockRepositoryBuilder(tmpdir)
builder.add_package("c")
builder.add_package("b", dependencies=[("c", None, None)])
builder.add_package("a", dependencies=[("b", None, None)])
with spack.repo.use_repositories(builder.root):
b_spec = Spec("b").concretized()
monkeypatch.setattr(Spec, "installed", property(lambda x: x.name != "a"))
a_spec = Spec("a")
a_spec._add_dependency(b_spec, depflag=dt.BUILD | dt.LINK, virtuals=())
a_spec.concretize()
assert set(x.name for x in a_spec.traverse()) == set(["a", "b", "c"])
@pytest.mark.usefixtures("config")
@pytest.mark.parametrize(
"spec_str,expr_str,expected",
[("x ^y@2", "y@2", True), ("x@1", "y", False), ("x", "y@3", True)],
)
def test_conditional_dep_with_user_constraints(tmpdir, spec_str, expr_str, expected):
"""This sets up packages X->Y such that X depends on Y conditionally. It
then constructs a Spec with X but with no constraints on X, so that the
initial normalization pass cannot determine whether the constraints are
met to add the dependency; this checks whether a user-specified constraint
on Y is applied properly.
"""
builder = spack.repo.MockRepositoryBuilder(tmpdir)
builder.add_package("y")
builder.add_package("x", dependencies=[("y", None, "x@2:")])
with spack.repo.use_repositories(builder.root):
spec = Spec(spec_str).concretized()
result = expr_str in spec
assert result is expected, "{0} in {1}".format(expr_str, spec)
@pytest.mark.usefixtures("mutable_mock_repo", "config")
class TestSpecDag:
def test_conflicting_package_constraints(self, set_dependency):
set_dependency("mpileaks", "mpich@1.0")
set_dependency("callpath", "mpich@2.0")
spec = Spec("mpileaks ^mpich ^callpath ^dyninst ^libelf ^libdwarf")
# TODO: try to do something to show that the issue was with
# TODO: the user's input or with package inconsistencies.
with pytest.raises(spack.spec.UnsatisfiableVersionSpecError):
spec.normalize()
def test_preorder_node_traversal(self):
dag = Spec("mpileaks ^zmpi")
dag.normalize()
names = ["mpileaks", "callpath", "dyninst", "libdwarf", "libelf", "zmpi", "fake"]
pairs = list(zip([0, 1, 2, 3, 4, 2, 3], names))
traversal = dag.traverse()
assert [x.name for x in traversal] == names
traversal = dag.traverse(depth=True)
assert [(x, y.name) for x, y in traversal] == pairs
def test_preorder_edge_traversal(self):
dag = Spec("mpileaks ^zmpi")
dag.normalize()
names = [
"mpileaks",
"callpath",
"dyninst",
"libdwarf",
"libelf",
"libelf",
"zmpi",
"fake",
"zmpi",
]
pairs = list(zip([0, 1, 2, 3, 4, 3, 2, 3, 1], names))
traversal = dag.traverse(cover="edges")
assert [x.name for x in traversal] == names
traversal = dag.traverse(cover="edges", depth=True)
assert [(x, y.name) for x, y in traversal] == pairs
def test_preorder_path_traversal(self):
dag = Spec("mpileaks ^zmpi")
dag.normalize()
names = [
"mpileaks",
"callpath",
"dyninst",
"libdwarf",
"libelf",
"libelf",
"zmpi",
"fake",
"zmpi",
"fake",
]
pairs = list(zip([0, 1, 2, 3, 4, 3, 2, 3, 1, 2], names))
traversal = dag.traverse(cover="paths")
assert [x.name for x in traversal] == names
traversal = dag.traverse(cover="paths", depth=True)
assert [(x, y.name) for x, y in traversal] == pairs
def test_postorder_node_traversal(self):
dag = Spec("mpileaks ^zmpi")
dag.normalize()
names = ["libelf", "libdwarf", "dyninst", "fake", "zmpi", "callpath", "mpileaks"]
pairs = list(zip([4, 3, 2, 3, 2, 1, 0], names))
traversal = dag.traverse(order="post")
assert [x.name for x in traversal] == names
traversal = dag.traverse(depth=True, order="post")
assert [(x, y.name) for x, y in traversal] == pairs
def test_postorder_edge_traversal(self):
dag = Spec("mpileaks ^zmpi")
dag.normalize()
names = [
"libelf",
"libdwarf",
"libelf",
"dyninst",
"fake",
"zmpi",
"callpath",
"zmpi",
"mpileaks",
]
pairs = list(zip([4, 3, 3, 2, 3, 2, 1, 1, 0], names))
traversal = dag.traverse(cover="edges", order="post")
assert [x.name for x in traversal] == names
traversal = dag.traverse(cover="edges", depth=True, order="post")
assert [(x, y.name) for x, y in traversal] == pairs
def test_postorder_path_traversal(self):
dag = Spec("mpileaks ^zmpi")
dag.normalize()
names = [
"libelf",
"libdwarf",
"libelf",
"dyninst",
"fake",
"zmpi",
"callpath",
"fake",
"zmpi",
"mpileaks",
]
pairs = list(zip([4, 3, 3, 2, 3, 2, 1, 2, 1, 0], names))
traversal = dag.traverse(cover="paths", order="post")
assert [x.name for x in traversal] == names
traversal = dag.traverse(cover="paths", depth=True, order="post")
assert [(x, y.name) for x, y in traversal] == pairs
def test_conflicting_spec_constraints(self):
mpileaks = Spec("mpileaks ^mpich ^callpath ^dyninst ^libelf ^libdwarf")
# Normalize then add conflicting constraints to the DAG (this is an
# extremely unlikely scenario, but we test for it anyway)
mpileaks.normalize()
mpileaks.edges_to_dependencies(name="mpich")[0].spec = Spec("mpich@1.0")
mpileaks.edges_to_dependencies(name="callpath")[0].spec.edges_to_dependencies(
name="mpich"
)[0].spec = Spec("mpich@2.0")
with pytest.raises(spack.spec.InconsistentSpecError):
mpileaks.flat_dependencies(copy=False)
def test_normalize_twice(self):
"""Make sure normalize can be run twice on the same spec,
and that it is idempotent."""
spec = Spec("mpileaks")
spec.normalize()
n1 = spec.copy()
spec.normalize()
assert n1 == spec
def test_normalize_a_lot(self):
spec = Spec("mpileaks")
spec.normalize()
spec.normalize()
spec.normalize()
spec.normalize()
def test_normalize_with_virtual_spec(self):
dag = Spec.from_literal(
{
"mpileaks": {
"callpath": {
"dyninst": {"libdwarf": {"libelf": None}, "libelf": None},
"mpi": None,
},
"mpi": None,
}
}
)
dag.normalize()
# make sure nothing with the same name occurs twice
counts = {}
for spec in dag.traverse(key=id):
if spec.name not in counts:
counts[spec.name] = 0
counts[spec.name] += 1
for name in counts:
assert counts[name] == 1
def test_dependents_and_dependencies_are_correct(self):
spec = Spec.from_literal(
{
"mpileaks": {
"callpath": {
"dyninst": {"libdwarf": {"libelf": None}, "libelf": None},
"mpi": None,
},
"mpi": None,
}
}
)
check_links(spec)
spec.normalize()
check_links(spec)
def test_unsatisfiable_version(self, set_dependency):
set_dependency("mpileaks", "mpich@1.0")
spec = Spec("mpileaks ^mpich@2.0 ^callpath ^dyninst ^libelf ^libdwarf")
with pytest.raises(spack.spec.UnsatisfiableVersionSpecError):
spec.normalize()
def test_unsatisfiable_compiler(self, set_dependency):
set_dependency("mpileaks", "mpich%gcc")
spec = Spec("mpileaks ^mpich%intel ^callpath ^dyninst ^libelf" " ^libdwarf")
with pytest.raises(spack.spec.UnsatisfiableCompilerSpecError):
spec.normalize()
def test_unsatisfiable_compiler_version(self, set_dependency):
set_dependency("mpileaks", "mpich%gcc@4.6")
spec = Spec("mpileaks ^mpich%gcc@4.5 ^callpath ^dyninst ^libelf" " ^libdwarf")
with pytest.raises(spack.spec.UnsatisfiableCompilerSpecError):
spec.normalize()
def test_unsatisfiable_architecture(self, set_dependency):
set_dependency("mpileaks", "mpich platform=test target=be")
spec = Spec(
"mpileaks ^mpich platform=test target=fe ^callpath" " ^dyninst ^libelf ^libdwarf"
)
with pytest.raises(spack.spec.UnsatisfiableArchitectureSpecError):
spec.normalize()
@pytest.mark.parametrize(
"spec_str", ["libelf ^mpich", "libelf ^libdwarf", "mpich ^dyninst ^libelf"]
)
def test_invalid_dep(self, spec_str):
spec = Spec(spec_str)
with pytest.raises(spack.error.SpecError):
spec.concretize()
def test_equal(self):
# Different spec structures to test for equality
flat = Spec.from_literal({"mpileaks ^callpath ^libelf ^libdwarf": None})
flat_init = Spec.from_literal(
{"mpileaks": {"callpath": None, "libdwarf": None, "libelf": None}}
)
flip_flat = Spec.from_literal(
{"mpileaks": {"libelf": None, "libdwarf": None, "callpath": None}}
)
dag = Spec.from_literal({"mpileaks": {"callpath": {"libdwarf": {"libelf": None}}}})
flip_dag = Spec.from_literal({"mpileaks": {"callpath": {"libelf": {"libdwarf": None}}}})
# All these are equal to each other with regular ==
specs = (flat, flat_init, flip_flat, dag, flip_dag)
for lhs, rhs in zip(specs, specs):
assert lhs == rhs
assert str(lhs) == str(rhs)
# Same DAGs constructed different ways are equal
assert flat.eq_dag(flat_init)
# order at same level does not matter -- (dep on same parent)
assert flat.eq_dag(flip_flat)
# DAGs should be unequal if nesting is different
assert not flat.eq_dag(dag)
assert not flat.eq_dag(flip_dag)
assert not flip_flat.eq_dag(dag)
assert not flip_flat.eq_dag(flip_dag)
assert not dag.eq_dag(flip_dag)
def test_normalize_mpileaks(self):
# Spec parsed in from a string
spec = Spec.from_literal(
{"mpileaks ^mpich ^callpath ^dyninst ^libelf@1.8.11 ^libdwarf": None}
)
# What that spec should look like after parsing
expected_flat = Spec.from_literal(
{
"mpileaks": {
"mpich": None,
"callpath": None,
"dyninst": None,
"libelf@1.8.11": None,
"libdwarf": None,
}
}
)
# What it should look like after normalization
mpich = Spec("mpich")
libelf = Spec("libelf@1.8.11")
expected_normalized = Spec.from_literal(
{
"mpileaks": {
"callpath": {
"dyninst": {"libdwarf": {libelf: None}, libelf: None},
mpich: None,
},
mpich: None,
}
}
)
# Similar to normalized spec, but now with copies of the same
# libelf node. Normalization should result in a single unique
# node for each package, so this is the wrong DAG.
non_unique_nodes = Spec.from_literal(
{
"mpileaks": {
"callpath": {
"dyninst": {"libdwarf": {"libelf@1.8.11": None}, "libelf@1.8.11": None},
mpich: None,
},
mpich: None,
}
},
normal=False,
)
# All specs here should be equal under regular equality
specs = (spec, expected_flat, expected_normalized, non_unique_nodes)
for lhs, rhs in zip(specs, specs):
assert lhs == rhs
assert str(lhs) == str(rhs)
# Test that equal and equal_dag are doing the right thing
assert spec == expected_flat
assert spec.eq_dag(expected_flat)
# Normalized has different DAG structure, so NOT equal.
assert spec != expected_normalized
assert not spec.eq_dag(expected_normalized)
# Again, different DAG structure so not equal.
assert spec != non_unique_nodes
assert not spec.eq_dag(non_unique_nodes)
spec.normalize()
# After normalizing, spec_dag_equal should match the normalized spec.
assert spec != expected_flat
assert not spec.eq_dag(expected_flat)
# verify DAG structure without deptypes.
assert spec.eq_dag(expected_normalized, deptypes=False)
assert not spec.eq_dag(non_unique_nodes, deptypes=False)
assert not spec.eq_dag(expected_normalized, deptypes=True)
assert not spec.eq_dag(non_unique_nodes, deptypes=True)
@pytest.mark.xfail(reason="String representation changed")
def test_normalize_with_virtual_package(self):
spec = Spec("mpileaks ^mpi ^libelf@1.8.11 ^libdwarf")
spec.normalize()
expected_normalized = Spec.from_literal(
{
"mpileaks": {
"callpath": {
"dyninst": {"libdwarf": {"libelf@1.8.11": None}, "libelf@1.8.11": None},
"mpi": None,
},
"mpi": None,
}
}
)
assert str(spec) == str(expected_normalized)
def test_contains(self):
spec = Spec("mpileaks ^mpi ^libelf@1.8.11 ^libdwarf")
assert Spec("mpi") in spec
assert Spec("libelf") in spec
assert Spec("libelf@1.8.11") in spec
assert Spec("libelf@1.8.12") not in spec
assert Spec("libdwarf") in spec
assert Spec("libgoblin") not in spec
assert Spec("mpileaks") in spec
def test_copy_simple(self):
orig = Spec("mpileaks")
copy = orig.copy()
check_links(copy)
assert orig == copy
assert orig.eq_dag(copy)
assert orig._normal == copy._normal
assert orig._concrete == copy._concrete
# ensure no shared nodes bt/w orig and copy.
orig_ids = set(id(s) for s in orig.traverse())
copy_ids = set(id(s) for s in copy.traverse())
assert not orig_ids.intersection(copy_ids)
def test_copy_normalized(self):
orig = Spec("mpileaks")
orig.normalize()
copy = orig.copy()
check_links(copy)
assert orig == copy
assert orig.eq_dag(copy)
# ensure no shared nodes bt/w orig and copy.
orig_ids = set(id(s) for s in orig.traverse())
copy_ids = set(id(s) for s in copy.traverse())
assert not orig_ids.intersection(copy_ids)
def test_copy_concretized(self):
orig = Spec("mpileaks")
orig.concretize()
copy = orig.copy()
check_links(copy)
assert orig == copy
assert orig.eq_dag(copy)
assert orig._normal == copy._normal
assert orig._concrete == copy._concrete
# ensure no shared nodes bt/w orig and copy.
orig_ids = set(id(s) for s in orig.traverse())
copy_ids = set(id(s) for s in copy.traverse())
assert not orig_ids.intersection(copy_ids)
def test_copy_through_spec_build_interface(self):
"""Check that copying dependencies using id(node) as a fast identifier of the
node works when the spec is wrapped in a SpecBuildInterface object.
"""
s = Spec("mpileaks").concretized()
c0 = s.copy()
assert c0 == s
# Single indirection
c1 = s["mpileaks"].copy()
assert c0 == c1 == s
# Double indirection
c2 = s["mpileaks"]["mpileaks"].copy()
assert c0 == c1 == c2 == s
"""
Here is the graph with deptypes labeled (assume all packages have a 'dt'
prefix). Arrows are marked with the deptypes ('b' for 'build', 'l' for
'link', 'r' for 'run').
use -bl-> top
top -b-> build1
top -bl-> link1
top -r-> run1
build1 -b-> build2
build1 -bl-> link2
build1 -r-> run2
link1 -bl-> link3
run1 -bl-> link5
run1 -r-> run3
link3 -b-> build2
link3 -bl-> link4
run3 -b-> build3
"""
def test_deptype_traversal(self):
dag = Spec("dtuse")
dag.normalize()
names = [
"dtuse",
"dttop",
"dtbuild1",
"dtbuild2",
"dtlink2",
"dtlink1",
"dtlink3",
"dtlink4",
]
traversal = dag.traverse(deptype=("build", "link"))
assert [x.name for x in traversal] == names
def test_deptype_traversal_with_builddeps(self):
dag = Spec("dttop")
dag.normalize()
names = ["dttop", "dtbuild1", "dtbuild2", "dtlink2", "dtlink1", "dtlink3", "dtlink4"]
traversal = dag.traverse(deptype=("build", "link"))
assert [x.name for x in traversal] == names
def test_deptype_traversal_full(self):
dag = Spec("dttop")
dag.normalize()
names = [
"dttop",
"dtbuild1",
"dtbuild2",
"dtlink2",
"dtrun2",
"dtlink1",
"dtlink3",
"dtlink4",
"dtrun1",
"dtlink5",
"dtrun3",
"dtbuild3",
]
traversal = dag.traverse(deptype=all)
assert [x.name for x in traversal] == names
def test_deptype_traversal_run(self):
dag = Spec("dttop")
dag.normalize()
names = ["dttop", "dtrun1", "dtrun3"]
traversal = dag.traverse(deptype="run")
assert [x.name for x in traversal] == names
def test_hash_bits(self):
"""Ensure getting first n bits of a base32-encoded DAG hash works."""
# RFC 4648 base32 decode table
b32 = dict((j, i) for i, j in enumerate("abcdefghijklmnopqrstuvwxyz"))
b32.update(dict((j, i) for i, j in enumerate("234567", 26)))
# some package hashes
tests = [
"35orsd4cenv743hg4i5vxha2lzayycby",
"6kfqtj7dap3773rxog6kkmoweix5gpwo",
"e6h6ff3uvmjbq3azik2ckr6ckwm3depv",
"snz2juf4ij7sv77cq3vs467q6acftmur",
"4eg47oedi5bbkhpoxw26v3oe6vamkfd7",
"vrwabwj6umeb5vjw6flx2rnft3j457rw",
]
for test_hash in tests:
# string containing raw bits of hash ('1' and '0')
expected = "".join([format(b32[c], "#07b").replace("0b", "") for c in test_hash])
for bits in (1, 2, 3, 4, 7, 8, 9, 16, 64, 117, 128, 160):
actual_int = hashutil.base32_prefix_bits(test_hash, bits)
fmt = "#0%sb" % (bits + 2)
actual = format(actual_int, fmt).replace("0b", "")
assert expected[:bits] == actual
with pytest.raises(ValueError):
hashutil.base32_prefix_bits(test_hash, 161)
with pytest.raises(ValueError):
hashutil.base32_prefix_bits(test_hash, 256)
def test_traversal_directions(self):
"""Make sure child and parent traversals of specs work."""
# Mock spec - d is used for a diamond dependency
spec = Spec.from_literal(
{"a": {"b": {"c": {"d": None}, "e": None}, "f": {"g": {"d": None}}}}
)
assert ["a", "b", "c", "d", "e", "f", "g"] == [
s.name for s in spec.traverse(direction="children")
]
assert ["g", "f", "a"] == [s.name for s in spec["g"].traverse(direction="parents")]
assert ["d", "c", "b", "a", "g", "f"] == [
s.name for s in spec["d"].traverse(direction="parents")
]
def test_edge_traversals(self):
"""Make sure child and parent traversals of specs work."""
# Mock spec - d is used for a diamond dependency
spec = Spec.from_literal(
{"a": {"b": {"c": {"d": None}, "e": None}, "f": {"g": {"d": None}}}}
)
assert ["a", "b", "c", "d", "e", "f", "g"] == [
s.name for s in spec.traverse(direction="children")
]
assert ["g", "f", "a"] == [s.name for s in spec["g"].traverse(direction="parents")]
assert ["d", "c", "b", "a", "g", "f"] == [
s.name for s in spec["d"].traverse(direction="parents")
]
def test_copy_dependencies(self):
s1 = Spec("mpileaks ^mpich2@1.1")
s2 = s1.copy()
assert "^mpich2@1.1" in s2
assert "^mpich2" in s2
def test_construct_spec_with_deptypes(self):
"""Ensure that it is possible to construct a spec with explicit
dependency types."""
s = Spec.from_literal(
{"a": {"b": {"c:build": None}, "d": {"e:build,link": {"f:run": None}}}}
)
assert s["b"].edges_to_dependencies(name="c")[0].depflag == dt.BUILD
assert s["d"].edges_to_dependencies(name="e")[0].depflag == dt.BUILD | dt.LINK
assert s["e"].edges_to_dependencies(name="f")[0].depflag == dt.RUN
assert s["c"].edges_from_dependents(name="b")[0].depflag == dt.BUILD
assert s["e"].edges_from_dependents(name="d")[0].depflag == dt.BUILD | dt.LINK
assert s["f"].edges_from_dependents(name="e")[0].depflag == dt.RUN
def check_diamond_deptypes(self, spec):
"""Validate deptypes in dt-diamond spec.
This ensures that concretization works properly when two packages
depend on the same dependency in different ways.
"""
assert (
spec["dt-diamond"].edges_to_dependencies(name="dt-diamond-left")[0].depflag
== dt.BUILD | dt.LINK
)
assert (
spec["dt-diamond"].edges_to_dependencies(name="dt-diamond-right")[0].depflag
== dt.BUILD | dt.LINK
)
assert (
spec["dt-diamond-left"].edges_to_dependencies(name="dt-diamond-bottom")[0].depflag
== dt.BUILD
)
assert (
spec["dt-diamond-right"].edges_to_dependencies(name="dt-diamond-bottom")[0].depflag
== dt.BUILD | dt.LINK | dt.RUN
)
def check_diamond_normalized_dag(self, spec):
dag = Spec.from_literal(
{
"dt-diamond": {
"dt-diamond-left:build,link": {"dt-diamond-bottom:build": None},
"dt-diamond-right:build,link": {"dt-diamond-bottom:build,link,run": None},
}
}
)
assert spec.eq_dag(dag)
def test_normalize_diamond_deptypes(self):
"""Ensure that dependency types are preserved even if the same thing is
depended on in two different ways."""
s = Spec("dt-diamond")
s.normalize()
self.check_diamond_deptypes(s)
self.check_diamond_normalized_dag(s)
def test_concretize_deptypes(self):
"""Ensure that dependency types are preserved after concretization."""
s = Spec("dt-diamond")
s.concretize()
self.check_diamond_deptypes(s)
def test_copy_deptypes(self):
"""Ensure that dependency types are preserved by spec copy."""
s1 = Spec("dt-diamond")
s1.normalize()
self.check_diamond_deptypes(s1)
self.check_diamond_normalized_dag(s1)
s2 = s1.copy()
self.check_diamond_normalized_dag(s2)
self.check_diamond_deptypes(s2)
s3 = Spec("dt-diamond")
s3.concretize()
self.check_diamond_deptypes(s3)
s4 = s3.copy()
self.check_diamond_deptypes(s4)
def test_getitem_query(self):
s = Spec("mpileaks")
s.concretize()
# Check a query to a non-virtual package
a = s["callpath"]
query = a.last_query
assert query.name == "callpath"
assert len(query.extra_parameters) == 0
assert not query.isvirtual
# Check a query to a virtual package
a = s["mpi"]
query = a.last_query
assert query.name == "mpi"
assert len(query.extra_parameters) == 0
assert query.isvirtual
# Check a query to a virtual package with
# extra parameters after query
a = s["mpi:cxx,fortran"]
query = a.last_query
assert query.name == "mpi"
assert len(query.extra_parameters) == 2
assert "cxx" in query.extra_parameters
assert "fortran" in query.extra_parameters
assert query.isvirtual
def test_getitem_exceptional_paths(self):
s = Spec("mpileaks")
s.concretize()
# Needed to get a proxy object
q = s["mpileaks"]
# Test that the attribute is read-only
with pytest.raises(AttributeError):
q.libs = "foo"
with pytest.raises(AttributeError):
q.libs
def test_canonical_deptype(self):
# special values
assert dt.canonicalize(all) == dt.ALL
assert dt.canonicalize("all") == dt.ALL
with pytest.raises(ValueError):
dt.canonicalize(None)
with pytest.raises(ValueError):
dt.canonicalize([None])
# everything in all_types is canonical
for v in dt.ALL_TYPES:
assert dt.canonicalize(v) == dt.flag_from_string(v)
# tuples
assert dt.canonicalize(("build",)) == dt.BUILD
assert dt.canonicalize(("build", "link", "run")) == dt.BUILD | dt.LINK | dt.RUN
assert dt.canonicalize(("build", "link")) == dt.BUILD | dt.LINK
assert dt.canonicalize(("build", "run")) == dt.BUILD | dt.RUN
# lists
assert dt.canonicalize(["build", "link", "run"]) == dt.BUILD | dt.LINK | dt.RUN
assert dt.canonicalize(["build", "link"]) == dt.BUILD | dt.LINK
assert dt.canonicalize(["build", "run"]) == dt.BUILD | dt.RUN
# sorting
assert dt.canonicalize(("run", "build", "link")) == dt.BUILD | dt.LINK | dt.RUN
assert dt.canonicalize(("run", "link", "build")) == dt.BUILD | dt.LINK | dt.RUN
assert dt.canonicalize(("run", "link")) == dt.LINK | dt.RUN
assert dt.canonicalize(("link", "build")) == dt.BUILD | dt.LINK
# deduplication
assert dt.canonicalize(("run", "run", "link")) == dt.RUN | dt.LINK
assert dt.canonicalize(("run", "link", "link")) == dt.RUN | dt.LINK
# can't put 'all' in tuple or list
with pytest.raises(ValueError):
dt.canonicalize(["all"])
with pytest.raises(ValueError):
dt.canonicalize(("all",))
# invalid values
with pytest.raises(ValueError):
dt.canonicalize("foo")
with pytest.raises(ValueError):
dt.canonicalize(("foo", "bar"))
with pytest.raises(ValueError):
dt.canonicalize(("foo",))
def test_invalid_literal_spec(self):
# Can't give type 'build' to a top-level spec
with pytest.raises(spack.parser.SpecSyntaxError):
Spec.from_literal({"foo:build": None})
# Can't use more than one ':' separator
with pytest.raises(KeyError):
Spec.from_literal({"foo": {"bar:build:link": None}})
def test_spec_tree_respect_deptypes(self):
# Version-test-root uses version-test-pkg as a build dependency
s = Spec("version-test-root").concretized()
out = s.tree(deptypes="all")
assert "version-test-pkg" in out
out = s.tree(deptypes=("link", "run"))
assert "version-test-pkg" not in out
def test_synthetic_construction_of_split_dependencies_from_same_package(mock_packages, config):
# Construct in a synthetic way (i.e. without using the solver)
# the following spec:
#
# b
# build / \ link,run
# c@2.0 c@1.0
#
# To demonstrate that a spec can now hold two direct
# dependencies from the same package
root = Spec("b").concretized()
link_run_spec = Spec("c@=1.0").concretized()
build_spec = Spec("c@=2.0").concretized()
root.add_dependency_edge(link_run_spec, depflag=dt.LINK, virtuals=())
root.add_dependency_edge(link_run_spec, depflag=dt.RUN, virtuals=())
root.add_dependency_edge(build_spec, depflag=dt.BUILD, virtuals=())
# Check dependencies from the perspective of root
assert len(root.dependencies()) == 2
assert all(x.name == "c" for x in root.dependencies())
assert "@2.0" in root.dependencies(name="c", deptype=dt.BUILD)[0]
assert "@1.0" in root.dependencies(name="c", deptype=dt.LINK | dt.RUN)[0]
# Check parent from the perspective of the dependencies
assert len(build_spec.dependents()) == 1
assert len(link_run_spec.dependents()) == 1
assert build_spec.dependents() == link_run_spec.dependents()
assert build_spec != link_run_spec
def test_synthetic_construction_bootstrapping(mock_packages, config):
# Construct the following spec:
#
# b@2.0
# | build
# b@1.0
#
root = Spec("b@=2.0").concretized()
bootstrap = Spec("b@=1.0").concretized()
root.add_dependency_edge(bootstrap, depflag=dt.BUILD, virtuals=())
assert len(root.dependencies()) == 1
assert root.dependencies()[0].name == "b"
assert root.name == "b"
def test_addition_of_different_deptypes_in_multiple_calls(mock_packages, config):
# Construct the following spec:
#
# b@2.0
# | build,link,run
# b@1.0
#
# with three calls and check we always have a single edge
root = Spec("b@=2.0").concretized()
bootstrap = Spec("b@=1.0").concretized()
for current_depflag in (dt.BUILD, dt.LINK, dt.RUN):
root.add_dependency_edge(bootstrap, depflag=current_depflag, virtuals=())
# Check edges in dependencies
assert len(root.edges_to_dependencies()) == 1
forward_edge = root.edges_to_dependencies(depflag=current_depflag)[0]
assert current_depflag & forward_edge.depflag
assert id(forward_edge.parent) == id(root)
assert id(forward_edge.spec) == id(bootstrap)
# Check edges from dependents
assert len(bootstrap.edges_from_dependents()) == 1
backward_edge = bootstrap.edges_from_dependents(depflag=current_depflag)[0]
assert current_depflag & backward_edge.depflag
assert id(backward_edge.parent) == id(root)
assert id(backward_edge.spec) == id(bootstrap)
@pytest.mark.parametrize(
"c1_depflag,c2_depflag",
[(dt.LINK, dt.BUILD | dt.LINK), (dt.LINK | dt.RUN, dt.BUILD | dt.LINK)],
)
def test_adding_same_deptype_with_the_same_name_raises(
mock_packages, config, c1_depflag, c2_depflag
):
p = Spec("b@=2.0").concretized()
c1 = Spec("b@=1.0").concretized()
c2 = Spec("b@=2.0").concretized()
p.add_dependency_edge(c1, depflag=c1_depflag, virtuals=())
with pytest.raises(spack.error.SpackError):
p.add_dependency_edge(c2, depflag=c2_depflag, virtuals=())
@pytest.mark.regression("33499")
def test_indexing_prefers_direct_or_transitive_link_deps():
# Test whether spec indexing prefers direct/transitive link type deps over deps of
# build/run/test deps, and whether it does fall back to a full dag search.
root = Spec("root")
# Use a and z to since we typically traverse by edges sorted alphabetically.
a1 = Spec("a1")
a2 = Spec("a2")
z1 = Spec("z1")
z2 = Spec("z2")
# Same package, different spec.
z3_flavor_1 = Spec("z3 +through_a1")
z3_flavor_2 = Spec("z3 +through_z1")
root.add_dependency_edge(a1, depflag=dt.BUILD | dt.RUN | dt.TEST, virtuals=())
# unique package as a dep of a build/run/test type dep.
a1.add_dependency_edge(a2, depflag=dt.ALL, virtuals=())
a1.add_dependency_edge(z3_flavor_1, depflag=dt.ALL, virtuals=())
# chain of link type deps root -> z1 -> z2 -> z3
root.add_dependency_edge(z1, depflag=dt.LINK, virtuals=())
z1.add_dependency_edge(z2, depflag=dt.LINK, virtuals=())
z2.add_dependency_edge(z3_flavor_2, depflag=dt.LINK, virtuals=())
# Indexing should prefer the link-type dep.
assert "through_z1" in root["z3"].variants
assert "through_a1" in a1["z3"].variants
# Ensure that the full DAG is still searched
assert root["a2"]