Support windows paths in a spec (#34405)

Refactor Spack's spec syntax parsing to handle Windows style paths
Amend unit tests to reflect this updated behavior.
This commit is contained in:
John W. Parent 2023-01-14 01:52:37 -05:00 committed by GitHub
parent 6e95417c98
commit ff38ff25cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 149 additions and 24 deletions

View File

@ -59,6 +59,7 @@
import enum import enum
import pathlib import pathlib
import re import re
import sys
from typing import Iterator, List, Match, Optional from typing import Iterator, List, Match, Optional
from llnl.util.tty import color from llnl.util.tty import color
@ -68,6 +69,7 @@
import spack.variant import spack.variant
import spack.version import spack.version
IS_WINDOWS = sys.platform == "win32"
#: Valid name for specs and variants. Here we are not using #: Valid name for specs and variants. Here we are not using
#: the previous "w[\w.-]*" since that would match most #: the previous "w[\w.-]*" since that would match most
#: characters that can be part of a word in any language #: characters that can be part of a word in any language
@ -80,8 +82,15 @@
HASH = r"[a-zA-Z_0-9]+" HASH = r"[a-zA-Z_0-9]+"
#: A filename starts either with a "." or a "/" or a "{name}/" #: A filename starts either with a "." or a "/" or a "{name}/,
FILENAME = r"(\.|\/|[a-zA-Z0-9-_]*\/)([a-zA-Z0-9-_\.\/]*)(\.json|\.yaml)" # or on Windows, a drive letter followed by a colon and "\"
# or "." or {name}\
WINDOWS_FILENAME = r"(\.|[a-zA-Z0-9-_]*\\|[a-zA-Z]:\\)([a-zA-Z0-9-_\.\\]*)(\.json|\.yaml)"
UNIX_FILENAME = r"(\.|\/|[a-zA-Z0-9-_]*\/)([a-zA-Z0-9-_\.\/]*)(\.json|\.yaml)"
if not IS_WINDOWS:
FILENAME = UNIX_FILENAME
else:
FILENAME = WINDOWS_FILENAME
VALUE = r"([a-zA-Z_0-9\-+\*.,:=\~\/\\]+)" VALUE = r"([a-zA-Z_0-9\-+\*.,:=\~\/\\]+)"
QUOTED_VALUE = r"[\"']+([a-zA-Z_0-9\-+\*.,:=\~\/\\\s]+)[\"']+" QUOTED_VALUE = r"[\"']+([a-zA-Z_0-9\-+\*.,:=\~\/\\\s]+)[\"']+"

View File

@ -3,6 +3,8 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import itertools import itertools
import os
import re
import sys import sys
import pytest import pytest
@ -10,9 +12,24 @@
import spack.platforms.test import spack.platforms.test
import spack.spec import spack.spec
import spack.variant import spack.variant
from spack.parser import SpecParser, SpecTokenizationError, Token, TokenType from spack.parser import (
UNIX_FILENAME,
WINDOWS_FILENAME,
SpecParser,
SpecTokenizationError,
Token,
TokenType,
)
is_windows = sys.platform == "win32" FAIL_ON_WINDOWS = pytest.mark.xfail(
sys.platform == "win32",
raises=(SpecTokenizationError, spack.spec.NoSuchHashError),
reason="Unix style path on Windows",
)
FAIL_ON_UNIX = pytest.mark.xfail(
sys.platform != "win32", raises=SpecTokenizationError, reason="Windows style path on Unix"
)
def simple_package_name(name): def simple_package_name(name):
@ -818,18 +835,6 @@ def test_redundant_spec(query_str, text_fmt, database):
("x platform=test platform=test", spack.spec.DuplicateArchitectureError), ("x platform=test platform=test", spack.spec.DuplicateArchitectureError),
("x os=fe platform=test target=fe os=fe", 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), ("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): def test_error_conditions(text, exc_cls):
@ -837,7 +842,114 @@ def test_error_conditions(text, exc_cls):
SpecParser(text).next_spec() SpecParser(text).next_spec()
@pytest.mark.skipif(is_windows, reason="Spec parsing does not currently support Windows paths") @pytest.mark.parametrize(
"text,exc_cls",
[
# Specfile related errors
pytest.param(
"/bogus/path/libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_WINDOWS,
),
pytest.param(
"../../libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_WINDOWS,
),
pytest.param(
"./libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_WINDOWS,
),
pytest.param(
"libfoo ^/bogus/path/libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_WINDOWS,
),
pytest.param(
"libfoo ^../../libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_WINDOWS,
),
pytest.param(
"libfoo ^./libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_WINDOWS,
),
pytest.param(
"/bogus/path/libdwarf.yamlfoobar",
spack.spec.SpecFilenameError,
marks=FAIL_ON_WINDOWS,
),
pytest.param(
"libdwarf^/bogus/path/libelf.yamlfoobar ^/path/to/bogus.yaml",
spack.spec.SpecFilenameError,
marks=FAIL_ON_WINDOWS,
),
pytest.param(
"c:\\bogus\\path\\libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_UNIX,
),
pytest.param(
"..\\..\\libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_UNIX,
),
pytest.param(
".\\libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_UNIX,
),
pytest.param(
"libfoo ^c:\\bogus\\path\\libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_UNIX,
),
pytest.param(
"libfoo ^..\\..\\libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_UNIX,
),
pytest.param(
"libfoo ^.\\libdwarf.yaml",
spack.spec.NoSuchSpecFileError,
marks=FAIL_ON_UNIX,
),
pytest.param(
"c:\\bogus\\path\\libdwarf.yamlfoobar",
spack.spec.SpecFilenameError,
marks=FAIL_ON_UNIX,
),
pytest.param(
"libdwarf^c:\\bogus\\path\\libelf.yamlfoobar ^c:\\path\\to\\bogus.yaml",
spack.spec.SpecFilenameError,
marks=FAIL_ON_UNIX,
),
],
)
def test_specfile_error_conditions_windows(text, exc_cls):
with pytest.raises(exc_cls):
SpecParser(text).next_spec()
@pytest.mark.parametrize(
"filename,regex",
[
(r"c:\abs\windows\\path.yaml", WINDOWS_FILENAME),
(r".\\relative\\dot\\win\\path.yaml", WINDOWS_FILENAME),
(r"relative\\windows\\path.yaml", WINDOWS_FILENAME),
("/absolute/path/to/file.yaml", UNIX_FILENAME),
("relative/path/to/file.yaml", UNIX_FILENAME),
("./dot/rel/to/file.yaml", UNIX_FILENAME),
],
)
def test_specfile_parsing(filename, regex):
match = re.match(regex, filename)
assert match
assert match.end() == len(filename)
def test_parse_specfile_simple(specfile_for, tmpdir): def test_parse_specfile_simple(specfile_for, tmpdir):
specfile = tmpdir.join("libdwarf.json") specfile = tmpdir.join("libdwarf.json")
s = specfile_for("libdwarf", specfile) s = specfile_for("libdwarf", specfile)
@ -883,7 +995,6 @@ def test_parse_filename_missing_slash_as_spec(specfile_for, tmpdir, filename):
) )
@pytest.mark.skipif(is_windows, reason="Spec parsing does not currently support Windows paths")
def test_parse_specfile_dependency(default_mock_concretization, tmpdir): def test_parse_specfile_dependency(default_mock_concretization, tmpdir):
"""Ensure we can use a specfile as a dependency""" """Ensure we can use a specfile as a dependency"""
s = default_mock_concretization("libdwarf") s = default_mock_concretization("libdwarf")
@ -899,12 +1010,13 @@ def test_parse_specfile_dependency(default_mock_concretization, tmpdir):
with specfile.dirpath().as_cwd(): with specfile.dirpath().as_cwd():
# Make sure this also works: "spack spec ./libelf.yaml" # Make sure this also works: "spack spec ./libelf.yaml"
spec = SpecParser(f"libdwarf^./{specfile.basename}").next_spec() spec = SpecParser(f"libdwarf^.{os.path.sep}{specfile.basename}").next_spec()
assert spec["libelf"] == s["libelf"] assert spec["libelf"] == s["libelf"]
# Should also be accepted: "spack spec ../<cur-dir>/libelf.yaml" # Should also be accepted: "spack spec ../<cur-dir>/libelf.yaml"
spec = SpecParser( spec = SpecParser(
f"libdwarf^../{specfile.dirpath().basename}/{specfile.basename}" f"libdwarf^..{os.path.sep}{specfile.dirpath().basename}\
{os.path.sep}{specfile.basename}"
).next_spec() ).next_spec()
assert spec["libelf"] == s["libelf"] assert spec["libelf"] == s["libelf"]
@ -918,16 +1030,20 @@ def test_parse_specfile_relative_paths(specfile_for, tmpdir):
with parent_dir.as_cwd(): with parent_dir.as_cwd():
# Make sure this also works: "spack spec ./libelf.yaml" # Make sure this also works: "spack spec ./libelf.yaml"
spec = SpecParser(f"./{basename}").next_spec() spec = SpecParser(f".{os.path.sep}{basename}").next_spec()
assert spec == s assert spec == s
# Should also be accepted: "spack spec ../<cur-dir>/libelf.yaml" # Should also be accepted: "spack spec ../<cur-dir>/libelf.yaml"
spec = SpecParser(f"../{parent_dir.basename}/{basename}").next_spec() spec = SpecParser(
f"..{os.path.sep}{parent_dir.basename}{os.path.sep}{basename}"
).next_spec()
assert spec == s assert spec == s
# Should also handle mixed clispecs and relative paths, e.g.: # Should also handle mixed clispecs and relative paths, e.g.:
# "spack spec mvapich_foo ../<cur-dir>/libelf.yaml" # "spack spec mvapich_foo ../<cur-dir>/libelf.yaml"
specs = SpecParser(f"mvapich_foo ../{parent_dir.basename}/{basename}").all_specs() specs = SpecParser(
f"mvapich_foo ..{os.path.sep}{parent_dir.basename}{os.path.sep}{basename}"
).all_specs()
assert len(specs) == 2 assert len(specs) == 2
assert specs[1] == s assert specs[1] == s
@ -937,7 +1053,7 @@ def test_parse_specfile_relative_subdir_path(specfile_for, tmpdir):
s = specfile_for("libdwarf", specfile) s = specfile_for("libdwarf", specfile)
with tmpdir.as_cwd(): with tmpdir.as_cwd():
spec = SpecParser(f"subdir/{specfile.basename}").next_spec() spec = SpecParser(f"subdir{os.path.sep}{specfile.basename}").next_spec()
assert spec == s assert spec == s