diff --git a/changelog/13522.bugfix.rst b/changelog/13522.bugfix.rst new file mode 100644 index 00000000000..683304251aa --- /dev/null +++ b/changelog/13522.bugfix.rst @@ -0,0 +1,5 @@ +Fixed :fixture:`pytester` in subprocess mode ignored all :attr`pytester.plugins ` except the first. + +Fixed :fixture:`pytester` in subprocess mode silently ignored non-str :attr:`pytester.plugins `. +Now it errors instead. +If you are affected by this, specify the plugin by name, or switch the affected tests to use :func:`pytester.runpytest_inprocess ` explicitly instead. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 59d2b0befe9..de4e2c8b136 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -682,9 +682,11 @@ def __init__( self._name = name self._path: Path = tmp_path_factory.mktemp(name, numbered=True) #: A list of plugins to use with :py:meth:`parseconfig` and - #: :py:meth:`runpytest`. Initially this is an empty list but plugins can - #: be added to the list. The type of items to add to the list depends on - #: the method using them so refer to them for details. + #: :py:meth:`runpytest`. Initially this is an empty list but plugins can + #: be added to the list. + #: + #: When running in subprocess mode, specify plugins by name (str) - adding + #: plugin objects directly is not supported. self.plugins: list[str | _PluggyPlugin] = [] self._sys_path_snapshot = SysPathsSnapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot() @@ -1229,10 +1231,9 @@ def parseconfig(self, *args: str | os.PathLike[str]) -> Config: """ import _pytest.config - new_args = self._ensure_basetemp(args) - new_args = [str(x) for x in new_args] + new_args = [str(x) for x in self._ensure_basetemp(args)] - config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type] + config = _pytest.config._prepareconfig(new_args, self.plugins) # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) @@ -1494,9 +1495,13 @@ def runpytest_subprocess( __tracebackhide__ = True p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) args = (f"--basetemp={p}", *args) - plugins = [x for x in self.plugins if isinstance(x, str)] - if plugins: - args = ("-p", plugins[0], *args) + for plugin in self.plugins: + if not isinstance(plugin, str): + raise ValueError( + f"Specifying plugins as objects is not supported in pytester subprocess mode; " + f"specify by name instead: {plugin}" + ) + args = ("-p", plugin, *args) args = self._getpytestargs() + args return self.run(*args, timeout=timeout) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 721e8c19d8b..a5ac8a91b8d 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -834,3 +834,25 @@ def test_two(): result.assert_outcomes(passed=1, deselected=1) # If deselected is not passed, it is not checked at all. result.assert_outcomes(passed=1) + + +def test_pytester_subprocess_with_string_plugins(pytester: Pytester) -> None: + """Test that pytester.runpytest_subprocess is OK with named (string) + `.plugins`.""" + pytester.plugins = ["pytester"] + + result = pytester.runpytest_subprocess() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + + +def test_pytester_subprocess_with_non_string_plugins(pytester: Pytester) -> None: + """Test that pytester.runpytest_subprocess fails with a proper error given + non-string `.plugins`.""" + + class MyPlugin: + pass + + pytester.plugins = [MyPlugin()] + + with pytest.raises(ValueError, match="plugins as objects is not supported"): + pytester.runpytest_subprocess()