Add shell-style wildcard support to 'testpaths' (#9897)
This is especially useful for large repositories (e.g. monorepos) that use a hierarchical file system organization for nested test paths. src/*/tests The implementation uses the standard `glob` module to perform wildcard expansion in Config.parse(). The related logic that determines whether or not to include 'testpaths' in the terminal header was previously relying on a weak heuristic: if Config.args matched 'testpaths', then its value was printed. That generally worked, but it could also print when the user explicitly used the same arguments on the command-line as listed in 'testpaths'. Not a big deal, but it shows that the check was logically incorrect. Now that 'testpaths' can contain wildcards, it's no longer possible to perform this simple comparison, so this change also introduces a public Config.ArgSource enum and Config.args_source attribute that explicitly names the "source" of the arguments: the command line, the invocation directory, or the 'testdata' configuration value.
This commit is contained in:
parent
611b579d21
commit
8ac6dce2c7
1
AUTHORS
1
AUTHORS
|
@ -165,6 +165,7 @@ Jeff Widman
|
||||||
Jenni Rinker
|
Jenni Rinker
|
||||||
John Eddie Ayson
|
John Eddie Ayson
|
||||||
John Towler
|
John Towler
|
||||||
|
Jon Parise
|
||||||
Jon Sonesen
|
Jon Sonesen
|
||||||
Jonas Obrist
|
Jonas Obrist
|
||||||
Jordan Guymon
|
Jordan Guymon
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added shell-style wildcard support to ``testpaths``.
|
|
@ -1699,6 +1699,8 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
Sets list of directories that should be searched for tests when
|
Sets list of directories that should be searched for tests when
|
||||||
no specific directories, files or test ids are given in the command line when
|
no specific directories, files or test ids are given in the command line when
|
||||||
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
||||||
|
File system paths may use shell-style wildcards, including the recursive
|
||||||
|
``**`` pattern.
|
||||||
Useful when all project tests are in a known location to speed up
|
Useful when all project tests are in a known location to speed up
|
||||||
test collection and to avoid picking up undesired tests by accident.
|
test collection and to avoid picking up undesired tests by accident.
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import argparse
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import copy
|
import copy
|
||||||
import enum
|
import enum
|
||||||
|
import glob
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -899,6 +900,19 @@ class Config:
|
||||||
dir: Path
|
dir: Path
|
||||||
"""The directory from which :func:`pytest.main` was invoked."""
|
"""The directory from which :func:`pytest.main` was invoked."""
|
||||||
|
|
||||||
|
class ArgsSource(enum.Enum):
|
||||||
|
"""Indicates the source of the test arguments.
|
||||||
|
|
||||||
|
.. versionadded:: 7.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Command line arguments.
|
||||||
|
ARGS = enum.auto()
|
||||||
|
#: Invocation directory.
|
||||||
|
INCOVATION_DIR = enum.auto()
|
||||||
|
#: 'testpaths' configuration value.
|
||||||
|
TESTPATHS = enum.auto()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pluginmanager: PytestPluginManager,
|
pluginmanager: PytestPluginManager,
|
||||||
|
@ -1308,15 +1322,25 @@ class Config:
|
||||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||||
self._parser.after_preparse = True # type: ignore
|
self._parser.after_preparse = True # type: ignore
|
||||||
try:
|
try:
|
||||||
|
source = Config.ArgsSource.ARGS
|
||||||
args = self._parser.parse_setoption(
|
args = self._parser.parse_setoption(
|
||||||
args, self.option, namespace=self.option
|
args, self.option, namespace=self.option
|
||||||
)
|
)
|
||||||
if not args:
|
if not args:
|
||||||
if self.invocation_params.dir == self.rootpath:
|
if self.invocation_params.dir == self.rootpath:
|
||||||
args = self.getini("testpaths")
|
source = Config.ArgsSource.TESTPATHS
|
||||||
|
testpaths: List[str] = self.getini("testpaths")
|
||||||
|
if self.known_args_namespace.pyargs:
|
||||||
|
args = testpaths
|
||||||
|
else:
|
||||||
|
args = []
|
||||||
|
for path in testpaths:
|
||||||
|
args.extend(sorted(glob.iglob(path, recursive=True)))
|
||||||
if not args:
|
if not args:
|
||||||
|
source = Config.ArgsSource.INCOVATION_DIR
|
||||||
args = [str(self.invocation_params.dir)]
|
args = [str(self.invocation_params.dir)]
|
||||||
self.args = args
|
self.args = args
|
||||||
|
self.args_source = source
|
||||||
except PrintHelp:
|
except PrintHelp:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -730,8 +730,8 @@ class TerminalReporter:
|
||||||
if config.inipath:
|
if config.inipath:
|
||||||
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
|
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
|
||||||
|
|
||||||
|
if config.args_source == Config.ArgsSource.TESTPATHS:
|
||||||
testpaths: List[str] = config.getini("testpaths")
|
testpaths: List[str] = config.getini("testpaths")
|
||||||
if config.invocation_params.dir == config.rootpath and config.args == testpaths:
|
|
||||||
line += ", testpaths: {}".format(", ".join(testpaths))
|
line += ", testpaths: {}".format(", ".join(testpaths))
|
||||||
|
|
||||||
result = [line]
|
result = [line]
|
||||||
|
|
|
@ -244,28 +244,32 @@ class TestCollectFS:
|
||||||
pytester.makeini(
|
pytester.makeini(
|
||||||
"""
|
"""
|
||||||
[pytest]
|
[pytest]
|
||||||
testpaths = gui uts
|
testpaths = */tests
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
tmp_path = pytester.path
|
tmp_path = pytester.path
|
||||||
ensure_file(tmp_path / "env" / "test_1.py").write_text("def test_env(): pass")
|
ensure_file(tmp_path / "a" / "test_1.py").write_text("def test_a(): pass")
|
||||||
ensure_file(tmp_path / "gui" / "test_2.py").write_text("def test_gui(): pass")
|
ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text(
|
||||||
ensure_file(tmp_path / "uts" / "test_3.py").write_text("def test_uts(): pass")
|
"def test_b(): pass"
|
||||||
|
)
|
||||||
|
ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text(
|
||||||
|
"def test_c(): pass"
|
||||||
|
)
|
||||||
|
|
||||||
# executing from rootdir only tests from `testpaths` directories
|
# executing from rootdir only tests from `testpaths` directories
|
||||||
# are collected
|
# are collected
|
||||||
items, reprec = pytester.inline_genitems("-v")
|
items, reprec = pytester.inline_genitems("-v")
|
||||||
assert [x.name for x in items] == ["test_gui", "test_uts"]
|
assert [x.name for x in items] == ["test_b", "test_c"]
|
||||||
|
|
||||||
# check that explicitly passing directories in the command-line
|
# check that explicitly passing directories in the command-line
|
||||||
# collects the tests
|
# collects the tests
|
||||||
for dirname in ("env", "gui", "uts"):
|
for dirname in ("a", "b", "c"):
|
||||||
items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname))
|
items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname))
|
||||||
assert [x.name for x in items] == ["test_%s" % dirname]
|
assert [x.name for x in items] == ["test_%s" % dirname]
|
||||||
|
|
||||||
# changing cwd to each subdirectory and running pytest without
|
# changing cwd to each subdirectory and running pytest without
|
||||||
# arguments collects the tests in that directory normally
|
# arguments collects the tests in that directory normally
|
||||||
for dirname in ("env", "gui", "uts"):
|
for dirname in ("a", "b", "c"):
|
||||||
monkeypatch.chdir(pytester.path.joinpath(dirname))
|
monkeypatch.chdir(pytester.path.joinpath(dirname))
|
||||||
items, reprec = pytester.inline_genitems()
|
items, reprec = pytester.inline_genitems()
|
||||||
assert [x.name for x in items] == ["test_%s" % dirname]
|
assert [x.name for x in items] == ["test_%s" % dirname]
|
||||||
|
|
Loading…
Reference in New Issue