Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow recent pytest versions to be used with Spack #25371

Merged
merged 7 commits into from
Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/unit_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ jobs:
patchelf cmake bison libbison-dev kcov
- name: Install Python packages
run: |
pip install --upgrade pip six setuptools codecov coverage[toml]
pip install --upgrade pip six setuptools pytest codecov coverage[toml]
# ensure style checks are not skipped in unit tests for python >= 3.6
# note that true/false (i.e., 1/0) are opposite in conditions in python and bash
if python -c 'import sys; sys.exit(not sys.version_info >= (3, 6))'; then
Expand Down Expand Up @@ -173,7 +173,7 @@ jobs:
sudo apt-get install -y coreutils kcov csh zsh tcsh fish dash bash
- name: Install Python packages
run: |
pip install --upgrade pip six setuptools codecov coverage[toml]
pip install --upgrade pip six setuptools pytest codecov coverage[toml]
- name: Setup git configuration
run: |
# Need this for the git tests to succeed.
Expand Down Expand Up @@ -272,7 +272,7 @@ jobs:
patchelf kcov
- name: Install Python packages
run: |
pip install --upgrade pip six setuptools codecov coverage[toml] clingo
pip install --upgrade pip six setuptools pytest codecov coverage[toml] clingo
- name: Setup git configuration
run: |
# Need this for the git tests to succeed.
Expand Down Expand Up @@ -315,7 +315,7 @@ jobs:
- name: Install Python packages
run: |
pip install --upgrade pip six setuptools
pip install --upgrade codecov coverage[toml]
pip install --upgrade pytest codecov coverage[toml]
- name: Setup Homebrew packages
run: |
brew install dash fish gcc gnupg2 kcov
Expand Down
1 change: 1 addition & 0 deletions lib/spack/docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('_spack_root/lib/spack/external'))
sys.path.insert(0, os.path.abspath('_spack_root/lib/spack/external/pytest-fallback'))

