
Caches used by repositories don't reference the global spack.repo.path instance anymore, but get the repository they refer to during initialization. Spec.virtual now use the index, and computation done to compute the index use Repository.is_virtual_safe. Code to construct mock packages and mock repository has been factored into a unique MockRepositoryBuilder that is used throughout the codebase. Add debug print for pushing and popping config scopes. Changed spack.repo.use_repositories so that it can override or not previous repos spack.repo.use_repositories updates spack.config.config according to the modifications done Removed a peculiar behavior from spack.config.Configuration where push would always bubble-up a scope named command_line if it existed
915 lines
30 KiB
Python
915 lines
30 KiB
Python
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
|
|
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
|
#
|
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
|
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
|
|
)
|
|
|
|
# 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
|
|
)
|
|
|
|
|
|
class TestSpecSyntax(object):
|
|
# ========================================================================
|
|
# Parse checks
|
|
# ========================================================================
|
|
|
|
def check_parse(self, expected, spec=None):
|
|
"""Assert that the provided spec is able to be parsed.
|
|
|
|
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.
|
|
|
|
"""
|
|
if spec is None:
|
|
spec = expected
|
|
output = sp.parse(spec)
|
|
|
|
parsed = " ".join(str(spec) for spec in output)
|
|
assert expected == parsed
|
|
|
|
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
|
|
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',
|
|
'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"
|
|
)
|
|
|
|
self.check_parse(
|
|
"x arch=test-redhat6-None" " ^y arch=test-None-core2" " ^z arch=linux-None-None",
|
|
"x os=fe " "^y target=be " "^z platform=linux",
|
|
)
|
|
|
|
self.check_parse(
|
|
"x arch=test-debian6-core2" " ^y arch=test-debian6-core2",
|
|
"x os=default_os target=default_target" " ^y os=default_os target=default_target",
|
|
)
|
|
|
|
self.check_parse("x ^y", "x@: ^y@:")
|
|
|
|
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
|
|
|
|
# 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
|
|
|
|
# hash + 2 names
|
|
specs = sp.parse("/" + mpileaks_zmpi.dag_hash() + " callpath" + " callpath")
|
|
assert len(specs) == 3
|
|
|
|
# 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 ^y"])
|
|
|
|
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 = [
|
|
"x arch=linux-rhel7-x86_64 arch=linux-rhel7-x86_64",
|
|
"x arch=linux-rhel7-x86_64 arch=linux-rhel7-ppc64le",
|
|
"x arch=linux-rhel7-ppc64le arch=linux-rhel7-x86_64",
|
|
"y ^x arch=linux-rhel7-x86_64 arch=linux-rhel7-x86_64",
|
|
"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
|
|
|
|
@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",
|
|
),
|
|
],
|
|
)
|
|
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)
|