Skip to content

Commit 8ac6dce

Browse files
authored
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.
1 parent 611b579 commit 8ac6dce

File tree

6 files changed

+42
-10
lines changed

6 files changed

+42
-10
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ Jeff Widman
165165
Jenni Rinker
166166
John Eddie Ayson
167167
John Towler
168+
Jon Parise
168169
Jon Sonesen
169170
Jonas Obrist
170171
Jordan Guymon

changelog/9897.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added shell-style wildcard support to ``testpaths``.

doc/en/reference/reference.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,8 @@ passed multiple times. The expected format is ``name=value``. For example::
16991699
Sets list of directories that should be searched for tests when
17001700
no specific directories, files or test ids are given in the command line when
17011701
executing pytest from the :ref:`rootdir <rootdir>` directory.
1702+
File system paths may use shell-style wildcards, including the recursive
1703+
``**`` pattern.
17021704
Useful when all project tests are in a known location to speed up
17031705
test collection and to avoid picking up undesired tests by accident.
17041706

src/_pytest/config/__init__.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import collections.abc
44
import copy
55
import enum
6+
import glob
67
import inspect
78
import os
89
import re
@@ -899,6 +900,19 @@ class InvocationParams:
899900
dir: Path
900901
"""The directory from which :func:`pytest.main` was invoked."""
901902

903+
class ArgsSource(enum.Enum):
904+
"""Indicates the source of the test arguments.
905+
906+
.. versionadded:: 7.2
907+
"""
908+
909+
#: Command line arguments.
910+
ARGS = enum.auto()
911+
#: Invocation directory.
912+
INCOVATION_DIR = enum.auto()
913+
#: 'testpaths' configuration value.
914+
TESTPATHS = enum.auto()
915+
902916
def __init__(
903917
self,
904918
pluginmanager: PytestPluginManager,
@@ -1308,15 +1322,25 @@ def parse(self, args: List[str], addopts: bool = True) -> None:
13081322
self.hook.pytest_cmdline_preparse(config=self, args=args)
13091323
self._parser.after_preparse = True # type: ignore
13101324
try:
1325+
source = Config.ArgsSource.ARGS
13111326
args = self._parser.parse_setoption(
13121327
args, self.option, namespace=self.option
13131328
)
13141329
if not args:
13151330
if self.invocation_params.dir == self.rootpath:
1316-
args = self.getini("testpaths")
1331+
source = Config.ArgsSource.TESTPATHS
1332+
testpaths: List[str] = self.getini("testpaths")
1333+
if self.known_args_namespace.pyargs:
1334+
args = testpaths
1335+
else:
1336+
args = []
1337+
for path in testpaths:
1338+
args.extend(sorted(glob.iglob(path, recursive=True)))
13171339
if not args:
1340+
source = Config.ArgsSource.INCOVATION_DIR
13181341
args = [str(self.invocation_params.dir)]
13191342
self.args = args
1343+
self.args_source = source
13201344
except PrintHelp:
13211345
pass
13221346

src/_pytest/terminal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -730,8 +730,8 @@ def pytest_report_header(self, config: Config) -> List[str]:
730730
if config.inipath:
731731
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
732732

733-
testpaths: List[str] = config.getini("testpaths")
734-
if config.invocation_params.dir == config.rootpath and config.args == testpaths:
733+
if config.args_source == Config.ArgsSource.TESTPATHS:
734+
testpaths: List[str] = config.getini("testpaths")
735735
line += ", testpaths: {}".format(", ".join(testpaths))
736736

737737
result = [line]

testing/test_collection.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,28 +244,32 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No
244244
pytester.makeini(
245245
"""
246246
[pytest]
247-
testpaths = gui uts
247+
testpaths = */tests
248248
"""
249249
)
250250
tmp_path = pytester.path
251-
ensure_file(tmp_path / "env" / "test_1.py").write_text("def test_env(): pass")
252-
ensure_file(tmp_path / "gui" / "test_2.py").write_text("def test_gui(): pass")
253-
ensure_file(tmp_path / "uts" / "test_3.py").write_text("def test_uts(): pass")
251+
ensure_file(tmp_path / "a" / "test_1.py").write_text("def test_a(): pass")
252+
ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text(
253+
"def test_b(): pass"
254+
)
255+
ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text(
256+
"def test_c(): pass"
257+
)
254258

255259
# executing from rootdir only tests from `testpaths` directories
256260
# are collected
257261
items, reprec = pytester.inline_genitems("-v")
258-
assert [x.name for x in items] == ["test_gui", "test_uts"]
262+
assert [x.name for x in items] == ["test_b", "test_c"]
259263

260264
# check that explicitly passing directories in the command-line
261265
# collects the tests
262-
for dirname in ("env", "gui", "uts"):
266+
for dirname in ("a", "b", "c"):
263267
items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname))
264268
assert [x.name for x in items] == ["test_%s" % dirname]
265269

266270
# changing cwd to each subdirectory and running pytest without
267271
# arguments collects the tests in that directory normally
268-
for dirname in ("env", "gui", "uts"):
272+
for dirname in ("a", "b", "c"):
269273
monkeypatch.chdir(pytester.path.joinpath(dirname))
270274
items, reprec = pytester.inline_genitems()
271275
assert [x.name for x in items] == ["test_%s" % dirname]

0 commit comments

Comments
 (0)