summaryrefslogtreecommitdiff
path: root/lib/spack/spack/test/spec_syntax.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/spack/test/spec_syntax.py')
-rw-r--r--lib/spack/spack/test/spec_syntax.py1849
1 files changed, 950 insertions, 899 deletions
diff --git a/lib/spack/spack/test/spec_syntax.py b/lib/spack/spack/test/spec_syntax.py
index cf1ce971d0..97c1a9a3ce 100644
--- a/lib/spack/spack/test/spec_syntax.py
+++ b/lib/spack/spack/test/spec_syntax.py
@@ -3,928 +3,979 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import itertools
-import os
-import shlex
import pytest
-import llnl.util.filesystem as fs
-
-import spack.hash_types as ht
-import spack.repo
-import spack.spec as sp
-import spack.store
-from spack.parse import Token
-from spack.spec import (
- AmbiguousHashError,
- DuplicateArchitectureError,
- DuplicateCompilerSpecError,
- DuplicateDependencyError,
- InvalidHashError,
- MultipleVersionError,
- NoSuchHashError,
- NoSuchSpecFileError,
- RedundantSpecError,
- Spec,
- SpecFilenameError,
- SpecParseError,
-)
-from spack.variant import DuplicateVariantError
-
-# Building blocks for complex lexing.
-complex_root = [
- Token(sp.ID, "mvapich_foo"),
-]
-
-kv_root = [
- Token(sp.ID, "mvapich_foo"),
- Token(sp.ID, "debug"),
- Token(sp.EQ),
- Token(sp.VAL, "4"),
-]
-
-complex_compiler = [
- Token(sp.PCT),
- Token(sp.ID, "intel"),
-]
-
-complex_compiler_v = [
- Token(sp.VER, "@12.1"),
- Token(sp.COLON),
- Token(sp.ID, "12.6"),
-]
-
-complex_compiler_v_space = [
- Token(sp.VER, "@"),
- Token(sp.ID, "12.1"),
- Token(sp.COLON),
- Token(sp.ID, "12.6"),
-]
-
-complex_dep1 = [
- Token(sp.DEP),
- Token(sp.ID, "_openmpi"),
- Token(sp.VER, "@1.2"),
- Token(sp.COLON),
- Token(sp.ID, "1.4"),
- Token(sp.COMMA),
- Token(sp.ID, "1.6"),
-]
-
-complex_dep1_space = [
- Token(sp.DEP),
- Token(sp.ID, "_openmpi"),
- Token(sp.VER, "@"),
- Token(sp.ID, "1.2"),
- Token(sp.COLON),
- Token(sp.ID, "1.4"),
- Token(sp.COMMA),
- Token(sp.ID, "1.6"),
-]
-
-complex_dep1_var = [
- Token(sp.ON),
- Token(sp.ID, "debug"),
- Token(sp.OFF),
- Token(sp.ID, "qt_4"),
-]
-
-complex_dep2 = [
- Token(sp.DEP),
- Token(sp.ID, "stackwalker"),
- Token(sp.VER, "@8.1_1e"),
-]
-
-complex_dep2_space = [
- Token(sp.DEP),
- Token(sp.ID, "stackwalker"),
- Token(sp.VER, "@"),
- Token(sp.ID, "8.1_1e"),
-]
-
-# Sample output from complex lexing
-complex_lex = (
- complex_root
- + complex_dep1
- + complex_compiler
- + complex_compiler_v
- + complex_dep1_var
- + complex_dep2
-)
+import spack.platforms.test
+import spack.spec
+import spack.variant
+from spack.parser import SpecParser, SpecTokenizationError, Token, TokenType
-# Another sample lexer output with a kv pair.
-kv_lex = (
- kv_root
- + complex_dep1
- + complex_compiler
- + complex_compiler_v_space
- + complex_dep1_var
- + complex_dep2_space
-)
+def simple_package_name(name):
+ """A simple package name in canonical form"""
+ return name, [Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value=name)], name
-class TestSpecSyntax(object):
- # ========================================================================
- # Parse checks
- # ========================================================================
- def check_parse(self, expected, spec=None):
- """Assert that the provided spec is able to be parsed.
+def dependency_with_version(text):
+ root, rest = text.split("^")
+ dependency, version = rest.split("@")
+ return (
+ text,
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value=root.strip()),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value=dependency.strip()),
+ Token(TokenType.VERSION, value=f"@{version}"),
+ ],
+ text,
+ )
- If this is called with one argument, it assumes that the
- string is canonical (i.e., no spaces and ~ instead of - for
- variants) and that it will convert back to the string it came
- from.
- If this is called with two arguments, the first argument is
- the expected canonical form and the second is a non-canonical
- input to be parsed.
+def compiler_with_version_range(text):
+ return text, [Token(TokenType.COMPILER_AND_VERSION, value=text)], text
- """
- if spec is None:
- spec = expected
- output = sp.parse(spec)
- parsed = " ".join(str(spec) for spec in output)
- assert expected == parsed
+@pytest.fixture()
+def specfile_for(default_mock_concretization):
+ def _specfile_for(spec_str, filename):
+ s = default_mock_concretization(spec_str)
+ is_json = str(filename).endswith(".json")
+ is_yaml = str(filename).endswith(".yaml")
+ if not is_json and not is_yaml:
+ raise ValueError("wrong extension used for specfile")
- def check_lex(self, tokens, spec):
- """Check that the provided spec parses to the provided token list."""
- spec = shlex.split(str(spec))
- lex_output = sp.SpecLexer().lex(spec)
- assert len(tokens) == len(lex_output), "unexpected number of tokens"
- for tok, spec_tok in zip(tokens, lex_output):
- if tok.type in (sp.ID, sp.VAL, sp.VER):
- assert tok == spec_tok
+ with filename.open("w") as f:
+ if is_json:
+ f.write(s.to_json())
else:
- # Only check the type for non-identifiers.
- assert tok.type == spec_tok.type
-
- def _check_raises(self, exc_type, items):
- for item in items:
- with pytest.raises(exc_type):
- Spec(item)
-
- # ========================================================================
- # Parse checks
- # ========================================================================
- def test_package_names(self):
- self.check_parse("mvapich")
- self.check_parse("mvapich_foo")
- self.check_parse("_mvapich_foo")
-
- def test_anonymous_specs(self):
- self.check_parse("%intel")
- self.check_parse("@2.7")
- self.check_parse("^zlib")
- self.check_parse("+foo")
- self.check_parse("arch=test-None-None", "platform=test")
- self.check_parse("@2.7:")
-
- def test_anonymous_specs_with_multiple_parts(self):
- # Parse anonymous spec with multiple tokens
- self.check_parse("@4.2: languages=go", "languages=go @4.2:")
- self.check_parse("@4.2: languages=go")
-
- def test_simple_dependence(self):
- self.check_parse("openmpi ^hwloc")
- self.check_parse("openmpi ^hwloc", "openmpi^hwloc")
-
- self.check_parse("openmpi ^hwloc ^libunwind")
- self.check_parse("openmpi ^hwloc ^libunwind", "openmpi^hwloc^libunwind")
-
- def test_version_after_compiler(self):
- self.check_parse("foo@2.0%bar@1.0", "foo %bar@1.0 @2.0")
-
- def test_dependencies_with_versions(self):
- self.check_parse("openmpi ^hwloc@1.2e6")
- self.check_parse("openmpi ^hwloc@1.2e6:")
- self.check_parse("openmpi ^hwloc@:1.4b7-rc3")
- self.check_parse("openmpi ^hwloc@1.2e6:1.4b7-rc3")
-
- def test_multiple_specs(self):
- self.check_parse("mvapich emacs")
-
- def test_multiple_specs_after_kv(self):
- self.check_parse('mvapich cppflags="-O3 -fPIC" emacs')
- self.check_parse('mvapich cflags="-O3" emacs', "mvapich cflags=-O3 emacs")
-
- def test_multiple_specs_long_second(self):
- self.check_parse(
- 'mvapich emacs@1.1.1%intel cflags="-O3"', "mvapich emacs @1.1.1 %intel cflags=-O3"
- )
- self.check_parse('mvapich cflags="-O3 -fPIC" emacs ^ncurses%intel')
- self.check_parse(
- 'mvapich cflags="-O3 -fPIC" emacs ^ncurses%intel',
+ f.write(s.to_yaml())
+ return s
+
+ return _specfile_for
+
+
+@pytest.mark.parametrize(
+ "spec_str,tokens,expected_roundtrip",
+ [
+ # Package names
+ simple_package_name("mvapich"),
+ simple_package_name("mvapich_foo"),
+ simple_package_name("_mvapich_foo"),
+ simple_package_name("3dtk"),
+ simple_package_name("ns-3-dev"),
+ # Single token anonymous specs
+ ("%intel", [Token(TokenType.COMPILER, value="%intel")], "%intel"),
+ ("@2.7", [Token(TokenType.VERSION, value="@2.7")], "@2.7"),
+ ("@2.7:", [Token(TokenType.VERSION, value="@2.7:")], "@2.7:"),
+ ("@:2.7", [Token(TokenType.VERSION, value="@:2.7")], "@:2.7"),
+ ("+foo", [Token(TokenType.BOOL_VARIANT, value="+foo")], "+foo"),
+ ("~foo", [Token(TokenType.BOOL_VARIANT, value="~foo")], "~foo"),
+ ("-foo", [Token(TokenType.BOOL_VARIANT, value="-foo")], "~foo"),
+ (
+ "platform=test",
+ [Token(TokenType.KEY_VALUE_PAIR, value="platform=test")],
+ "arch=test-None-None",
+ ),
+ # Multiple tokens anonymous specs
+ (
+ "languages=go @4.2:",
+ [
+ Token(TokenType.KEY_VALUE_PAIR, value="languages=go"),
+ Token(TokenType.VERSION, value="@4.2:"),
+ ],
+ "@4.2: languages=go",
+ ),
+ (
+ "@4.2: languages=go",
+ [
+ Token(TokenType.VERSION, value="@4.2:"),
+ Token(TokenType.KEY_VALUE_PAIR, value="languages=go"),
+ ],
+ "@4.2: languages=go",
+ ),
+ (
+ "^zlib",
+ [
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="zlib"),
+ ],
+ "^zlib",
+ ),
+ # Specs with simple dependencies
+ (
+ "openmpi ^hwloc",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="openmpi"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="hwloc"),
+ ],
+ "openmpi ^hwloc",
+ ),
+ (
+ "openmpi ^hwloc ^libunwind",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="openmpi"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="hwloc"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="libunwind"),
+ ],
+ "openmpi ^hwloc ^libunwind",
+ ),
+ (
+ "openmpi ^hwloc^libunwind",
+ [ # White spaces are tested
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="openmpi"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="hwloc"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="libunwind"),
+ ],
+ "openmpi ^hwloc ^libunwind",
+ ),
+ # Version after compiler
+ (
+ "foo %bar@1.0 @2.0",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="foo"),
+ Token(TokenType.COMPILER_AND_VERSION, value="%bar@1.0"),
+ Token(TokenType.VERSION, value="@2.0"),
+ ],
+ "foo@2.0%bar@1.0",
+ ),
+ # Single dependency with version
+ dependency_with_version("openmpi ^hwloc@1.2e6"),
+ dependency_with_version("openmpi ^hwloc@1.2e6:"),
+ dependency_with_version("openmpi ^hwloc@:1.4b7-rc3"),
+ dependency_with_version("openmpi ^hwloc@1.2e6:1.4b7-rc3"),
+ # Complex specs with multiple constraints
+ (
+ "mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1+debug~qt_4 ^stackwalker@8.1_1e",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich_foo"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="_openmpi"),
+ Token(TokenType.VERSION, value="@1.2:1.4,1.6"),
+ Token(TokenType.COMPILER_AND_VERSION, value="%intel@12.1"),
+ Token(TokenType.BOOL_VARIANT, value="+debug"),
+ Token(TokenType.BOOL_VARIANT, value="~qt_4"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="stackwalker"),
+ Token(TokenType.VERSION, value="@8.1_1e"),
+ ],
+ "mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1+debug~qt_4 ^stackwalker@8.1_1e",
+ ),
+ (
+ "mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1~qt_4 debug=2 ^stackwalker@8.1_1e",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich_foo"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="_openmpi"),
+ Token(TokenType.VERSION, value="@1.2:1.4,1.6"),
+ Token(TokenType.COMPILER_AND_VERSION, value="%intel@12.1"),
+ Token(TokenType.BOOL_VARIANT, value="~qt_4"),
+ Token(TokenType.KEY_VALUE_PAIR, value="debug=2"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="stackwalker"),
+ Token(TokenType.VERSION, value="@8.1_1e"),
+ ],
+ "mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1~qt_4 debug=2 ^stackwalker@8.1_1e",
+ ),
+ (
+ "mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1 cppflags=-O3 +debug~qt_4 ^stackwalker@8.1_1e", # noqa: E501
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich_foo"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="_openmpi"),
+ Token(TokenType.VERSION, value="@1.2:1.4,1.6"),
+ Token(TokenType.COMPILER_AND_VERSION, value="%intel@12.1"),
+ Token(TokenType.KEY_VALUE_PAIR, value="cppflags=-O3"),
+ Token(TokenType.BOOL_VARIANT, value="+debug"),
+ Token(TokenType.BOOL_VARIANT, value="~qt_4"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="stackwalker"),
+ Token(TokenType.VERSION, value="@8.1_1e"),
+ ],
+ 'mvapich_foo ^_openmpi@1.2:1.4,1.6%intel@12.1 cppflags="-O3" +debug~qt_4 ^stackwalker@8.1_1e', # noqa: E501
+ ),
+ # Specs containing YAML or JSON in the package name
+ (
+ "yaml-cpp@0.1.8%intel@12.1 ^boost@3.1.4",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="yaml-cpp"),
+ Token(TokenType.VERSION, value="@0.1.8"),
+ Token(TokenType.COMPILER_AND_VERSION, value="%intel@12.1"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="boost"),
+ Token(TokenType.VERSION, value="@3.1.4"),
+ ],
+ "yaml-cpp@0.1.8%intel@12.1 ^boost@3.1.4",
+ ),
+ (
+ r"builtin.yaml-cpp%gcc",
+ [
+ Token(TokenType.FULLY_QUALIFIED_PACKAGE_NAME, value="builtin.yaml-cpp"),
+ Token(TokenType.COMPILER, value="%gcc"),
+ ],
+ "yaml-cpp%gcc",
+ ),
+ (
+ r"testrepo.yaml-cpp%gcc",
+ [
+ Token(TokenType.FULLY_QUALIFIED_PACKAGE_NAME, value="testrepo.yaml-cpp"),
+ Token(TokenType.COMPILER, value="%gcc"),
+ ],
+ "yaml-cpp%gcc",
+ ),
+ (
+ r"builtin.yaml-cpp@0.1.8%gcc@7.2.0 ^boost@3.1.4",
+ [
+ Token(TokenType.FULLY_QUALIFIED_PACKAGE_NAME, value="builtin.yaml-cpp"),
+ Token(TokenType.VERSION, value="@0.1.8"),
+ Token(TokenType.COMPILER_AND_VERSION, value="%gcc@7.2.0"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="boost"),
+ Token(TokenType.VERSION, value="@3.1.4"),
+ ],
+ "yaml-cpp@0.1.8%gcc@7.2.0 ^boost@3.1.4",
+ ),
+ (
+ r"builtin.yaml-cpp ^testrepo.boost ^zlib",
+ [
+ Token(TokenType.FULLY_QUALIFIED_PACKAGE_NAME, value="builtin.yaml-cpp"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.FULLY_QUALIFIED_PACKAGE_NAME, value="testrepo.boost"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="zlib"),
+ ],
+ "yaml-cpp ^boost ^zlib",
+ ),
+ # Canonicalization of the string representation
+ (
+ r"mvapich ^stackwalker ^_openmpi", # Dependencies are reordered
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="stackwalker"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="_openmpi"),
+ ],
+ "mvapich ^_openmpi ^stackwalker",
+ ),
+ (
+ r"y~f+e~d+c~b+a", # Variants are reordered
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="y"),
+ Token(TokenType.BOOL_VARIANT, value="~f"),
+ Token(TokenType.BOOL_VARIANT, value="+e"),
+ Token(TokenType.BOOL_VARIANT, value="~d"),
+ Token(TokenType.BOOL_VARIANT, value="+c"),
+ Token(TokenType.BOOL_VARIANT, value="~b"),
+ Token(TokenType.BOOL_VARIANT, value="+a"),
+ ],
+ "y+a~b+c~d+e~f",
+ ),
+ ("@:", [Token(TokenType.VERSION, value="@:")], r""),
+ ("@1.6,1.2:1.4", [Token(TokenType.VERSION, value="@1.6,1.2:1.4")], r"@1.2:1.4,1.6"),
+ (
+ r"os=fe", # Various translations associated with the architecture
+ [Token(TokenType.KEY_VALUE_PAIR, value="os=fe")],
+ "arch=test-redhat6-None",
+ ),
+ (
+ r"os=default_os",
+ [Token(TokenType.KEY_VALUE_PAIR, value="os=default_os")],
+ "arch=test-debian6-None",
+ ),
+ (
+ r"target=be",
+ [Token(TokenType.KEY_VALUE_PAIR, value="target=be")],
+ f"arch=test-None-{spack.platforms.test.Test.default}",
+ ),
+ (
+ r"target=default_target",
+ [Token(TokenType.KEY_VALUE_PAIR, value="target=default_target")],
+ f"arch=test-None-{spack.platforms.test.Test.default}",
+ ),
+ (
+ r"platform=linux",
+ [Token(TokenType.KEY_VALUE_PAIR, value="platform=linux")],
+ r"arch=linux-None-None",
+ ),
+ # Version hash pair
+ (
+ rf"develop-branch-version@{'abc12'*8}=develop",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="develop-branch-version"),
+ Token(TokenType.VERSION_HASH_PAIR, value=f"@{'abc12'*8}=develop"),
+ ],
+ rf"develop-branch-version@{'abc12'*8}=develop",
+ ),
+ # Redundant specs
+ (
+ r"x ^y@foo ^y@foo",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="x"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="y"),
+ Token(TokenType.VERSION, value="@foo"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="y"),
+ Token(TokenType.VERSION, value="@foo"),
+ ],
+ r"x ^y@foo",
+ ),
+ (
+ r"x ^y@foo ^y+bar",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="x"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="y"),
+ Token(TokenType.VERSION, value="@foo"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="y"),
+ Token(TokenType.BOOL_VARIANT, value="+bar"),
+ ],
+ r"x ^y@foo+bar",
+ ),
+ (
+ r"x ^y@foo +bar ^y@foo",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="x"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="y"),
+ Token(TokenType.VERSION, value="@foo"),
+ Token(TokenType.BOOL_VARIANT, value="+bar"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="y"),
+ Token(TokenType.VERSION, value="@foo"),
+ ],
+ r"x ^y@foo+bar",
+ ),
+ # Ambiguous variant specification
+ (
+ r"_openmpi +debug-qt_4", # Parse as a single bool variant
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="_openmpi"),
+ Token(TokenType.BOOL_VARIANT, value="+debug-qt_4"),
+ ],
+ r"_openmpi+debug-qt_4",
+ ),
+ (
+ r"_openmpi +debug -qt_4", # Parse as two variants
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="_openmpi"),
+ Token(TokenType.BOOL_VARIANT, value="+debug"),
+ Token(TokenType.BOOL_VARIANT, value="-qt_4"),
+ ],
+ r"_openmpi+debug~qt_4",
+ ),
+ (
+ r"_openmpi +debug~qt_4", # Parse as two variants
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="_openmpi"),
+ Token(TokenType.BOOL_VARIANT, value="+debug"),
+ Token(TokenType.BOOL_VARIANT, value="~qt_4"),
+ ],
+ r"_openmpi+debug~qt_4",
+ ),
+ # Key value pairs with ":" and "," in the value
+ (
+ r"target=:broadwell,icelake",
+ [
+ Token(TokenType.KEY_VALUE_PAIR, value="target=:broadwell,icelake"),
+ ],
+ r"arch=None-None-:broadwell,icelake",
+ ),
+ # Hash pair version followed by a variant
+ (
+ f"develop-branch-version@git.{'a' * 40}=develop+var1+var2",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="develop-branch-version"),
+ Token(TokenType.VERSION_HASH_PAIR, value=f"@git.{'a' * 40}=develop"),
+ Token(TokenType.BOOL_VARIANT, value="+var1"),
+ Token(TokenType.BOOL_VARIANT, value="+var2"),
+ ],
+ f"develop-branch-version@git.{'a' * 40}=develop+var1+var2",
+ ),
+ # Compiler with version ranges
+ compiler_with_version_range("%gcc@10.2.1:"),
+ compiler_with_version_range("%gcc@:10.2.1"),
+ compiler_with_version_range("%gcc@10.2.1:12.1.0"),
+ compiler_with_version_range("%gcc@10.1.0,12.2.1:"),
+ compiler_with_version_range("%gcc@:8.4.3,10.2.1:12.1.0"),
+ # Special key value arguments
+ ("dev_path=*", [Token(TokenType.KEY_VALUE_PAIR, value="dev_path=*")], "dev_path=*"),
+ (
+ "dev_path=none",
+ [Token(TokenType.KEY_VALUE_PAIR, value="dev_path=none")],
+ "dev_path=none",
+ ),
+ (
+ "dev_path=../relpath/work",
+ [Token(TokenType.KEY_VALUE_PAIR, value="dev_path=../relpath/work")],
+ "dev_path=../relpath/work",
+ ),
+ (
+ "dev_path=/abspath/work",
+ [Token(TokenType.KEY_VALUE_PAIR, value="dev_path=/abspath/work")],
+ "dev_path=/abspath/work",
+ ),
+ # One liner for flags like 'a=b=c' that are injected
+ (
+ "cflags=a=b=c",
+ [Token(TokenType.KEY_VALUE_PAIR, value="cflags=a=b=c")],
+ 'cflags="a=b=c"',
+ ),
+ (
+ "cflags=a=b=c",
+ [Token(TokenType.KEY_VALUE_PAIR, value="cflags=a=b=c")],
+ 'cflags="a=b=c"',
+ ),
+ (
+ "cflags=a=b=c+~",
+ [Token(TokenType.KEY_VALUE_PAIR, value="cflags=a=b=c+~")],
+ 'cflags="a=b=c+~"',
+ ),
+ (
+ "cflags=-Wl,a,b,c",
+ [Token(TokenType.KEY_VALUE_PAIR, value="cflags=-Wl,a,b,c")],
+ 'cflags="-Wl,a,b,c"',
+ ),
+ # Multi quoted
+ (
+ "cflags=''-Wl,a,b,c''",
+ [Token(TokenType.KEY_VALUE_PAIR, value="cflags=''-Wl,a,b,c''")],
+ 'cflags="-Wl,a,b,c"',
+ ),
+ (
+ 'cflags=="-O3 -g"',
+ [Token(TokenType.PROPAGATED_KEY_VALUE_PAIR, value='cflags=="-O3 -g"')],
+ 'cflags=="-O3 -g"',
+ ),
+ # Way too many spaces
+ (
+ "@1.2 : 1.4 , 1.6 ",
+ [Token(TokenType.VERSION, value="@1.2 : 1.4 , 1.6")],
+ "@1.2:1.4,1.6",
+ ),
+ (
+ "@1.2 : develop",
+ [
+ Token(TokenType.VERSION, value="@1.2 : develop"),
+ ],
+ "@1.2:develop",
+ ),
+ (
+ "@1.2 : develop = foo",
+ [
+ Token(TokenType.VERSION, value="@1.2 :"),
+ Token(TokenType.KEY_VALUE_PAIR, value="develop = foo"),
+ ],
+ "@1.2: develop=foo",
+ ),
+ (
+ "% intel @ 12.1 : 12.6 + debug",
+ [
+ Token(TokenType.COMPILER_AND_VERSION, value="% intel @ 12.1 : 12.6"),
+ Token(TokenType.BOOL_VARIANT, value="+ debug"),
+ ],
+ "%intel@12.1:12.6+debug",
+ ),
+ (
+ "@ 12.1 : 12.6 + debug - qt_4",
+ [
+ Token(TokenType.VERSION, value="@ 12.1 : 12.6"),
+ Token(TokenType.BOOL_VARIANT, value="+ debug"),
+ Token(TokenType.BOOL_VARIANT, value="- qt_4"),
+ ],
+ "@12.1:12.6+debug~qt_4",
+ ),
+ (
+ "@10.4.0:10,11.3.0:target=aarch64:",
+ [
+ Token(TokenType.VERSION, value="@10.4.0:10,11.3.0:"),
+ Token(TokenType.KEY_VALUE_PAIR, value="target=aarch64:"),
+ ],
+ "@10.4.0:10,11.3.0: arch=None-None-aarch64:",
+ ),
+ (
+ "@:0.4 % nvhpc",
+ [
+ Token(TokenType.VERSION, value="@:0.4"),
+ Token(TokenType.COMPILER, value="% nvhpc"),
+ ],
+ "@:0.4%nvhpc",
+ ),
+ ],
+)
+def test_parse_single_spec(spec_str, tokens, expected_roundtrip):
+ parser = SpecParser(spec_str)
+ assert parser.tokens() == tokens
+ assert str(parser.next_spec()) == expected_roundtrip
+
+
+@pytest.mark.parametrize(
+ "text,tokens,expected_specs",
+ [
+ (
+ "mvapich emacs",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="emacs"),
+ ],
+ ["mvapich", "emacs"],
+ ),
+ (
+ "mvapich cppflags='-O3 -fPIC' emacs",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich"),
+ Token(TokenType.KEY_VALUE_PAIR, value="cppflags='-O3 -fPIC'"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="emacs"),
+ ],
+ ["mvapich cppflags='-O3 -fPIC'", "emacs"],
+ ),
+ (
+ "mvapich cppflags=-O3 emacs",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich"),
+ Token(TokenType.KEY_VALUE_PAIR, value="cppflags=-O3"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="emacs"),
+ ],
+ ["mvapich cppflags=-O3", "emacs"],
+ ),
+ (
+ "mvapich emacs @1.1.1 %intel cflags=-O3",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="emacs"),
+ Token(TokenType.VERSION, value="@1.1.1"),
+ Token(TokenType.COMPILER, value="%intel"),
+ Token(TokenType.KEY_VALUE_PAIR, value="cflags=-O3"),
+ ],
+ ["mvapich", "emacs @1.1.1 %intel cflags=-O3"],
+ ),
+ (
'mvapich cflags="-O3 -fPIC" emacs^ncurses%intel',
- )
-
- def test_spec_with_version_hash_pair(self):
- hash = "abc12" * 8
- self.check_parse("develop-branch-version@%s=develop" % hash)
-
- def test_full_specs(self):
- self.check_parse(
- "mvapich_foo" " ^_openmpi@1.2:1.4,1.6%intel@12.1+debug~qt_4" " ^stackwalker@8.1_1e"
- )
- self.check_parse(
- "mvapich_foo" " ^_openmpi@1.2:1.4,1.6%intel@12.1~qt_4 debug=2" " ^stackwalker@8.1_1e"
- )
- self.check_parse(
- "mvapich_foo"
- ' ^_openmpi@1.2:1.4,1.6%intel@12.1 cppflags="-O3" +debug~qt_4'
- " ^stackwalker@8.1_1e"
- )
- self.check_parse(
- "mvapich_foo"
- " ^_openmpi@1.2:1.4,1.6%intel@12.1~qt_4 debug=2"
- " ^stackwalker@8.1_1e arch=test-redhat6-x86"
- )
-
- def test_yaml_specs(self):
- self.check_parse("yaml-cpp@0.1.8%intel@12.1" " ^boost@3.1.4")
- tempspec = r"builtin.yaml-cpp%gcc"
- self.check_parse(tempspec.strip("builtin."), spec=tempspec)
- tempspec = r"testrepo.yaml-cpp%gcc"
- self.check_parse(tempspec.strip("testrepo."), spec=tempspec)
- tempspec = r"builtin.yaml-cpp@0.1.8%gcc"
- self.check_parse(tempspec.strip("builtin."), spec=tempspec)
- tempspec = r"builtin.yaml-cpp@0.1.8%gcc@7.2.0"
- self.check_parse(tempspec.strip("builtin."), spec=tempspec)
- tempspec = r"builtin.yaml-cpp@0.1.8%gcc@7.2.0" r" ^boost@3.1.4"
- self.check_parse(tempspec.strip("builtin."), spec=tempspec)
-
- def test_canonicalize(self):
- self.check_parse(
- "mvapich_foo"
- " ^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4"
- " ^stackwalker@8.1_1e",
- "mvapich_foo "
- "^_openmpi@1.6,1.2:1.4%intel@12.1:12.6+debug~qt_4 "
- "^stackwalker@8.1_1e",
- )
-
- self.check_parse(
- "mvapich_foo"
- " ^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4"
- " ^stackwalker@8.1_1e",
- "mvapich_foo "
- "^stackwalker@8.1_1e "
- "^_openmpi@1.6,1.2:1.4%intel@12.1:12.6~qt_4+debug",
- )
-
- self.check_parse(
- "x ^y@1,2:3,4%intel@1,2,3,4+a~b+c~d+e~f", "x ^y~f+e~d+c~b+a@4,2:3,1%intel@4,3,2,1"
- )
-
- default_target = spack.platforms.test.Test.default
- self.check_parse(
- "x arch=test-redhat6-None"
- + (" ^y arch=test-None-%s" % default_target)
- + " ^z arch=linux-None-None",
- "x os=fe " "^y target=be " "^z platform=linux",
- )
-
- self.check_parse(
- ("x arch=test-debian6-%s" % default_target)
- + (" ^y arch=test-debian6-%s" % default_target),
- "x os=default_os target=default_target" " ^y os=default_os target=default_target",
- )
-
- self.check_parse("x ^y", "x@: ^y@:")
-
- def test_parse_redundant_deps(self):
- self.check_parse("x ^y@foo", "x ^y@foo ^y@foo")
- self.check_parse("x ^y@foo+bar", "x ^y@foo ^y+bar")
- self.check_parse("x ^y@foo+bar", "x ^y@foo+bar ^y")
- self.check_parse("x ^y@foo+bar", "x ^y ^y@foo+bar")
-
- def test_parse_errors(self):
- errors = ["x@@1.2", "x ^y@@1.2", "x@1.2::", "x::"]
- self._check_raises(SpecParseError, errors)
-
- def _check_hash_parse(self, spec):
- """Check several ways to specify a spec by hash."""
- # full hash
- self.check_parse(str(spec), "/" + spec.dag_hash())
-
- # partial hash
- self.check_parse(str(spec), "/ " + spec.dag_hash()[:5])
-
- # name + hash
- self.check_parse(str(spec), spec.name + "/" + spec.dag_hash())
-
- # name + version + space + partial hash
- self.check_parse(
- str(spec), spec.name + "@" + str(spec.version) + " /" + spec.dag_hash()[:6]
- )
-
- @pytest.mark.db
- def test_spec_by_hash(self, database):
- specs = database.query()
- assert len(specs) # make sure something's in the DB
-
- for spec in specs:
- self._check_hash_parse(spec)
-
- @pytest.mark.db
- def test_dep_spec_by_hash(self, database):
- mpileaks_zmpi = database.query_one("mpileaks ^zmpi")
- zmpi = database.query_one("zmpi")
- fake = database.query_one("fake")
-
- assert "fake" in mpileaks_zmpi
- assert "zmpi" in mpileaks_zmpi
-
- mpileaks_hash_fake = sp.Spec("mpileaks ^/" + fake.dag_hash())
- assert "fake" in mpileaks_hash_fake
- assert mpileaks_hash_fake["fake"] == fake
-
- mpileaks_hash_zmpi = sp.Spec(
- "mpileaks %" + str(mpileaks_zmpi.compiler) + " ^ / " + zmpi.dag_hash()
- )
- assert "zmpi" in mpileaks_hash_zmpi
- assert mpileaks_hash_zmpi["zmpi"] == zmpi
- assert mpileaks_hash_zmpi.compiler == mpileaks_zmpi.compiler
-
- mpileaks_hash_fake_and_zmpi = sp.Spec(
- "mpileaks ^/" + fake.dag_hash()[:4] + "^ / " + zmpi.dag_hash()[:5]
- )
- assert "zmpi" in mpileaks_hash_fake_and_zmpi
- assert mpileaks_hash_fake_and_zmpi["zmpi"] == zmpi
-
- assert "fake" in mpileaks_hash_fake_and_zmpi
- assert mpileaks_hash_fake_and_zmpi["fake"] == fake
-
- @pytest.mark.db
- def test_multiple_specs_with_hash(self, database):
- mpileaks_zmpi = database.query_one("mpileaks ^zmpi")
- callpath_mpich2 = database.query_one("callpath ^mpich2")
-
- # name + hash + separate hash
- specs = sp.parse(
- "mpileaks /" + mpileaks_zmpi.dag_hash() + "/" + callpath_mpich2.dag_hash()
- )
- assert len(specs) == 2
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="mvapich"),
+ Token(TokenType.KEY_VALUE_PAIR, value='cflags="-O3 -fPIC"'),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="emacs"),
+ Token(TokenType.DEPENDENCY, value="^"),
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="ncurses"),
+ Token(TokenType.COMPILER, value="%intel"),
+ ],
+ ['mvapich cflags="-O3 -fPIC"', "emacs ^ncurses%intel"],
+ ),
+ ],
+)
+def test_parse_multiple_specs(text, tokens, expected_specs):
+ total_parser = SpecParser(text)
+ assert total_parser.tokens() == tokens
+
+ for single_spec_text in expected_specs:
+ single_spec_parser = SpecParser(single_spec_text)
+ assert str(total_parser.next_spec()) == str(single_spec_parser.next_spec())
+
+
+@pytest.mark.parametrize(
+ "text,expected_in_error",
+ [
+ ("x@@1.2", "x@@1.2\n ^^^^^"),
+ ("y ^x@@1.2", "y ^x@@1.2\n ^^^^^"),
+ ("x@1.2::", "x@1.2::\n ^"),
+ ("x::", "x::\n ^^"),
+ ],
+)
+def test_error_reporting(text, expected_in_error):
+ parser = SpecParser(text)
+ with pytest.raises(SpecTokenizationError) as exc:
+ parser.tokens()
+ assert expected_in_error in str(exc), parser.tokens()
+
+
+@pytest.mark.parametrize(
+ "text,tokens",
+ [
+ ("/abcde", [Token(TokenType.DAG_HASH, value="/abcde")]),
+ (
+ "foo/abcde",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="foo"),
+ Token(TokenType.DAG_HASH, value="/abcde"),
+ ],
+ ),
+ (
+ "foo@1.2.3 /abcde",
+ [
+ Token(TokenType.UNQUALIFIED_PACKAGE_NAME, value="foo"),
+ Token(TokenType.VERSION, value="@1.2.3"),
+ Token(TokenType.DAG_HASH, value="/abcde"),
+ ],
+ ),
+ ],
+)
+def test_spec_by_hash_tokens(text, tokens):
+ parser = SpecParser(text)
+ assert parser.tokens() == tokens
- # 2 separate hashes
- specs = sp.parse("/" + mpileaks_zmpi.dag_hash() + "/" + callpath_mpich2.dag_hash())
- assert len(specs) == 2
- # 2 separate hashes + name
- specs = sp.parse(
- "/" + mpileaks_zmpi.dag_hash() + "/" + callpath_mpich2.dag_hash() + " callpath"
- )
- assert len(specs) == 3
+@pytest.mark.db
+def test_spec_by_hash(database):
+ mpileaks = database.query_one("mpileaks ^zmpi")
+
+ hash_str = f"/{mpileaks.dag_hash()}"
+ assert str(SpecParser(hash_str).next_spec()) == str(mpileaks)
+
+ short_hash_str = f"/{mpileaks.dag_hash()[:5]}"
+ assert str(SpecParser(short_hash_str).next_spec()) == str(mpileaks)
- # hash + 2 names
- specs = sp.parse("/" + mpileaks_zmpi.dag_hash() + " callpath" + " callpath")
- assert len(specs) == 3
+ name_version_and_hash = f"{mpileaks.name}@{mpileaks.version} /{mpileaks.dag_hash()[:5]}"
+ assert str(SpecParser(name_version_and_hash).next_spec()) == str(mpileaks)
- # hash + name + hash
- specs = sp.parse(
- "/" + mpileaks_zmpi.dag_hash() + " callpath" + " / " + callpath_mpich2.dag_hash()
- )
- assert len(specs) == 2
- @pytest.mark.db
- def test_ambiguous_hash(self, mutable_database):
- x1 = Spec("a")
- x1.concretize()
- x1._hash = "xyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
- x2 = Spec("a")
- x2.concretize()
- x2._hash = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
-
- mutable_database.add(x1, spack.store.layout)
- mutable_database.add(x2, spack.store.layout)
-
- # ambiguity in first hash character
- self._check_raises(AmbiguousHashError, ["/x"])
-
- # ambiguity in first hash character AND spec name
- self._check_raises(AmbiguousHashError, ["a/x"])
-
- @pytest.mark.db
- def test_invalid_hash(self, database):
- mpileaks_zmpi = database.query_one("mpileaks ^zmpi")
- zmpi = database.query_one("zmpi")
-
- mpileaks_mpich = database.query_one("mpileaks ^mpich")
- mpich = database.query_one("mpich")
-
- # name + incompatible hash
- self._check_raises(
- InvalidHashError, ["zmpi /" + mpich.dag_hash(), "mpich /" + zmpi.dag_hash()]
- )
-
- # name + dep + incompatible hash
- self._check_raises(
- InvalidHashError,
- [
- "mpileaks ^mpich /" + mpileaks_zmpi.dag_hash(),
- "mpileaks ^zmpi /" + mpileaks_mpich.dag_hash(),
- ],
- )
-
- @pytest.mark.db
- def test_nonexistent_hash(self, database):
- """Ensure we get errors for nonexistant hashes."""
- specs = database.query()
-
- # This hash shouldn't be in the test DB. What are the odds :)
- no_such_hash = "aaaaaaaaaaaaaaa"
- hashes = [s._hash for s in specs]
- assert no_such_hash not in [h[: len(no_such_hash)] for h in hashes]
-
- self._check_raises(NoSuchHashError, ["/" + no_such_hash, "mpileaks /" + no_such_hash])
-
- @pytest.mark.db
- def test_redundant_spec(self, database):
- """Check that redundant spec constraints raise errors.
-
- TODO (TG): does this need to be an error? Or should concrete
- specs only raise errors if constraints cause a contradiction?
-
- """
- mpileaks_zmpi = database.query_one("mpileaks ^zmpi")
- callpath_zmpi = database.query_one("callpath ^zmpi")
- dyninst = database.query_one("dyninst")
-
- mpileaks_mpich2 = database.query_one("mpileaks ^mpich2")
-
- redundant_specs = [
- # redudant compiler
- "/" + mpileaks_zmpi.dag_hash() + "%" + str(mpileaks_zmpi.compiler),
- # redudant version
- "mpileaks/" + mpileaks_mpich2.dag_hash() + "@" + str(mpileaks_mpich2.version),
- # redundant dependency
- "callpath /" + callpath_zmpi.dag_hash() + "^ libelf",
- # redundant flags
- "/" + dyninst.dag_hash() + ' cflags="-O3 -fPIC"',
- ]
-
- self._check_raises(RedundantSpecError, redundant_specs)
-
- def test_duplicate_variant(self):
- duplicates = [
- "x@1.2+debug+debug",
- "x ^y@1.2+debug debug=true",
- "x ^y@1.2 debug=false debug=true",
- "x ^y@1.2 debug=false ~debug",
- ]
- self._check_raises(DuplicateVariantError, duplicates)
-
- def test_multiple_versions(self):
- multiples = [
- "x@1.2@2.3",
- "x@1.2:2.3@1.4",
- "x@1.2@2.3:2.4",
- "x@1.2@2.3,2.4",
- "x@1.2 +foo~bar @2.3",
- "x@1.2%y@1.2@2.3:2.4",
- ]
- self._check_raises(MultipleVersionError, multiples)
-
- def test_duplicate_dependency(self):
- self._check_raises(DuplicateDependencyError, ["x ^y@1 ^y@2"])
-
- def test_duplicate_compiler(self):
- duplicates = [
- "x%intel%intel",
- "x%intel%gcc",
- "x%gcc%intel",
- "x ^y%intel%intel",
- "x ^y%intel%gcc",
- "x ^y%gcc%intel",
- ]
- self._check_raises(DuplicateCompilerSpecError, duplicates)
-
- def test_duplicate_architecture(self):
- duplicates = [
+@pytest.mark.db
+def test_dep_spec_by_hash(database):
+ mpileaks_zmpi = database.query_one("mpileaks ^zmpi")
+ zmpi = database.query_one("zmpi")
+ fake = database.query_one("fake")
+
+ assert "fake" in mpileaks_zmpi
+ assert "zmpi" in mpileaks_zmpi
+
+ mpileaks_hash_fake = SpecParser(f"mpileaks ^/{fake.dag_hash()}").next_spec()
+ assert "fake" in mpileaks_hash_fake
+ assert mpileaks_hash_fake["fake"] == fake
+
+ mpileaks_hash_zmpi = SpecParser(
+ f"mpileaks %{mpileaks_zmpi.compiler} ^ /{zmpi.dag_hash()}"
+ ).next_spec()
+ assert "zmpi" in mpileaks_hash_zmpi
+ assert mpileaks_hash_zmpi["zmpi"] == zmpi
+ assert mpileaks_hash_zmpi.compiler == mpileaks_zmpi.compiler
+
+ mpileaks_hash_fake_and_zmpi = SpecParser(
+ f"mpileaks ^/{fake.dag_hash()[:4]} ^ /{zmpi.dag_hash()[:5]}"
+ ).next_spec()
+ assert "zmpi" in mpileaks_hash_fake_and_zmpi
+ assert mpileaks_hash_fake_and_zmpi["zmpi"] == zmpi
+
+ assert "fake" in mpileaks_hash_fake_and_zmpi
+ assert mpileaks_hash_fake_and_zmpi["fake"] == fake
+
+
+@pytest.mark.db
+def test_multiple_specs_with_hash(database):
+ mpileaks_zmpi = database.query_one("mpileaks ^zmpi")
+ callpath_mpich2 = database.query_one("callpath ^mpich2")
+
+ # name + hash + separate hash
+ specs = SpecParser(
+ f"mpileaks /{mpileaks_zmpi.dag_hash()} /{callpath_mpich2.dag_hash()}"
+ ).all_specs()
+ assert len(specs) == 2
+
+ # 2 separate hashes
+ specs = SpecParser(f"/{mpileaks_zmpi.dag_hash()} /{callpath_mpich2.dag_hash()}").all_specs()
+ assert len(specs) == 2
+
+ # 2 separate hashes + name
+ specs = SpecParser(
+ f"/{mpileaks_zmpi.dag_hash()} /{callpath_mpich2.dag_hash()} callpath"
+ ).all_specs()
+ assert len(specs) == 3
+
+ # hash + 2 names
+ specs = SpecParser(f"/{mpileaks_zmpi.dag_hash()} callpath callpath").all_specs()
+ assert len(specs) == 3
+
+ # hash + name + hash
+ specs = SpecParser(
+ f"/{mpileaks_zmpi.dag_hash()} callpath /{callpath_mpich2.dag_hash()}"
+ ).all_specs()
+ assert len(specs) == 2
+
+
+@pytest.mark.db
+def test_ambiguous_hash(mutable_database, default_mock_concretization):
+ x1 = default_mock_concretization("a")
+ x2 = x1.copy()
+ x1._hash = "xyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
+ x2._hash = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ mutable_database.add(x1, spack.store.layout)
+ mutable_database.add(x2, spack.store.layout)
+
+ # ambiguity in first hash character
+ with pytest.raises(spack.spec.AmbiguousHashError):
+ SpecParser("/x").next_spec()
+
+ # ambiguity in first hash character AND spec name
+ with pytest.raises(spack.spec.AmbiguousHashError):
+ SpecParser("a/x").next_spec()
+
+
+@pytest.mark.db
+def test_invalid_hash(database):
+ zmpi = database.query_one("zmpi")
+ mpich = database.query_one("mpich")
+
+ # name + incompatible hash
+ with pytest.raises(spack.spec.InvalidHashError):
+ SpecParser(f"zmpi /{mpich.dag_hash()}").next_spec()
+ with pytest.raises(spack.spec.InvalidHashError):
+ SpecParser(f"mpich /{zmpi.dag_hash()}").next_spec()
+
+ # name + dep + incompatible hash
+ with pytest.raises(spack.spec.InvalidHashError):
+ SpecParser(f"mpileaks ^zmpi /{mpich.dag_hash()}").next_spec()
+
+
+@pytest.mark.db
+def test_nonexistent_hash(database):
+ """Ensure we get errors for non existent hashes."""
+ specs = database.query()
+
+ # This hash shouldn't be in the test DB. What are the odds :)
+ no_such_hash = "aaaaaaaaaaaaaaa"
+ hashes = [s._hash for s in specs]
+ assert no_such_hash not in [h[: len(no_such_hash)] for h in hashes]
+
+ with pytest.raises(spack.spec.NoSuchHashError):
+ SpecParser(f"/{no_such_hash}").next_spec()
+
+
+@pytest.mark.db
+@pytest.mark.parametrize(
+ "query_str,text_fmt",
+ [
+ ("mpileaks ^zmpi", r"/{hash}%{0.compiler}"),
+ ("callpath ^zmpi", r"callpath /{hash} ^libelf"),
+ ("dyninst", r'/{hash} cflags="-O3 -fPIC"'),
+ ("mpileaks ^mpich2", r"mpileaks/{hash} @{0.version}"),
+ ],
+)
+def test_redundant_spec(query_str, text_fmt, database):
+ """Check that redundant spec constraints raise errors."""
+ spec = database.query_one(query_str)
+ text = text_fmt.format(spec, hash=spec.dag_hash())
+ with pytest.raises(spack.spec.RedundantSpecError):
+ SpecParser(text).next_spec()
+
+
+@pytest.mark.parametrize(
+ "text,exc_cls",
+ [
+ # Duplicate variants
+ ("x@1.2+debug+debug", spack.variant.DuplicateVariantError),
+ ("x ^y@1.2+debug debug=true", spack.variant.DuplicateVariantError),
+ ("x ^y@1.2 debug=false debug=true", spack.variant.DuplicateVariantError),
+ ("x ^y@1.2 debug=false ~debug", spack.variant.DuplicateVariantError),
+ # Multiple versions
+ ("x@1.2@2.3", spack.spec.MultipleVersionError),
+ ("x@1.2:2.3@1.4", spack.spec.MultipleVersionError),
+ ("x@1.2@2.3:2.4", spack.spec.MultipleVersionError),
+ ("x@1.2@2.3,2.4", spack.spec.MultipleVersionError),
+ ("x@1.2 +foo~bar @2.3", spack.spec.MultipleVersionError),
+ ("x@1.2%y@1.2@2.3:2.4", spack.spec.MultipleVersionError),
+ # Duplicate dependency
+ ("x ^y@1 ^y@2", spack.spec.DuplicateDependencyError),
+ # Duplicate compiler
+ ("x%intel%intel", spack.spec.DuplicateCompilerSpecError),
+ ("x%intel%gcc", spack.spec.DuplicateCompilerSpecError),
+ ("x%gcc%intel", spack.spec.DuplicateCompilerSpecError),
+ ("x ^y%intel%intel", spack.spec.DuplicateCompilerSpecError),
+ ("x ^y%intel%gcc", spack.spec.DuplicateCompilerSpecError),
+ ("x ^y%gcc%intel", spack.spec.DuplicateCompilerSpecError),
+ # Duplicate Architectures
+ (
"x arch=linux-rhel7-x86_64 arch=linux-rhel7-x86_64",
+ spack.spec.DuplicateArchitectureError,
+ ),
+ (
"x arch=linux-rhel7-x86_64 arch=linux-rhel7-ppc64le",
+ spack.spec.DuplicateArchitectureError,
+ ),
+ (
"x arch=linux-rhel7-ppc64le arch=linux-rhel7-x86_64",
+ spack.spec.DuplicateArchitectureError,
+ ),
+ (
"y ^x arch=linux-rhel7-x86_64 arch=linux-rhel7-x86_64",
+ spack.spec.DuplicateArchitectureError,
+ ),
+ (
"y ^x arch=linux-rhel7-x86_64 arch=linux-rhel7-ppc64le",
- ]
- self._check_raises(DuplicateArchitectureError, duplicates)
-
- def test_duplicate_architecture_component(self):
- duplicates = [
- "x os=fe os=fe",
- "x os=fe os=be",
- "x target=fe target=fe",
- "x target=fe target=be",
- "x platform=test platform=test",
- "x os=fe platform=test target=fe os=fe",
- "x target=be platform=test os=be os=fe",
- ]
- self._check_raises(DuplicateArchitectureError, duplicates)
-
- @pytest.mark.usefixtures("config")
- def test_parse_yaml_simple(self, mock_packages, tmpdir):
- s = Spec("libdwarf")
- s.concretize()
-
- specfile = tmpdir.join("libdwarf.yaml")
-
- with specfile.open("w") as f:
- f.write(s.to_yaml(hash=ht.dag_hash))
-
- # Check an absolute path to spec.yaml by itself:
- # "spack spec /path/to/libdwarf.yaml"
- specs = sp.parse(specfile.strpath)
- assert len(specs) == 1
-
- # Check absolute path to spec.yaml mixed with a clispec, e.g.:
- # "spack spec mvapich_foo /path/to/libdwarf.yaml"
- specs = sp.parse("mvapich_foo {0}".format(specfile.strpath))
- assert len(specs) == 2
+ spack.spec.DuplicateArchitectureError,
+ ),
+ ("x os=fe os=fe", spack.spec.DuplicateArchitectureError),
+ ("x os=fe os=be", spack.spec.DuplicateArchitectureError),
+ ("x target=fe target=fe", spack.spec.DuplicateArchitectureError),
+ ("x target=fe target=be", spack.spec.DuplicateArchitectureError),
+ ("x platform=test platform=test", spack.spec.DuplicateArchitectureError),
+ ("x os=fe platform=test target=fe os=fe", spack.spec.DuplicateArchitectureError),
+ ("x target=be platform=test os=be os=fe", spack.spec.DuplicateArchitectureError),
+ # Specfile related errors
+ ("/bogus/path/libdwarf.yaml", spack.spec.NoSuchSpecFileError),
+ ("../../libdwarf.yaml", spack.spec.NoSuchSpecFileError),
+ ("./libdwarf.yaml", spack.spec.NoSuchSpecFileError),
+ ("libfoo ^/bogus/path/libdwarf.yaml", spack.spec.NoSuchSpecFileError),
+ ("libfoo ^../../libdwarf.yaml", spack.spec.NoSuchSpecFileError),
+ ("libfoo ^./libdwarf.yaml", spack.spec.NoSuchSpecFileError),
+ ("/bogus/path/libdwarf.yamlfoobar", spack.spec.SpecFilenameError),
+ (
+ "libdwarf^/bogus/path/libelf.yamlfoobar ^/path/to/bogus.yaml",
+ spack.spec.SpecFilenameError,
+ ),
+ ],
+)
+def test_error_conditions(text, exc_cls):
+ with pytest.raises(exc_cls):
+ SpecParser(text).next_spec()
+
+
+def test_parse_specfile_simple(specfile_for, tmpdir):
+ specfile = tmpdir.join("libdwarf.json")
+ s = specfile_for("libdwarf", specfile)
+
+ spec = SpecParser(specfile.strpath).next_spec()
+ assert spec == s
+
+ # Check we can mix literal and spec-file in text
+ specs = SpecParser(f"mvapich_foo {specfile.strpath}").all_specs()
+ assert len(specs) == 2
+
+
+@pytest.mark.parametrize("filename", ["libelf.yaml", "libelf.json"])
+def test_parse_filename_missing_slash_as_spec(specfile_for, tmpdir, filename):
+ """Ensure that libelf(.yaml|.json) parses as a spec, NOT a file."""
+ specfile = tmpdir.join(filename)
+ specfile_for(filename.split(".")[0], specfile)
+
+ # Move to where the specfile is located so that libelf.yaml is there
+ with tmpdir.as_cwd():
+ specs = SpecParser("libelf.yaml").all_specs()
+ assert len(specs) == 1
+
+ spec = specs[0]
+ assert spec.name == "yaml"
+ assert spec.namespace == "libelf"
+ assert spec.fullname == "libelf.yaml"
+
+ # Check that if we concretize this spec, we get a good error
+ # message that mentions we might've meant a file.
+ with pytest.raises(spack.repo.UnknownEntityError) as exc_info:
+ spec.concretize()
+ assert exc_info.value.long_message
+ assert (
+ "Did you mean to specify a filename with './libelf.yaml'?" in exc_info.value.long_message
+ )
- @pytest.mark.usefixtures("config")
- def test_parse_filename_missing_slash_as_spec(self, mock_packages, tmpdir):
- """Ensure that libelf.yaml parses as a spec, NOT a file."""
- # TODO: This test is brittle, as it should cover also the JSON case now.
- s = Spec("libelf")
- s.concretize()
-
- specfile = tmpdir.join("libelf.yaml")
-
- # write the file to the current directory to make sure it exists,
- # and that we still do not parse the spec as a file.
- with specfile.open("w") as f:
- f.write(s.to_yaml(hash=ht.dag_hash))
-
- # Check the spec `libelf.yaml` in the working directory, which
- # should evaluate to a spec called `yaml` in the `libelf`
- # namespace, NOT a spec for `libelf`.
- with tmpdir.as_cwd():
- specs = sp.parse("libelf.yaml")
- assert len(specs) == 1
-
- spec = specs[0]
- assert spec.name == "yaml"
- assert spec.namespace == "libelf"
- assert spec.fullname == "libelf.yaml"
-
- # check that if we concretize this spec, we get a good error
- # message that mentions we might've meant a file.
- with pytest.raises(spack.repo.UnknownEntityError) as exc_info:
- spec.concretize()
- assert exc_info.value.long_message
- assert (
- "Did you mean to specify a filename with './libelf.yaml'?"
- in exc_info.value.long_message
- )
-
- # make sure that only happens when the spec ends in yaml
- with pytest.raises(spack.repo.UnknownPackageError) as exc_info:
- Spec("builtin.mock.doesnotexist").concretize()
- assert not exc_info.value.long_message or (
- "Did you mean to specify a filename with" not in exc_info.value.long_message
- )
-
- @pytest.mark.usefixtures("config")
- def test_parse_yaml_dependency(self, mock_packages, tmpdir):
- s = Spec("libdwarf")
- s.concretize()
-
- specfile = tmpdir.join("libelf.yaml")
-
- with specfile.open("w") as f:
- f.write(s["libelf"].to_yaml(hash=ht.dag_hash))
-
- # Make sure we can use yaml path as dependency, e.g.:
- # "spack spec libdwarf ^ /path/to/libelf.yaml"
- specs = sp.parse("libdwarf ^ {0}".format(specfile.strpath))
- assert len(specs) == 1
-
- @pytest.mark.usefixtures("config")
- def test_parse_yaml_relative_paths(self, mock_packages, tmpdir):
- s = Spec("libdwarf")
- s.concretize()
-
- specfile = tmpdir.join("libdwarf.yaml")
-
- with specfile.open("w") as f:
- f.write(s.to_yaml(hash=ht.dag_hash))
-
- file_name = specfile.basename
- parent_dir = os.path.basename(specfile.dirname)
-
- # Relative path to specfile
- with fs.working_dir(specfile.dirname):
- # Test for command like: "spack spec libelf.yaml"
- # This should parse a single spec, but should not concretize.
- # See test_parse_filename_missing_slash_as_spec()
- specs = sp.parse("{0}".format(file_name))
- assert len(specs) == 1
-
- # Make sure this also works: "spack spec ./libelf.yaml"
- specs = sp.parse("./{0}".format(file_name))
- assert len(specs) == 1
-
- # Should also be accepted: "spack spec ../<cur-dir>/libelf.yaml"
- specs = sp.parse("../{0}/{1}".format(parent_dir, file_name))
- assert len(specs) == 1
-
- # Should also handle mixed clispecs and relative paths, e.g.:
- # "spack spec mvapich_foo ../<cur-dir>/libelf.yaml"
- specs = sp.parse("mvapich_foo ../{0}/{1}".format(parent_dir, file_name))
- assert len(specs) == 2
-
- @pytest.mark.usefixtures("config")
- def test_parse_yaml_relative_subdir_path(self, mock_packages, tmpdir):
- s = Spec("libdwarf")
- s.concretize()
-
- specfile = tmpdir.mkdir("subdir").join("libdwarf.yaml")
-
- with specfile.open("w") as f:
- f.write(s.to_yaml(hash=ht.dag_hash))
-
- file_name = specfile.basename
-
- # Relative path to specfile
- with tmpdir.as_cwd():
- assert os.path.exists("subdir/{0}".format(file_name))
-
- # Test for command like: "spack spec libelf.yaml"
- specs = sp.parse("subdir/{0}".format(file_name))
- assert len(specs) == 1
-
- @pytest.mark.usefixtures("config")
- def test_parse_yaml_dependency_relative_paths(self, mock_packages, tmpdir):
- s = Spec("libdwarf")
- s.concretize()
-
- specfile = tmpdir.join("libelf.yaml")
-
- with specfile.open("w") as f:
- f.write(s["libelf"].to_yaml(hash=ht.dag_hash))
-
- file_name = specfile.basename
- parent_dir = os.path.basename(specfile.dirname)
-
- # Relative path to specfile
- with fs.working_dir(specfile.dirname):
- # Test for command like: "spack spec libelf.yaml"
- specs = sp.parse("libdwarf^{0}".format(file_name))
- assert len(specs) == 1
-
- # Make sure this also works: "spack spec ./libelf.yaml"
- specs = sp.parse("libdwarf^./{0}".format(file_name))
- assert len(specs) == 1
-
- # Should also be accepted: "spack spec ../<cur-dir>/libelf.yaml"
- specs = sp.parse("libdwarf^../{0}/{1}".format(parent_dir, file_name))
- assert len(specs) == 1
-
- def test_parse_yaml_error_handling(self):
- self._check_raises(
- NoSuchSpecFileError,
- [
- # Single spec that looks like a yaml path
- "/bogus/path/libdwarf.yaml",
- "../../libdwarf.yaml",
- "./libdwarf.yaml",
- # Dependency spec that looks like a yaml path
- "libdwarf^/bogus/path/libelf.yaml",
- "libdwarf ^../../libelf.yaml",
- "libdwarf^ ./libelf.yaml",
- # Multiple specs, one looks like a yaml path
- "mvapich_foo /bogus/path/libelf.yaml",
- "mvapich_foo ../../libelf.yaml",
- "mvapich_foo ./libelf.yaml",
- ],
- )
-
- def test_nice_error_for_no_space_after_spec_filename(self):
- """Ensure that omitted spaces don't give weird errors about hashes."""
- self._check_raises(
- SpecFilenameError,
- [
- "/bogus/path/libdwarf.yamlfoobar",
- "libdwarf^/bogus/path/libelf.yamlfoobar ^/path/to/bogus.yaml",
- ],
- )
-
- @pytest.mark.usefixtures("config")
- def test_yaml_spec_not_filename(self, mock_packages, tmpdir):
- with pytest.raises(spack.repo.UnknownPackageError):
- Spec("builtin.mock.yaml").concretize()
-
- with pytest.raises(spack.repo.UnknownPackageError):
- Spec("builtin.mock.yamlfoobar").concretize()
-
- @pytest.mark.usefixtures("config")
- def test_parse_yaml_variant_error(self, mock_packages, tmpdir):
- s = Spec("a")
- s.concretize()
-
- specfile = tmpdir.join("a.yaml")
-
- with specfile.open("w") as f:
- f.write(s.to_yaml(hash=ht.dag_hash))
-
- with pytest.raises(RedundantSpecError):
- # Trying to change a variant on a concrete spec is an error
- sp.parse("{0} ~bvv".format(specfile.strpath))
-
- # ========================================================================
- # Lex checks
- # ========================================================================
- def test_ambiguous(self):
- # This first one is ambiguous because - can be in an identifier AND
- # indicate disabling an option.
- with pytest.raises(AssertionError):
- self.check_lex(
- complex_lex,
- "mvapich_foo"
- "^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug-qt_4"
- "^stackwalker@8.1_1e",
- )
-
- # The following lexes are non-ambiguous (add a space before -qt_4)
- # and should all result in the tokens in complex_lex
- def test_minimal_spaces(self):
- self.check_lex(
- complex_lex,
- "mvapich_foo"
- "^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug -qt_4"
- "^stackwalker@8.1_1e",
- )
- self.check_lex(
- complex_lex,
- "mvapich_foo" "^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4" "^stackwalker@8.1_1e",
- )
-
- def test_spaces_between_dependences(self):
- lex_key = (
- complex_root
- + complex_dep1
- + complex_compiler
- + complex_compiler_v
- + complex_dep1_var
- + complex_dep2_space
- )
- self.check_lex(
- lex_key,
- "mvapich_foo "
- "^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug -qt_4 "
- "^stackwalker @ 8.1_1e",
- )
- self.check_lex(
- lex_key,
- "mvapich_foo "
- "^_openmpi@1.2:1.4,1.6%intel@12.1:12.6+debug~qt_4 "
- "^stackwalker @ 8.1_1e",
- )
-
- def test_spaces_between_options(self):
- self.check_lex(
- complex_lex,
- "mvapich_foo "
- "^_openmpi @1.2:1.4,1.6 %intel @12.1:12.6 +debug -qt_4 "
- "^stackwalker @8.1_1e",
- )
-
- def test_way_too_many_spaces(self):
- lex_key = (
- complex_root
- + complex_dep1
- + complex_compiler
- + complex_compiler_v_space
- + complex_dep1_var
- + complex_dep2_space
- )
- self.check_lex(
- lex_key,
- "mvapich_foo "
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
- lex_key = (
- complex_root
- + complex_dep1
- + complex_compiler
- + complex_compiler_v_space
- + complex_dep1_var
- + complex_dep2_space
- )
- self.check_lex(
- lex_key,
- "mvapich_foo "
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug ~ qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
-
- def test_kv_with_quotes(self):
- self.check_lex(
- kv_lex,
- "mvapich_foo debug='4' "
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
- self.check_lex(
- kv_lex,
- 'mvapich_foo debug="4" '
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
- self.check_lex(
- kv_lex,
- "mvapich_foo 'debug = 4' "
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
-
- def test_kv_without_quotes(self):
- self.check_lex(
- kv_lex,
- "mvapich_foo debug=4 "
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
-
- def test_kv_with_spaces(self):
- self.check_lex(
- kv_lex,
- "mvapich_foo debug = 4 "
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
- self.check_lex(
- kv_lex,
- "mvapich_foo debug =4 "
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
- self.check_lex(
- kv_lex,
- "mvapich_foo debug= 4 "
- "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
- "^ stackwalker @ 8.1_1e",
- )
-
- @pytest.mark.parametrize(
- "expected_tokens,spec_string",
- [
- (
- [Token(sp.ID, "target"), Token(sp.EQ, "="), Token(sp.VAL, "broadwell")],
- "target=broadwell",
- ),
- (
- [Token(sp.ID, "target"), Token(sp.EQ, "="), Token(sp.VAL, ":broadwell,icelake")],
- "target=:broadwell,icelake",
- ),
- ],
+ # make sure that only happens when the spec ends in yaml
+ with pytest.raises(spack.repo.UnknownPackageError) as exc_info:
+ SpecParser("builtin.mock.doesnotexist").next_spec().concretize()
+ assert not exc_info.value.long_message or (
+ "Did you mean to specify a filename with" not in exc_info.value.long_message
)
- def test_target_tokenization(self, expected_tokens, spec_string):
- self.check_lex(expected_tokens, spec_string)
-
- @pytest.mark.regression("20310")
- def test_compare_abstract_specs(self):
- """Spec comparisons must be valid for abstract specs.
-
- Check that the spec cmp_key appropriately handles comparing specs for
- which some attributes are None in exactly one of two specs"""
- # Add fields in order they appear in `Spec._cmp_node`
- constraints = [
- None,
- "foo",
- "foo.foo",
- "foo.foo@foo",
- "foo.foo@foo+foo",
- "foo.foo@foo+foo arch=foo-foo-foo",
- "foo.foo@foo+foo arch=foo-foo-foo %foo",
- "foo.foo@foo+foo arch=foo-foo-foo %foo cflags=foo",
- ]
- specs = [Spec(s) for s in constraints]
-
- for a, b in itertools.product(specs, repeat=2):
- # Check that we can compare without raising an error
- assert a <= b or b < a
-
- def test_git_ref_specs_with_variants(self):
- spec_str = "develop-branch-version@git.{h}=develop+var1+var2".format(h="a" * 40)
- self.check_parse(spec_str)
-
- def test_git_ref_spec_equivalences(self, mock_packages, mock_stage):
- s1 = sp.Spec("develop-branch-version@git.{hash}=develop".format(hash="a" * 40))
- s2 = sp.Spec("develop-branch-version@git.{hash}=develop".format(hash="b" * 40))
- s3 = sp.Spec("develop-branch-version@git.0.2.15=develop")
- s_no_git = sp.Spec("develop-branch-version@develop")
-
- assert s1.satisfies(s_no_git)
- assert s2.satisfies(s_no_git)
- assert not s_no_git.satisfies(s1)
- assert not s2.satisfies(s1)
- assert not s3.satisfies(s1)
-
- @pytest.mark.regression("32471")
- @pytest.mark.parametrize("spec_str", ["target=x86_64", "os=redhat6", "target=x86_64:"])
- def test_platform_is_none_if_not_present(self, spec_str):
- s = sp.Spec(spec_str)
- assert s.architecture.platform is None, s
+
+
+def test_parse_specfile_dependency(default_mock_concretization, tmpdir):
+ """Ensure we can use a specfile as a dependency"""
+ s = default_mock_concretization("libdwarf")
+
+ specfile = tmpdir.join("libelf.json")
+ with specfile.open("w") as f:
+ f.write(s["libelf"].to_json())
+
+ # Make sure we can use yaml path as dependency, e.g.:
+ # "spack spec libdwarf ^ /path/to/libelf.json"
+ spec = SpecParser(f"libdwarf ^ {specfile.strpath}").next_spec()
+ assert spec["libelf"] == s["libelf"]
+
+ with specfile.dirpath().as_cwd():
+ # Make sure this also works: "spack spec ./libelf.yaml"
+ spec = SpecParser(f"libdwarf^./{specfile.basename}").next_spec()
+ assert spec["libelf"] == s["libelf"]
+
+ # Should also be accepted: "spack spec ../<cur-dir>/libelf.yaml"
+ spec = SpecParser(
+ f"libdwarf^../{specfile.dirpath().basename}/{specfile.basename}"
+ ).next_spec()
+ assert spec["libelf"] == s["libelf"]
+
+
+def test_parse_specfile_relative_paths(specfile_for, tmpdir):
+ specfile = tmpdir.join("libdwarf.json")
+ s = specfile_for("libdwarf", specfile)
+
+ basename = specfile.basename
+ parent_dir = specfile.dirpath()
+
+ with parent_dir.as_cwd():
+ # Make sure this also works: "spack spec ./libelf.yaml"
+ spec = SpecParser(f"./{basename}").next_spec()
+ assert spec == s
+
+ # Should also be accepted: "spack spec ../<cur-dir>/libelf.yaml"
+ spec = SpecParser(f"../{parent_dir.basename}/{basename}").next_spec()
+ assert spec == s
+
+ # Should also handle mixed clispecs and relative paths, e.g.:
+ # "spack spec mvapich_foo ../<cur-dir>/libelf.yaml"
+ specs = SpecParser(f"mvapich_foo ../{parent_dir.basename}/{basename}").all_specs()
+ assert len(specs) == 2
+ assert specs[1] == s
+
+
+def test_parse_specfile_relative_subdir_path(specfile_for, tmpdir):
+ specfile = tmpdir.mkdir("subdir").join("libdwarf.json")
+ s = specfile_for("libdwarf", specfile)
+
+ with tmpdir.as_cwd():
+ spec = SpecParser(f"subdir/{specfile.basename}").next_spec()
+ assert spec == s
+
+
+@pytest.mark.regression("20310")
+def test_compare_abstract_specs():
+ """Spec comparisons must be valid for abstract specs.
+
+ Check that the spec cmp_key appropriately handles comparing specs for
+ which some attributes are None in exactly one of two specs
+ """
+ # Add fields in order they appear in `Spec._cmp_node`
+ constraints = [
+ "foo",
+ "foo.foo",
+ "foo.foo@foo",
+ "foo.foo@foo+foo",
+ "foo.foo@foo+foo arch=foo-foo-foo",
+ "foo.foo@foo+foo arch=foo-foo-foo %foo",
+ "foo.foo@foo+foo arch=foo-foo-foo %foo cflags=foo",
+ ]
+ specs = [SpecParser(s).next_spec() for s in constraints]
+
+ for a, b in itertools.product(specs, repeat=2):
+ # Check that we can compare without raising an error
+ assert a <= b or b < a
+
+
+def test_git_ref_spec_equivalences(mock_packages):
+ spec_hash_fmt = "develop-branch-version@git.{hash}=develop"
+ s1 = SpecParser(spec_hash_fmt.format(hash="a" * 40)).next_spec()
+ s2 = SpecParser(spec_hash_fmt.format(hash="b" * 40)).next_spec()
+ s3 = SpecParser("develop-branch-version@git.0.2.15=develop").next_spec()
+ s_no_git = SpecParser("develop-branch-version@develop").next_spec()
+
+ assert s1.satisfies(s_no_git)
+ assert s2.satisfies(s_no_git)
+ assert not s_no_git.satisfies(s1)
+ assert not s2.satisfies(s1)
+ assert not s3.satisfies(s1)
+
+
+@pytest.mark.regression("32471")
+@pytest.mark.parametrize("spec_str", ["target=x86_64", "os=redhat6", "target=x86_64:"])
+def test_platform_is_none_if_not_present(spec_str):
+ s = SpecParser(spec_str).next_spec()
+ assert s.architecture.platform is None, s