Merge pull request #12472 from pbrezina/testresult-markup

(cherry picked from commit 90459a8fd3)
This commit is contained in:
Sviatoslav Sydorenko (Святослав Сидоренко) 2024-07-01 17:06:07 +02:00 committed by patchback[bot]
parent d4d4ca3213
commit 5c3b9c3a2b
4 changed files with 41 additions and 18 deletions

View File

@ -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`.

View File

@ -13,6 +13,7 @@ from typing import Iterator
from typing import Literal from typing import Literal
from typing import Mapping from typing import Mapping
from typing import NoReturn from typing import NoReturn
from typing import Sequence
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionChainRepr
@ -30,6 +31,7 @@ from _pytest._io import TerminalWriter
from _pytest.config import Config from _pytest.config import Config
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.outcomes import fail
from _pytest.outcomes import skip from _pytest.outcomes import skip
@ -190,11 +192,26 @@ class BaseReport:
return domain return domain
return None 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( _category, _short, verbose = config.hook.pytest_report_teststatus(
report=self, config=config 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]: def _to_json(self) -> dict[str, Any]:
"""Return the contents of this report as a dict of builtin entries, """Return the contents of this report as a dict of builtin entries,

View File

@ -1179,10 +1179,10 @@ class TerminalReporter:
def show_xfailed(lines: list[str]) -> None: def show_xfailed(lines: list[str]) -> None:
xfailed = self.stats.get("xfailed", []) xfailed = self.stats.get("xfailed", [])
for rep in xfailed: for rep in xfailed:
verbose_word = rep._get_verbose_word(self.config) verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
markup_word = self._tw.markup( self.config, {_color_for_type["warnings"]: True}
verbose_word, **{_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) nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
line = f"{markup_word} {nodeid}" line = f"{markup_word} {nodeid}"
reason = rep.wasxfail reason = rep.wasxfail
@ -1194,10 +1194,10 @@ class TerminalReporter:
def show_xpassed(lines: list[str]) -> None: def show_xpassed(lines: list[str]) -> None:
xpassed = self.stats.get("xpassed", []) xpassed = self.stats.get("xpassed", [])
for rep in xpassed: for rep in xpassed:
verbose_word = rep._get_verbose_word(self.config) verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
markup_word = self._tw.markup( self.config, {_color_for_type["warnings"]: True}
verbose_word, **{_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) nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
line = f"{markup_word} {nodeid}" line = f"{markup_word} {nodeid}"
reason = rep.wasxfail reason = rep.wasxfail
@ -1210,10 +1210,10 @@ class TerminalReporter:
fskips = _folded_skips(self.startpath, skipped) if skipped else [] fskips = _folded_skips(self.startpath, skipped) if skipped else []
if not fskips: if not fskips:
return return
verbose_word = skipped[0]._get_verbose_word(self.config) verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup(
markup_word = self._tw.markup( self.config, {_color_for_type["warnings"]: True}
verbose_word, **{_color_for_type["warnings"]: True}
) )
markup_word = self._tw.markup(verbose_word, **verbose_markup)
prefix = "Skipped: " prefix = "Skipped: "
for num, fspath, lineno, reason in fskips: for num, fspath, lineno, reason in fskips:
if reason.startswith(prefix): 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] config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool]
) -> str: ) -> str:
"""Get summary line for a report, trying to add reprcrash message.""" """Get summary line for a report, trying to add reprcrash message."""
verbose_word = rep._get_verbose_word(config) verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
word = tw.markup(verbose_word, **word_markup) config, word_markup
)
word = tw.markup(verbose_word, **verbose_markup)
node = _get_node_id_with_markup(tw, config, rep) node = _get_node_id_with_markup(tw, config, rep)
line = f"{word} {node}" line = f"{word} {node}"

View File

@ -326,16 +326,17 @@ class TestTerminal:
tr.rewrite("hey", erase=True) tr.rewrite("hey", erase=True)
assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ") assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ")
@pytest.mark.parametrize("category", ["foo", "failed", "error", "passed"])
def test_report_teststatus_explicit_markup( def test_report_teststatus_explicit_markup(
self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping, category: str
) -> None: ) -> None:
"""Test that TerminalReporter handles markup explicitly provided by """Test that TerminalReporter handles markup explicitly provided by
a pytest_report_teststatus hook.""" a pytest_report_teststatus hook."""
monkeypatch.setenv("PY_COLORS", "1") monkeypatch.setenv("PY_COLORS", "1")
pytester.makeconftest( pytester.makeconftest(
""" f"""
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
return 'foo', 'F', ('FOO', {'red': True}) return {category !r}, 'F', ('FOO', {{'red': True}})
""" """
) )
pytester.makepyfile( pytester.makepyfile(
@ -344,7 +345,9 @@ class TestTerminal:
pass pass
""" """
) )
result = pytester.runpytest("-v") result = pytester.runpytest("-v")
assert not result.stderr.lines
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"]) 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) self.option = Namespace(verbose=0)
class rep: class rep:
def _get_verbose_word(self, *args): def _get_verbose_word_with_markup(self, *args):
return mocked_verbose_word return mocked_verbose_word, {}
class longrepr: class longrepr:
class reprcrash: class reprcrash: