diff options
Diffstat (limited to 'lib/spack/spack/test/spec_syntax.py')
-rw-r--r-- | lib/spack/spack/test/spec_syntax.py | 1849 |
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 |