diff --git a/changelog/14377.bugfix.rst b/changelog/14377.bugfix.rst new file mode 100644 index 00000000000..0de38dde111 --- /dev/null +++ b/changelog/14377.bugfix.rst @@ -0,0 +1 @@ +Fixed :meth:`Config.get_terminal_writer() ` to fall back gracefully when the ``terminalreporter`` plugin has been unregistered, avoiding an internal ``AssertionError`` in plugin setups that still need assertion rendering. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 47e85df0951..65447566baf 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1175,8 +1175,9 @@ def get_terminal_writer(self) -> TerminalWriter: terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin( "terminalreporter" ) - assert terminalreporter is not None - return terminalreporter._tw + if terminalreporter is not None: + return terminalreporter._tw + return create_terminal_writer(self) def pytest_cmdline_parse( self, pluginmanager: PytestPluginManager, args: list[str] diff --git a/testing/test_config.py b/testing/test_config.py index 296461c12fc..bec13cde2ae 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -13,6 +13,7 @@ from typing import Any import _pytest._code +from _pytest._io import TerminalWriter from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _iter_rewritable_modules from _pytest.config import _strtobool @@ -1719,6 +1720,64 @@ def pytest_sessionstart(session): assert result.ret == 0 +def test_get_terminal_writer_falls_back_without_terminalreporter( + pytester: Pytester, +) -> None: + config = pytester.parseconfigure("--color=no", "--code-highlight=no") + terminalreporter = config.pluginmanager.get_plugin("terminalreporter") + assert terminalreporter is not None + + config.pluginmanager.unregister(terminalreporter) + + writer = config.get_terminal_writer() + assert isinstance(writer, TerminalWriter) + assert writer.hasmarkup is False + assert writer.code_highlight is False + + +def test_assertion_rewriting_works_without_terminalreporter( + pytester: Pytester, +) -> None: + pytester.makeconftest( + """ + import pathlib + import pytest + + @pytest.hookimpl(trylast=True) + def pytest_configure(config): + reporter = config.pluginmanager.get_plugin("terminalreporter") + assert reporter is not None + config.pluginmanager.unregister(reporter) + + def pytest_runtest_logreport(report): + if report.when == "call" and report.failed: + pathlib.Path("report.txt").write_text( + str(report.longrepr), encoding="utf-8" + ) + """ + ) + pytester.makepyfile( + helper=""" + def run(): + assert "actual" == "expected" + """, + test_foo=""" + from helper import run + + def test_foo(): + run() + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.TESTS_FAILED + + report = pytester.path.joinpath("report.txt").read_text(encoding="utf-8") + assert '"actual" == "expected"' in report + assert "helper.py:2: AssertionError" in report + assert "terminalreporter is not None" not in report + + def test_invalid_options_show_extra_information(pytester: Pytester) -> None: """Display extra information when pytest exits due to unrecognized options in the command-line."""