Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

testresult: correctly apply verbose word markup and avoid crash #12472

Merged
merged 5 commits into from
Jul 1, 2024
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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @@
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(
pbrezina marked this conversation as resolved.
Show resolved Hide resolved
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

Check warning on line 208 in src/_pytest/reports.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/reports.py#L205-L208

Added lines #L205 - L208 were not covered by tests

fail( # pragma: no cover
"pytest_report_teststatus() hook (from a plugin) returned "
nicoddemus marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1206,10 +1206,10 @@
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(

Check warning on line 1209 in src/_pytest/terminal.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/terminal.py#L1209

Added line #L1209 was not covered by tests
self.config, {_color_for_type["warnings"]: True}
)
markup_word = self._tw.markup(verbose_word, **verbose_markup)

Check warning on line 1212 in src/_pytest/terminal.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/terminal.py#L1212

Added line #L1212 was not covered by tests
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
line = f"{markup_word} {nodeid}"
reason = rep.wasxfail
Expand All @@ -1221,10 +1221,10 @@
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(

Check warning on line 1224 in src/_pytest/terminal.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/terminal.py#L1224

Added line #L1224 was not covered by tests
self.config, {_color_for_type["warnings"]: True}
)
markup_word = self._tw.markup(verbose_word, **verbose_markup)

Check warning on line 1227 in src/_pytest/terminal.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/terminal.py#L1227

Added line #L1227 was not covered by tests
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
line = f"{markup_word} {nodeid}"
reason = rep.wasxfail
Expand All @@ -1237,10 +1237,10 @@
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(

Check warning on line 1240 in src/_pytest/terminal.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/terminal.py#L1240

Added line #L1240 was not covered by tests
self.config, {_color_for_type["warnings"]: True}
)
markup_word = self._tw.markup(verbose_word, **verbose_markup)

Check warning on line 1243 in src/_pytest/terminal.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/terminal.py#L1243

Added line #L1243 was not covered by tests
prefix = "Skipped: "
for num, fspath, lineno, reason in fskips:
if reason.startswith(prefix):
Expand Down Expand Up @@ -1421,8 +1421,10 @@
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
Loading