diff --git a/AUTHORS b/AUTHORS index 1a8c5306f75..643799edcef 100644 --- a/AUTHORS +++ b/AUTHORS @@ -164,6 +164,7 @@ Jeff Widman Jenni Rinker John Eddie Ayson John Towler +Jon Parise Jon Sonesen Jonas Obrist Jordan Guymon diff --git a/changelog/X.feature.rst b/changelog/X.feature.rst new file mode 100644 index 00000000000..7464067bfcd --- /dev/null +++ b/changelog/X.feature.rst @@ -0,0 +1 @@ +Added shell-style wildcard support to ``testpaths``. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 167c8fed9a3..66dba36d89b 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1761,6 +1761,7 @@ passed multiple times. The expected format is ``name=value``. For example:: 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 executing pytest from the :ref:`rootdir ` directory. + May use shell-style wildcards, including the recursive ``**`` pattern. Useful when all project tests are in a known location to speed up test collection and to avoid picking up undesired tests by accident. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 05abaa8eda6..e234176aa65 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -3,6 +3,7 @@ import collections.abc import copy import enum +import glob import inspect import os import re @@ -899,6 +900,20 @@ class InvocationParams: dir: Path """The directory from which :func:`pytest.main` was invoked.""" + @final + class ArgsSource(enum.IntEnum): + """Indicates the source of the test arguments. + + .. versionadded:: 7.2 + """ + + #: Command line arguments. + ARGS = 0 + #: Invocation directory. + INCOVATION_DIR = 1 + #: 'testpaths' configuration value. + TESTPATHS = 2 + def __init__( self, pluginmanager: PytestPluginManager, @@ -1308,15 +1323,22 @@ def parse(self, args: List[str], addopts: bool = True) -> None: self.hook.pytest_cmdline_preparse(config=self, args=args) self._parser.after_preparse = True # type: ignore try: + source = Config.ArgsSource.ARGS args = self._parser.parse_setoption( args, self.option, namespace=self.option ) if not args: if self.invocation_params.dir == self.rootpath: - args = self.getini("testpaths") + args = [] + source = Config.ArgsSource.TESTPATHS + testpaths = cast(List[str], self.getini("testpaths")) + for path in testpaths: + args.extend(glob.iglob(path, recursive=True)) if not args: + source = Config.ArgsSource.INCOVATION_DIR args = [str(self.invocation_params.dir)] self.args = args + self.args_source = source except PrintHelp: pass diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b4848c48aba..6811fe073fb 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -728,8 +728,8 @@ def pytest_report_header(self, config: Config) -> List[str]: if config.inipath: line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) - testpaths: List[str] = config.getini("testpaths") - if config.invocation_params.dir == config.rootpath and config.args == testpaths: + if config.args_source == Config.ArgsSource.TESTPATHS: + testpaths: List[str] = config.getini("testpaths") line += ", testpaths: {}".format(", ".join(testpaths)) result = [line] diff --git a/testing/test_collection.py b/testing/test_collection.py index 9099ec57fca..fd493119572 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1207,26 +1207,25 @@ def test_1(): def test_collect_pyargs_with_testpaths( pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: - testmod = pytester.mkdir("testmod") - # NOTE: __init__.py is not collected since it does not match python_files. - testmod.joinpath("__init__.py").write_text("def test_func(): pass") - testmod.joinpath("test_file.py").write_text("def test_func(): pass") + for i in range(3): + testpkg = pytester.mkpydir(f"tests{i}") + testmod = pytester.mkdir(str(testpkg.joinpath("testmod"))) + # NOTE: __init__.py is not collected since it does not match python_files. + testmod.joinpath("__init__.py").write_text("def test_func(): pass") + testmod.joinpath("test_file.py").write_text("def test_func(): pass") - root = pytester.mkdir("root") - root.joinpath("pytest.ini").write_text( - textwrap.dedent( - """ + pytester.makeini( + """ [pytest] addopts = --pyargs - testpaths = testmod + testpaths = tests*/testmod """ - ) ) monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep) with monkeypatch.context() as mp: - mp.chdir(root) + mp.chdir(pytester.path) result = pytester.runpytest_subprocess() - result.stdout.fnmatch_lines(["*1 passed in*"]) + result.stdout.fnmatch_lines(["*3 passed in*"]) def test_collect_symlink_file_arg(pytester: Pytester) -> None: