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/9897.feature.rst b/changelog/9897.feature.rst new file mode 100644 index 00000000000..7464067bfcd --- /dev/null +++ b/changelog/9897.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..52767165691 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,19 @@ class InvocationParams: dir: Path """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__( self, pluginmanager: PytestPluginManager, @@ -1308,15 +1322,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: List[str] = self.getini("testpaths") + for path in testpaths: + args.extend(sorted(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..c74eedf5d27 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -244,28 +244,32 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No pytester.makeini( """ [pytest] - testpaths = gui uts + testpaths = */tests """ ) tmp_path = pytester.path - ensure_file(tmp_path / "env" / "test_1.py").write_text("def test_env(): pass") - ensure_file(tmp_path / "gui" / "test_2.py").write_text("def test_gui(): pass") - ensure_file(tmp_path / "uts" / "test_3.py").write_text("def test_uts(): pass") + ensure_file(tmp_path / "a" / "test_1.py").write_text("def test_a(): pass") + ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text( + "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 # are collected 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 # collects the tests - for dirname in ("env", "gui", "uts"): + for dirname in ("a", "b", "c"): items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname)) assert [x.name for x in items] == ["test_%s" % dirname] # changing cwd to each subdirectory and running pytest without # 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)) items, reprec = pytester.inline_genitems() assert [x.name for x in items] == ["test_%s" % dirname] @@ -1212,19 +1216,16 @@ def test_collect_pyargs_with_testpaths( 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 """ - ) ) 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*"])