diff --git a/changelog/2044.improvement.rst b/changelog/2044.improvement.rst new file mode 100644 index 000000000..c9e47c3f6 --- /dev/null +++ b/changelog/2044.improvement.rst @@ -0,0 +1 @@ +Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS". diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 7d2943dd0..2e68e2575 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -13,6 +13,7 @@ from functools import partial from pathlib import Path from typing import Any from typing import Callable +from typing import cast from typing import Dict from typing import Generator from typing import List @@ -545,6 +546,16 @@ class TerminalReporter: line = self._locationline(rep.nodeid, *rep.location) if not running_xdist: self.write_ensure_prefix(line, word, **markup) + if rep.skipped or hasattr(report, "wasxfail"): + available_width = ( + (self._tw.fullwidth - self._tw.width_of_current_line) + - len(" [100%]") + - 1 + ) + reason = _get_raw_skip_reason(rep) + reason_ = _format_trimmed(" ({})", reason, available_width) + if reason_ is not None: + self._tw.write(reason_) if self._show_progress_info: self._write_progress_information_filling_space() else: @@ -1249,6 +1260,31 @@ def _get_pos(config: Config, rep: BaseReport): return nodeid +def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str]: + """Format msg into format, ellipsizing it if doesn't fit in available_width. + + Returns None if even the ellipsis can't fit. + """ + # Only use the first line. + i = msg.find("\n") + if i != -1: + msg = msg[:i] + + ellipsis = "..." + format_width = wcswidth(format.format("")) + if format_width + len(ellipsis) > available_width: + return None + + if format_width + wcswidth(msg) > available_width: + available_width -= len(ellipsis) + msg = msg[:available_width] + while format_width + wcswidth(msg) > available_width: + msg = msg[:-1] + msg += ellipsis + + return format.format(msg) + + def _get_line_with_reprcrash_message( config: Config, rep: BaseReport, termwidth: int ) -> str: @@ -1257,11 +1293,7 @@ def _get_line_with_reprcrash_message( pos = _get_pos(config, rep) line = f"{verbose_word} {pos}" - len_line = wcswidth(line) - ellipsis, len_ellipsis = "...", 3 - if len_line > termwidth - len_ellipsis: - # No space for an additional message. - return line + line_width = wcswidth(line) try: # Type ignored intentionally -- possible AttributeError expected. @@ -1269,22 +1301,11 @@ def _get_line_with_reprcrash_message( except AttributeError: pass else: - # Only use the first line. - i = msg.find("\n") - if i != -1: - msg = msg[:i] - len_msg = wcswidth(msg) + available_width = termwidth - line_width + msg = _format_trimmed(" - {}", msg, available_width) + if msg is not None: + line += msg - sep, len_sep = " - ", 3 - max_len_msg = termwidth - len_line - len_sep - if max_len_msg >= len_ellipsis: - if len_msg > max_len_msg: - max_len_msg -= len_ellipsis - msg = msg[:max_len_msg] - while wcswidth(msg) > max_len_msg: - msg = msg[:-1] - msg += ellipsis - line += sep + msg return line @@ -1361,3 +1382,22 @@ def format_session_duration(seconds: float) -> str: else: dt = datetime.timedelta(seconds=int(seconds)) return f"{seconds:.2f}s ({dt})" + + +def _get_raw_skip_reason(report: TestReport) -> str: + """Get the reason string of a skip/xfail/xpass test report. + + The string is just the part given by the user. + """ + if hasattr(report, "wasxfail"): + reason = cast(str, report.wasxfail) + if reason.startswith("reason: "): + reason = reason[len("reason: ") :] + return reason + else: + assert report.skipped + assert isinstance(report.longrepr, tuple) + _, _, reason = report.longrepr + if reason.startswith("Skipped: "): + reason = reason[len("Skipped: ") :] + return reason diff --git a/testing/test_terminal.py b/testing/test_terminal.py index a4d22d2aa..fdd4301f9 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -5,6 +5,7 @@ import sys import textwrap from io import StringIO from pathlib import Path +from types import SimpleNamespace from typing import cast from typing import Dict from typing import List @@ -23,8 +24,11 @@ from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester from _pytest.reports import BaseReport from _pytest.reports import CollectReport +from _pytest.reports import TestReport from _pytest.terminal import _folded_skips +from _pytest.terminal import _format_trimmed from _pytest.terminal import _get_line_with_reprcrash_message +from _pytest.terminal import _get_raw_skip_reason from _pytest.terminal import _plugin_nameversions from _pytest.terminal import getreportopt from _pytest.terminal import TerminalReporter @@ -342,6 +346,33 @@ class TestTerminal: color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"]) ) + def test_verbose_skip_reason(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.skip(reason="123") + def test_1(): + pass + + @pytest.mark.xfail(reason="456") + def test_2(): + pass + + @pytest.mark.xfail(reason="789") + def test_3(): + assert False + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "test_verbose_skip_reason.py::test_1 SKIPPED (123) *", + "test_verbose_skip_reason.py::test_2 XPASS (456) *", + "test_verbose_skip_reason.py::test_3 XFAIL (789) *", + ] + ) + class TestCollectonly: def test_collectonly_basic(self, pytester: Pytester) -> None: @@ -2345,3 +2376,27 @@ class TestCodeHighlight: ] ) ) + + +def test_raw_skip_reason_skipped() -> None: + report = SimpleNamespace() + report.skipped = True + report.longrepr = ("xyz", 3, "Skipped: Just so") + + reason = _get_raw_skip_reason(cast(TestReport, report)) + assert reason == "Just so" + + +def test_raw_skip_reason_xfail() -> None: + report = SimpleNamespace() + report.wasxfail = "reason: To everything there is a season" + + reason = _get_raw_skip_reason(cast(TestReport, report)) + assert reason == "To everything there is a season" + + +def test_format_trimmed() -> None: + msg = "unconditional skip" + + assert _format_trimmed(" ({}) ", msg, len(msg) + 4) == " (unconditional skip) " + assert _format_trimmed(" ({}) ", msg, len(msg) + 3) == " (unconditional ...) "