From faeb16146b811488ebbcbd17ef6f9102314065b2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 11 May 2023 09:22:17 -0300 Subject: [PATCH] Consider testpaths for initial conftests The 'testpaths' option is meant to be identical to execute pytest passing the 'testpaths' directories explicitly. Fix #10987 --- changelog/10987.bugfix.rst | 1 + doc/en/reference/reference.rst | 16 ++++++++++++---- src/_pytest/config/__init__.py | 26 ++++++++++++++++++++++---- testing/test_collection.py | 23 +++++++++++++++++++++++ testing/test_conftest.py | 2 +- 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 changelog/10987.bugfix.rst diff --git a/changelog/10987.bugfix.rst b/changelog/10987.bugfix.rst new file mode 100644 index 000000000..2aafff5f5 --- /dev/null +++ b/changelog/10987.bugfix.rst @@ -0,0 +1 @@ +:confval:`testpaths` is now honored to load root ``conftests``. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 18b2da953..4bda48386 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1713,13 +1713,12 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: testpaths - - 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. 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 test collection and to avoid picking up undesired tests by accident. @@ -1728,8 +1727,17 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] testpaths = testing doc - This tells pytest to only look for tests in ``testing`` and ``doc`` - directories when executing from the root directory. + This configuration means that executing: + + .. code-block:: console + + pytest + + has the same practical effects as executing: + + .. code-block:: console + + pytest testing doc .. confval:: tmp_path_retention_count diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 720f39531..25cf75e98 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -526,7 +526,10 @@ class PytestPluginManager(PluginManager): # Internal API for local conftest plugin handling. # def _set_initial_conftests( - self, namespace: argparse.Namespace, rootpath: Path + self, + namespace: argparse.Namespace, + rootpath: Path, + testpaths_ini: Sequence[str], ) -> None: """Load initial conftest files given a preparsed "namespace". @@ -543,7 +546,7 @@ class PytestPluginManager(PluginManager): ) self._noconftest = namespace.noconftest self._using_pyargs = namespace.pyargs - testpaths = namespace.file_or_dir + testpaths = namespace.file_or_dir + testpaths_ini foundanchor = False for testpath in testpaths: path = str(testpath) @@ -552,7 +555,20 @@ class PytestPluginManager(PluginManager): if i != -1: path = path[:i] anchor = absolutepath(current / path) - if anchor.exists(): # we found some file object + + # On Python 3.7 on Windows, anchor.exists() might raise + # if the anchor contains glob characters (for example "*//tests"), specially + # in the case of the 'testpaths' ini option. + # Using an explicit version check to remove this code later once + # Python 3.7 is dropped. + if sys.version_info[:2] == (3, 7): + try: + anchor_exists = anchor.exists() + except OSError: # pragma: no cover + anchor_exists = False + else: + anchor_exists = anchor.exists() + if anchor_exists: # We found some file object. self._try_load_conftest(anchor, namespace.importmode, rootpath) foundanchor = True if not foundanchor: @@ -1131,7 +1147,9 @@ class Config: @hookimpl(trylast=True) def pytest_load_initial_conftests(self, early_config: "Config") -> None: self.pluginmanager._set_initial_conftests( - early_config.known_args_namespace, rootpath=early_config.rootpath + early_config.known_args_namespace, + rootpath=early_config.rootpath, + testpaths_ini=self.getini("testpaths"), ) def _initini(self, args: Sequence[str]) -> None: diff --git a/testing/test_collection.py b/testing/test_collection.py index d907244d5..f78ae7bea 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1247,6 +1247,29 @@ def test_collect_pyargs_with_testpaths( result.stdout.fnmatch_lines(["*1 passed in*"]) +def test_initial_conftests_with_testpaths(pytester: Pytester) -> None: + """The testpaths ini option should load conftests in those paths as 'initial' (#10987).""" + p = pytester.mkdir("some_path") + p.joinpath("conftest.py").write_text( + textwrap.dedent( + """ + def pytest_sessionstart(session): + raise Exception("pytest_sessionstart hook is successfully run") + """ + ) + ) + pytester.makeini( + """ + [pytest] + testpaths = some_path + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + "INTERNALERROR* Exception: pytest_sessionstart hook is successfully run" + ) + + def test_collect_symlink_file_arg(pytester: Pytester) -> None: """Collect a direct symlink works even if it does not match python_files (#4325).""" real = pytester.makepyfile( diff --git a/testing/test_conftest.py b/testing/test_conftest.py index d2bf860c6..d6abca536 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -35,7 +35,7 @@ def conftest_setinitial( self.importmode = "prepend" namespace = cast(argparse.Namespace, Namespace()) - conftest._set_initial_conftests(namespace, rootpath=Path(args[0])) + conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[]) @pytest.mark.usefixtures("_sys_snapshot")