summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPeter Scheibel <scheibel1@llnl.gov>2023-12-19 16:37:44 -0800
committerGitHub <noreply@github.com>2023-12-19 16:37:44 -0800
commit5d50ad3941ebeee95d972baff265e31be22a380a (patch)
tree8b9bd1c15f21bcaf46c79b5862dc0031d4e500e1 /lib
parenta43156a86131a0b138f356a2721709a2070665ca (diff)
downloadspack-5d50ad3941ebeee95d972baff265e31be22a380a.tar.gz
spack-5d50ad3941ebeee95d972baff265e31be22a380a.tar.bz2
spack-5d50ad3941ebeee95d972baff265e31be22a380a.tar.xz
spack-5d50ad3941ebeee95d972baff265e31be22a380a.zip
"spack diff": add ignore option for dependencies (#41711)
* add trim function to `Spec` and `--ignore` option to 'spack diff' Allows user to compare two specs while ignoring the sub-DAG of a particular dependency, e.g. spack diff --ignore=mpi --ignore=zlib trilinos/abcdef trilinos/fedcba to focus on differences closer to the root of the software stack
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/cmd/diff.py15
-rw-r--r--lib/spack/spack/spec.py14
-rw-r--r--lib/spack/spack/test/cmd/diff.py138
-rw-r--r--lib/spack/spack/test/concretize_requirements.py30
-rw-r--r--lib/spack/spack/test/conftest.py21
-rw-r--r--lib/spack/spack/test/spec_semantics.py11
6 files changed, 204 insertions, 25 deletions
diff --git a/lib/spack/spack/cmd/diff.py b/lib/spack/spack/cmd/diff.py
index f6a5c5ce7a..0e20d7c30b 100644
--- a/lib/spack/spack/cmd/diff.py
+++ b/lib/spack/spack/cmd/diff.py
@@ -44,6 +44,9 @@ def setup_parser(subparser):
action="append",
help="select the attributes to show (defaults to all)",
)
+ subparser.add_argument(
+ "--ignore", action="append", help="omit diffs related to these dependencies"
+ )
def shift(asp_function):
@@ -54,7 +57,7 @@ def shift(asp_function):
return asp.AspFunction(first, rest)
-def compare_specs(a, b, to_string=False, color=None):
+def compare_specs(a, b, to_string=False, color=None, ignore_packages=None):
"""
Generate a comparison, including diffs (for each side) and an intersection.
@@ -73,6 +76,14 @@ def compare_specs(a, b, to_string=False, color=None):
if color is None:
color = get_color_when()
+ a = a.copy()
+ b = b.copy()
+
+ if ignore_packages:
+ for pkg_name in ignore_packages:
+ a.trim(pkg_name)
+ b.trim(pkg_name)
+
# Prepare a solver setup to parse differences
setup = asp.SpackSolverSetup()
@@ -209,7 +220,7 @@ def diff(parser, args):
# Calculate the comparison (c)
color = False if args.dump_json else get_color_when()
- c = compare_specs(specs[0], specs[1], to_string=True, color=color)
+ c = compare_specs(specs[0], specs[1], to_string=True, color=color, ignore_packages=args.ignore)
# Default to all attributes
attributes = args.attribute or ["all"]
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 600ce988d7..270cf5eaf2 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -4728,6 +4728,20 @@ class Spec:
def build_spec(self, value):
self._build_spec = value
+ def trim(self, dep_name):
+ """
+ Remove any package that is or provides `dep_name` transitively
+ from this tree. This can also remove other dependencies if
+ they are only present because of `dep_name`.
+ """
+ for spec in list(self.traverse()):
+ new_dependencies = _EdgeMap() # A new _EdgeMap
+ for pkg_name, edge_list in spec._dependencies.items():
+ for edge in edge_list:
+ if (dep_name not in edge.virtuals) and (not dep_name == edge.spec.name):
+ new_dependencies.add(edge)
+ spec._dependencies = new_dependencies
+
def splice(self, other, transitive):
"""Splices dependency "other" into this ("target") Spec, and return the
result as a concrete Spec.
diff --git a/lib/spack/spack/test/cmd/diff.py b/lib/spack/spack/test/cmd/diff.py
index b90c61fbc8..9a901b9cbc 100644
--- a/lib/spack/spack/test/cmd/diff.py
+++ b/lib/spack/spack/test/cmd/diff.py
@@ -10,12 +10,150 @@ import spack.config
import spack.main
import spack.store
import spack.util.spack_json as sjson
+from spack.test.conftest import create_test_repo
install_cmd = spack.main.SpackCommand("install")
diff_cmd = spack.main.SpackCommand("diff")
find_cmd = spack.main.SpackCommand("find")
+_p1 = (
+ "p1",
+ """\
+class P1(Package):
+ version("1.0")
+
+ variant("p1var", default=True)
+ variant("usev1", default=True)
+
+ depends_on("p2")
+ depends_on("v1", when="+usev1")
+""",
+)
+
+
+_p2 = (
+ "p2",
+ """\
+class P2(Package):
+ version("1.0")
+
+ variant("p2var", default=True)
+
+ depends_on("p3")
+""",
+)
+
+
+_p3 = (
+ "p3",
+ """\
+class P3(Package):
+ version("1.0")
+
+ variant("p3var", default=True)
+""",
+)
+
+_i1 = (
+ "i1",
+ """\
+class I1(Package):
+ version("1.0")
+
+ provides("v1")
+
+ variant("i1var", default=True)
+
+ depends_on("p3")
+ depends_on("p4")
+""",
+)
+
+_i2 = (
+ "i2",
+ """\
+class I2(Package):
+ version("1.0")
+
+ provides("v1")
+
+ variant("i2var", default=True)
+
+ depends_on("p3")
+ depends_on("p4")
+""",
+)
+
+
+_p4 = (
+ "p4",
+ """\
+class P4(Package):
+ version("1.0")
+
+ variant("p4var", default=True)
+""",
+)
+
+
+# Note that the hash of p1 will differ depending on the variant chosen
+# we probably always want to omit that from diffs
+@pytest.fixture
+def _create_test_repo(tmpdir, mutable_config):
+ """
+ p1____
+ | \
+ p2 v1
+ | ____/ |
+ p3 p4
+
+ i1 and i2 provide v1 (and both have the same dependencies)
+
+ All packages have an associated variant
+ """
+ yield create_test_repo(tmpdir, [_p1, _p2, _p3, _i1, _i2, _p4])
+
+
+@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_diff_ignore(test_repo):
+ specA = spack.spec.Spec("p1+usev1").concretized()
+ specB = spack.spec.Spec("p1~usev1").concretized()
+
+ c1 = spack.cmd.diff.compare_specs(specA, specB, to_string=False)
+
+ def match(function, name, args):
+ limit = len(args)
+ return function.name == name and list(args[:limit]) == list(function.args[:limit])
+
+ def find(function_list, name, args):
+ return any(match(f, name, args) for f in function_list)
+
+ assert find(c1["a_not_b"], "node_os", ["p4"])
+
+ c2 = spack.cmd.diff.compare_specs(specA, specB, ignore_packages=["v1"], to_string=False)
+
+ assert not find(c2["a_not_b"], "node_os", ["p4"])
+ assert find(c2["intersect"], "node_os", ["p3"])
+
+ # Check ignoring changes on multiple packages
+
+ specA = spack.spec.Spec("p1+usev1 ^p3+p3var").concretized()
+ specA = spack.spec.Spec("p1~usev1 ^p3~p3var").concretized()
+
+ c3 = spack.cmd.diff.compare_specs(specA, specB, to_string=False)
+ assert find(c3["a_not_b"], "variant_value", ["p3", "p3var"])
+
+ c4 = spack.cmd.diff.compare_specs(specA, specB, ignore_packages=["v1", "p3"], to_string=False)
+ assert not find(c4["a_not_b"], "node_os", ["p4"])
+ assert not find(c4["a_not_b"], "variant_value", ["p3"])
+
+
def test_diff_cmd(install_mockery, mock_fetch, mock_archive, mock_packages):
"""Test that we can install two packages and diff them"""
diff --git a/lib/spack/spack/test/concretize_requirements.py b/lib/spack/spack/test/concretize_requirements.py
index 529d481b2f..0932793b6f 100644
--- a/lib/spack/spack/test/concretize_requirements.py
+++ b/lib/spack/spack/test/concretize_requirements.py
@@ -16,6 +16,7 @@ import spack.util.spack_yaml as syaml
import spack.version
from spack.solver.asp import InternalConcretizerError, UnsatisfiableSpecError
from spack.spec import Spec
+from spack.test.conftest import create_test_repo
from spack.util.url import path_to_file_url
pytestmark = [
@@ -92,30 +93,13 @@ class U(Package):
@pytest.fixture
-def create_test_repo(tmpdir, mutable_config):
- repo_path = str(tmpdir)
- repo_yaml = tmpdir.join("repo.yaml")
- with open(str(repo_yaml), "w") as f:
- f.write(
- """\
-repo:
- namespace: testcfgrequirements
-"""
- )
-
- packages_dir = tmpdir.join("packages")
- for pkg_name, pkg_str in [_pkgx, _pkgy, _pkgv, _pkgt, _pkgu]:
- pkg_dir = packages_dir.ensure(pkg_name, dir=True)
- pkg_file = pkg_dir.join("package.py")
- with open(str(pkg_file), "w") as f:
- f.write(pkg_str)
-
- yield spack.repo.Repo(repo_path)
+def _create_test_repo(tmpdir, mutable_config):
+ yield create_test_repo(tmpdir, [_pkgx, _pkgy, _pkgv, _pkgt, _pkgu])
@pytest.fixture
-def test_repo(create_test_repo, monkeypatch, mock_stage):
- with spack.repo.use_repositories(create_test_repo) as mock_repo_path:
+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
@@ -530,7 +514,7 @@ packages:
assert s2.satisfies("@2.5")
-def test_reuse_oneof(concretize_scope, create_test_repo, mutable_database, fake_installs):
+def test_reuse_oneof(concretize_scope, _create_test_repo, mutable_database, fake_installs):
conf_str = """\
packages:
y:
@@ -538,7 +522,7 @@ packages:
- one_of: ["@2.5", "%gcc"]
"""
- with spack.repo.use_repositories(create_test_repo):
+ with spack.repo.use_repositories(_create_test_repo):
s1 = Spec("y@2.5%gcc").concretized()
s1.package.do_install(fake=True, explicit=True)
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
index c832c4d1d5..9d3ef7652d 100644
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -1976,3 +1976,24 @@ def mock_modules_root(tmp_path, monkeypatch):
"""Sets the modules root to a temporary directory, to avoid polluting configuration scopes."""
fn = functools.partial(_root_path, path=str(tmp_path))
monkeypatch.setattr(spack.modules.common, "root_path", fn)
+
+
+def create_test_repo(tmpdir, pkg_name_content_tuples):
+ repo_path = str(tmpdir)
+ repo_yaml = tmpdir.join("repo.yaml")
+ with open(str(repo_yaml), "w") as f:
+ f.write(
+ """\
+repo:
+ namespace: testcfgrequirements
+"""
+ )
+
+ packages_dir = tmpdir.join("packages")
+ for pkg_name, pkg_str in pkg_name_content_tuples:
+ pkg_dir = packages_dir.ensure(pkg_name, dir=True)
+ pkg_file = pkg_dir.join("package.py")
+ with open(str(pkg_file), "w") as f:
+ f.write(pkg_str)
+
+ return spack.repo.Repo(repo_path)
diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py
index 87ed1e4b3f..97855ed5bb 100644
--- a/lib/spack/spack/test/spec_semantics.py
+++ b/lib/spack/spack/test/spec_semantics.py
@@ -1288,6 +1288,17 @@ def test_call_dag_hash_on_old_dag_hash_spec(mock_packages, default_mock_concreti
spec.package_hash()
+def test_spec_trim(mock_packages, config):
+ top = Spec("dt-diamond").concretized()
+ top.trim("dt-diamond-left")
+ remaining = set(x.name for x in top.traverse())
+ assert set(["dt-diamond", "dt-diamond-right", "dt-diamond-bottom"]) == remaining
+
+ top.trim("dt-diamond-right")
+ remaining = set(x.name for x in top.traverse())
+ assert set(["dt-diamond"]) == remaining
+
+
@pytest.mark.regression("30861")
def test_concretize_partial_old_dag_hash_spec(mock_packages, config):
# create an "old" spec with no package hash