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 <pbrezina@redhat.com>
This commit is contained in:
parent
dbf7dee8c8
commit
41ca4dd44e
|
|
@ -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`.
|
||||
|
|
@ -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(
|
||||
"pytest_report_teststatus() hook (from a plugin) returned "
|
||||
"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,
|
||||
|
|
|
|||
|
|
@ -1206,10 +1206,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
|
||||
|
|
@ -1221,10 +1221,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
|
||||
|
|
@ -1237,10 +1237,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):
|
||||
|
|
@ -1421,8 +1421,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}"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue