Skip to content

Commit 39ec880

Browse files
committed
testresult: correctly apply verbose word markup and avoid crash
The following snippet would have resulted in crash on multiple places since `_get_verbose_word` expects only string, not a tuple. ```python @pytest.hookimpl(tryfirst=True) def pytest_report_teststatus(report: pytest.CollectReport | pytest.TestReport, config: pytest.Config): if report.when == "call": return ("error", "A", ("AVC", {"bold": True, "red": True})) return None ``` ``` Traceback (most recent call last): File "/home/pbrezina/workspace/sssd/.venv/bin/pytest", line 8, in <module> sys.exit(console_main()) ^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/config/__init__.py", line 207, in console_main code = main() ^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/config/__init__.py", line 179, in main ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_hooks.py", line 513, in __call__ return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec return self._inner_hookexec(hook_name, methods, kwargs, firstresult) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall raise exception.with_traceback(exception.__traceback__) File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 103, in _multicall res = hook_impl.function(*args) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/main.py", line 333, in pytest_cmdline_main return wrap_session(config, _main) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/main.py", line 321, in wrap_session config.hook.pytest_sessionfinish( File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_hooks.py", line 513, in __call__ return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec return self._inner_hookexec(hook_name, methods, kwargs, firstresult) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall raise exception.with_traceback(exception.__traceback__) File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 122, in _multicall teardown.throw(exception) # type: ignore[union-attr] ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/logging.py", line 872, in pytest_sessionfinish return (yield) ^^^^^ File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 124, in _multicall teardown.send(result) # type: ignore[union-attr] ^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 899, in pytest_sessionfinish self.config.hook.pytest_terminal_summary( File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_hooks.py", line 513, in __call__ return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec return self._inner_hookexec(hook_name, methods, kwargs, firstresult) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall raise exception.with_traceback(exception.__traceback__) File "/home/pbrezina/workspace/sssd/.venv/lib64/python3.11/site-packages/pluggy/_callers.py", line 124, in _multicall teardown.send(result) # type: ignore[union-attr] ^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 923, in pytest_terminal_summary self.short_test_summary() File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 1272, in short_test_summary action(lines) File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 1205, in show_simple line = _get_line_with_reprcrash_message( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/terminal.py", line 1429, in _get_line_with_reprcrash_message word = tw.markup(verbose_word, **word_markup) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/pbrezina/workspace/pytest/src/_pytest/_io/terminalwriter.py", line 114, in markup text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~ TypeError: can only concatenate str (not "tuple") to str ``` Signed-off-by: Pavel Březina <[email protected]>
1 parent 53bf188 commit 39ec880

File tree

4 files changed

+30
-18
lines changed

4 files changed

+30
-18
lines changed

changelog/12472.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed a crash when returning category ``"error"`` or ``"failed"`` with a custom test status from :hook:`pytest_report_teststatus` hook -- :user:`pbrezina`.

src/_pytest/reports.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,17 @@ def head_line(self) -> str | None:
190190
return domain
191191
return None
192192

193-
def _get_verbose_word(self, config: Config):
193+
def _get_verbose_word_with_markup(
194+
self, config: Config, default_markup: Mapping[str, bool]
195+
) -> tuple[str, Mapping[str, bool]]:
194196
_category, _short, verbose = config.hook.pytest_report_teststatus(
195197
report=self, config=config
196198
)
197-
return verbose
199+
200+
if isinstance(verbose, str):
201+
return verbose, default_markup
202+
203+
return verbose[0], verbose[1]
198204

199205
def _to_json(self) -> dict[str, Any]:
200206
"""Return the contents of this report as a dict of builtin entries,

src/_pytest/terminal.py

+13-11
Original file line numberDiff line numberDiff line change
@@ -1206,10 +1206,10 @@ def show_simple(lines: list[str], *, stat: str) -> None:
12061206
def show_xfailed(lines: list[str]) -> None:
12071207
xfailed = self.stats.get("xfailed", [])
12081208
for rep in xfailed:
1209-
verbose_word = rep._get_verbose_word(self.config)
1210-
markup_word = self._tw.markup(
1211-
verbose_word, **{_color_for_type["warnings"]: True}
1209+
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
1210+
self.config, {_color_for_type["warnings"]: True}
12121211
)
1212+
markup_word = self._tw.markup(verbose_word, **verbose_markup)
12131213
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
12141214
line = f"{markup_word} {nodeid}"
12151215
reason = rep.wasxfail
@@ -1221,10 +1221,10 @@ def show_xfailed(lines: list[str]) -> None:
12211221
def show_xpassed(lines: list[str]) -> None:
12221222
xpassed = self.stats.get("xpassed", [])
12231223
for rep in xpassed:
1224-
verbose_word = rep._get_verbose_word(self.config)
1225-
markup_word = self._tw.markup(
1226-
verbose_word, **{_color_for_type["warnings"]: True}
1224+
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
1225+
self.config, {_color_for_type["warnings"]: True}
12271226
)
1227+
markup_word = self._tw.markup(verbose_word, **verbose_markup)
12281228
nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
12291229
line = f"{markup_word} {nodeid}"
12301230
reason = rep.wasxfail
@@ -1237,10 +1237,10 @@ def show_skipped(lines: list[str]) -> None:
12371237
fskips = _folded_skips(self.startpath, skipped) if skipped else []
12381238
if not fskips:
12391239
return
1240-
verbose_word = skipped[0]._get_verbose_word(self.config)
1241-
markup_word = self._tw.markup(
1242-
verbose_word, **{_color_for_type["warnings"]: True}
1240+
verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup(
1241+
self.config, {_color_for_type["warnings"]: True}
12431242
)
1243+
markup_word = self._tw.markup(verbose_word, **verbose_markup)
12441244
prefix = "Skipped: "
12451245
for num, fspath, lineno, reason in fskips:
12461246
if reason.startswith(prefix):
@@ -1421,8 +1421,10 @@ def _get_line_with_reprcrash_message(
14211421
config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool]
14221422
) -> str:
14231423
"""Get summary line for a report, trying to add reprcrash message."""
1424-
verbose_word = rep._get_verbose_word(config)
1425-
word = tw.markup(verbose_word, **word_markup)
1424+
verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
1425+
config, word_markup
1426+
)
1427+
word = tw.markup(verbose_word, **verbose_markup)
14261428
node = _get_node_id_with_markup(tw, config, rep)
14271429

14281430
line = f"{word} {node}"

testing/test_terminal.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -326,16 +326,17 @@ def test_rewrite(self, pytester: Pytester, monkeypatch) -> None:
326326
tr.rewrite("hey", erase=True)
327327
assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ")
328328

329+
@pytest.mark.parametrize("category", ["foo", "failed", "error", "passed"])
329330
def test_report_teststatus_explicit_markup(
330-
self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping
331+
self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping, category: str
331332
) -> None:
332333
"""Test that TerminalReporter handles markup explicitly provided by
333334
a pytest_report_teststatus hook."""
334335
monkeypatch.setenv("PY_COLORS", "1")
335336
pytester.makeconftest(
336-
"""
337+
f"""
337338
def pytest_report_teststatus(report):
338-
return 'foo', 'F', ('FOO', {'red': True})
339+
return {category !r}, 'F', ('FOO', {{'red': True}})
339340
"""
340341
)
341342
pytester.makepyfile(
@@ -344,7 +345,9 @@ def test_foobar():
344345
pass
345346
"""
346347
)
348+
347349
result = pytester.runpytest("-v")
350+
assert not result.stderr.lines
348351
result.stdout.fnmatch_lines(
349352
color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"])
350353
)
@@ -2385,8 +2388,8 @@ def __init__(self):
23852388
self.option = Namespace(verbose=0)
23862389

23872390
class rep:
2388-
def _get_verbose_word(self, *args):
2389-
return mocked_verbose_word
2391+
def _get_verbose_word_with_markup(self, *args):
2392+
return mocked_verbose_word, {}
23902393

23912394
class longrepr:
23922395
class reprcrash:

0 commit comments

Comments
 (0)