# 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

# Sample output for a complex lexing.
complex_lex = [Token(sp.ID, 'mvapich_foo'),
               Token(sp.DEP),
               Token(sp.ID, '_openmpi'),
               Token(sp.AT),
               Token(sp.ID, '1.2'),
               Token(sp.COLON),
               Token(sp.ID, '1.4'),
               Token(sp.COMMA),
               Token(sp.ID, '1.6'),
               Token(sp.PCT),
               Token(sp.ID, 'intel'),
               Token(sp.AT),
               Token(sp.ID, '12.1'),
               Token(sp.COLON),
               Token(sp.ID, '12.6'),
               Token(sp.ON),
               Token(sp.ID, 'debug'),
               Token(sp.OFF),
               Token(sp.ID, 'qt_4'),
               Token(sp.DEP),
               Token(sp.ID, 'stackwalker'),
               Token(sp.AT),
               Token(sp.ID, '8.1_1e')]

# Another sample lexer output with a kv pair.
kv_lex = [Token(sp.ID, 'mvapich_foo'),
          Token(sp.ID, 'debug'),
          Token(sp.EQ),
          Token(sp.VAL, '4'),
          Token(sp.DEP),
          Token(sp.ID, '_openmpi'),
          Token(sp.AT),
          Token(sp.ID, '1.2'),
          Token(sp.COLON),
          Token(sp.ID, '1.4'),
          Token(sp.COMMA),
          Token(sp.ID, '1.6'),
          Token(sp.PCT),
          Token(sp.ID, 'intel'),
          Token(sp.AT),
          Token(sp.ID, '12.1'),
          Token(sp.COLON),
          Token(sp.ID, '12.6'),
          Token(sp.ON),
          Token(sp.ID, 'debug'),
          Token(sp.OFF),
          Token(sp.ID, 'qt_4'),
          Token(sp.DEP),
          Token(sp.ID, 'stackwalker'),
          Token(sp.AT),
          Token(sp.ID, '8.1_1e')]


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 == sp.ID or tok.type == sp.VAL:
                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_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."""
        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.UnknownPackageError) 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):
        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_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):
        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_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
