Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/12472.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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`.
21 changes: 19 additions & 2 deletions src/_pytest/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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
Expand All @@ -30,6 +31,7 @@
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


Expand Down Expand Up @@ -190,11 +192,26 @@ def head_line(self) -> str | None:
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,
Expand Down
24 changes: 13 additions & 11 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,10 +1179,10 @@ def show_simple(lines: list[str], *, stat: str) -> None:
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
Expand All @@ -1194,10 +1194,10 @@ def show_xfailed(lines: list[str]) -> None:
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
Expand All @@ -1210,10 +1210,10 @@ def show_skipped(lines: list[str]) -> None:
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):
Expand Down Expand Up @@ -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}"
Expand Down
13 changes: 8 additions & 5 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,16 +326,17 @@ def test_rewrite(self, pytester: Pytester, monkeypatch) -> None:
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(
Expand All @@ -344,7 +345,9 @@ def test_foobar():
pass
"""
)

result = pytester.runpytest("-v")
assert not result.stderr.lines
result.stdout.fnmatch_lines(
color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"])
)
Expand Down Expand Up @@ -2385,8 +2388,8 @@ def __init__(self):
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:
Expand Down