if sys.version_info[0] < 3:
sys.path.insert(
Expand Down
86 changes: 62 additions & 24 deletions lib/spack/spack/cmd/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@

import argparse
import collections
import os.path
import re
import sys

import pytest
try:
import pytest
except ImportError:
pytest = None # type: ignore

from six import StringIO

import llnl.util.filesystem
import llnl.util.tty.color as color
from llnl.util.filesystem import working_dir
from llnl.util.tty.colify import colify

import spack.paths
Expand Down Expand Up @@ -67,7 +72,25 @@ def setup_parser(subparser):

def do_list(args, extra_args):
"""Print a lists of tests than what pytest offers."""
# Run test collection and get the tree out.
def colorize(c, prefix):
if isinstance(prefix, tuple):
return "::".join(
color.colorize("@%s{%s}" % (c, p))
for p in prefix if p != "()"
)
return color.colorize("@%s{%s}" % (c, prefix))

# To list the files we just need to inspect the filesystem,
# which doesn't need to wait for pytest collection and doesn't
# require parsing pytest output
files = llnl.util.filesystem.find(
root=spack.paths.test_path, files='*.py', recursive=True
)
files = [
os.path.relpath(f, start=spack.paths.spack_root)
for f in files if not f.endswith(('conftest.py', '__init__.py'))
]

old_output = sys.stdout
try:
sys.stdout = output = StringIO()
Expand All @@ -76,12 +99,13 @@ def do_list(args, extra_args):
sys.stdout = old_output

lines = output.getvalue().split('\n')
tests = collections.defaultdict(lambda: set())
prefix = []
tests = collections.defaultdict(set)

# collect tests into sections
node_regexp = re.compile(r"(\s*)<([^ ]*) ['\"]?([^']*)['\"]?>")
key_parts, name_parts = [], []
for line in lines:
match = re.match(r"(\s*)<([^ ]*) '([^']*)'", line)
match = node_regexp.match(line)
if not match:
continue
indent, nodetype, name = match.groups()
Expand All @@ -90,25 +114,31 @@ def do_list(args, extra_args):
if "[" in name:
name = name[:name.index("[")]

depth = len(indent) // 2

if nodetype.endswith("Function"):
key = tuple(prefix)
tests[key].add(name)
else:
prefix = prefix[:depth]
prefix.append(name)

def colorize(c, prefix):
if isinstance(prefix, tuple):
return "::".join(
color.colorize("@%s{%s}" % (c, p))
for p in prefix if p != "()"
)
return color.colorize("@%s{%s}" % (c, prefix))
len_indent = len(indent)
if os.path.isabs(name):
name = os.path.relpath(name, start=spack.paths.spack_root)

item = (len_indent, name, nodetype)

# Reduce the parts to the scopes that are of interest
name_parts = [x for x in name_parts if x[0] < len_indent]
key_parts = [x for x in key_parts if x[0] < len_indent]

# From version 3.X to version 6.X the output format
# changed a lot in pytest, and probably will change
# in the future - so this manipulation might be fragile
if nodetype.lower() == 'function':
name_parts.append(item)
key_end = os.path.join(*[x[1] for x in key_parts])
key = next(f for f in files if f.endswith(key_end))
tests[key].add(tuple(x[1] for x in name_parts))
elif nodetype.lower() == 'class':
name_parts.append(item)
elif nodetype.lower() in ('package', 'module'):
key_parts.append(item)

if args.list == "list":
files = set(prefix[0] for prefix in tests)
files = set(tests.keys())
color_files = [colorize("B", file) for file in sorted(files)]
colify(color_files)

Expand Down Expand Up @@ -144,6 +174,14 @@ def add_back_pytest_args(args, unknown_args):


def unit_test(parser, args, unknown_args):
global pytest
if pytest is None:
vendored_pytest_dir = os.path.join(
spack.paths.external_path, 'pytest-fallback'
)
sys.path.append(vendored_pytest_dir)
import pytest

if args.pytest_help:
# make the pytest.main help output more accurate
sys.argv[0] = 'spack unit-test'
Expand All @@ -161,7 +199,7 @@ def unit_test(parser, args, unknown_args):
pytest_root = spack.extensions.path_for_extension(target, *extensions)

# pytest.ini lives in the root of the spack repository.
with working_dir(pytest_root):
with llnl.util.filesystem.working_dir(pytest_root):
if args.list:
do_list(args, pytest_args)
return
Expand Down
5 changes: 4 additions & 1 deletion lib/spack/spack/test/cmd/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ def test_list_with_pytest_arg():


def test_list_with_keywords():
output = spack_test('--list', '-k', 'cmd/unit_test.py')
# Here we removed querying with a "/" to separate directories
# since the behavior is inconsistent across different pytest
# versions, see https://stackoverflow.com/a/48814787/771663
output = spack_test('--list', '-k', 'unit_test.py')
assert output.strip() == cmd_test_py


Expand Down
12 changes: 9 additions & 3 deletions lib/spack/spack/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,14 @@ def _skip_if_missing_executables(request):
"""Permits to mark tests with 'require_executables' and skip the
tests if the executables passed as arguments are not found.
"""
if request.node.get_marker('requires_executables'):
required_execs = request.node.get_marker('requires_executables').args
if hasattr(request.node, 'get_marker'):
# TODO: Remove the deprecated API as soon as we drop support for Python 2.6
marker = request.node.get_marker('requires_executables')
else:
marker = request.node.get_closest_marker('requires_executables')

if marker:
required_execs = marker.args
missing_execs = [
x for x in required_execs if spack.util.executable.which(x) is None
]
Expand Down Expand Up @@ -1453,7 +1459,7 @@ def invalid_spec(request):
return request.param


@pytest.fixture("module")
@pytest.fixture(scope='module')
def mock_test_repo(tmpdir_factory):
"""Create an empty repository."""
repo_namespace = 'mock_test_repo'
Expand Down