From 5c3b9c3a2b8bfdab5a666d8a23171ab8ed91a412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Mon, 1 Jul 2024 17:06:07 +0200 Subject: [PATCH] Merge pull request #12472 from pbrezina/testresult-markup (cherry picked from commit 90459a8fd3e308ba89f4d1a43cb2ee0412db523c) --- changelog/12472.bugfix.rst | 1 + src/_pytest/reports.py | 21 +++++++++++++++++++-- src/_pytest/terminal.py | 24 +++++++++++++----------- testing/test_terminal.py | 13 ++++++++----- 4 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 changelog/12472.bugfix.rst diff --git a/changelog/12472.bugfix.rst b/changelog/12472.bugfix.rst new file mode 100644 index 000000000..f08e9d1f9 --- /dev/null +++ b/changelog/12472.bugfix.rst @@ -0,0 +1 @@ +Fixed a crash when returning category ``"error"`` or ``"failed"`` with a custom test status from :hook:`pytest_report_teststatus` hook -- :user:`pbrezina`. diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 8846d0dc8..30fb7a7b0 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -13,6 +13,7 @@ from typing import Iterator from typing import Literal from typing import Mapping from typing import NoReturn +from typing import Sequence from typing import TYPE_CHECKING from _pytest._code.code import ExceptionChainRepr @@ -30,6 +31,7 @@ from _pytest._io import TerminalWriter from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item +from _pytest.outcomes import fail from _pytest.outcomes import skip @@ -190,11 +192,26 @@ class BaseReport: return domain return None - def _get_verbose_word(self, config: Config): + def _get_verbose_word_with_markup( + self, config: Config, default_markup: Mapping[str, bool] + ) -> tuple[str, Mapping[str, bool]]: _category, _short, verbose = config.hook.pytest_report_teststatus( report=self, config=config ) - return verbose + + if isinstance(verbose, str): + return verbose, default_markup + + if isinstance(verbose, Sequence) and len(verbose) == 2: + word, markup = verbose + if isinstance(word, str) and isinstance(markup, Mapping): + return word, markup + + fail( # pragma: no cover + "pytest_report_teststatus() hook (from a plugin) returned " + f"an invalid verbose value: {verbose!r}.\nExpected either a string " + "or a tuple of (word, markup)." + ) def _to_json(self) -> dict[str, Any]: """Return the contents of this report as a dict of builtin entries, diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b3b8226c7..07630af4b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1179,10 +1179,10 @@ class TerminalReporter: def show_xfailed(lines: list[str]) -> None: xfailed = self.stats.get("xfailed", []) for rep in xfailed: - verbose_word = rep._get_verbose_word(self.config) - markup_word = self._tw.markup( - verbose_word, **{_color_for_type["warnings"]: True} + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) nodeid = _get_node_id_with_markup(self._tw, self.config, rep) line = f"{markup_word} {nodeid}" reason = rep.wasxfail @@ -1194,10 +1194,10 @@ class TerminalReporter: def show_xpassed(lines: list[str]) -> None: xpassed = self.stats.get("xpassed", []) for rep in xpassed: - verbose_word = rep._get_verbose_word(self.config) - markup_word = self._tw.markup( - verbose_word, **{_color_for_type["warnings"]: True} + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) nodeid = _get_node_id_with_markup(self._tw, self.config, rep) line = f"{markup_word} {nodeid}" reason = rep.wasxfail @@ -1210,10 +1210,10 @@ class TerminalReporter: fskips = _folded_skips(self.startpath, skipped) if skipped else [] if not fskips: return - verbose_word = skipped[0]._get_verbose_word(self.config) - markup_word = self._tw.markup( - verbose_word, **{_color_for_type["warnings"]: True} + verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) prefix = "Skipped: " for num, fspath, lineno, reason in fskips: if reason.startswith(prefix): @@ -1394,8 +1394,10 @@ def _get_line_with_reprcrash_message( config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool] ) -> str: """Get summary line for a report, trying to add reprcrash message.""" - verbose_word = rep._get_verbose_word(config) - word = tw.markup(verbose_word, **word_markup) + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + config, word_markup + ) + word = tw.markup(verbose_word, **verbose_markup) node = _get_node_id_with_markup(tw, config, rep) line = f"{word} {node}" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 308a397a4..b4593152d 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -326,16 +326,17 @@ class TestTerminal: tr.rewrite("hey", erase=True) assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ") + @pytest.mark.parametrize("category", ["foo", "failed", "error", "passed"]) def test_report_teststatus_explicit_markup( - self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping + self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping, category: str ) -> None: """Test that TerminalReporter handles markup explicitly provided by a pytest_report_teststatus hook.""" monkeypatch.setenv("PY_COLORS", "1") pytester.makeconftest( - """ + f""" def pytest_report_teststatus(report): - return 'foo', 'F', ('FOO', {'red': True}) + return {category !r}, 'F', ('FOO', {{'red': True}}) """ ) pytester.makepyfile( @@ -344,7 +345,9 @@ class TestTerminal: pass """ ) + result = pytester.runpytest("-v") + assert not result.stderr.lines result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"]) ) @@ -2385,8 +2388,8 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None: self.option = Namespace(verbose=0) class rep: - def _get_verbose_word(self, *args): - return mocked_verbose_word + def _get_verbose_word_with_markup(self, *args): + return mocked_verbose_word, {} class longrepr: class reprcrash: