Repository: pytest-dev/pytest
Pull request: Improve verbose output by wrapping skip/xfail reasons with margin
Fixes #10940.

This functionality is covered by an existing test that only needed minor updates to match new output.  There were a couple of identical lines in the output used for text matching, so I added numbers to disambiguate.

Let me know if there are any suggestions or changes needed.

Problem statement:
Improve progress alignment
First of all, thanks to all contributors, your work is much appreciated!

The right aligned percent progress looks great in the terminal, but sometimes the nice alignment is marred by verbose (`-vv`+) output.  
Here is an example of the current alignment:
```
tests/implementations/test_hdfs.py::TestUPathHDFS::test_fsspec_compat SKIPPED (Possible installation
 problem, error: Unable to load libhdfs: ./libhdfs.so: cannot open shared object file: No such file 
or directory) [ 49%]
tests/test_core.py::TestUPathMock::test_instance_type <- upath/tests/cases.py PASSED          [ 50%]
```

I was thinking that it would be nice to keep the percentages aligned even for multiline output.  It struck me that this could be done by always padding to the rounded up next multiple of terminal width.  I think this would make the output in these situations look much nicer.

Here is an example of how the output could be improved:
```
tests/implementations/test_hdfs.py::TestUPathHDFS::test_fsspec_compat SKIPPED (Possible
installation problem, error: Unable to load libhdfs: ./libhdfs.so: cannot open shared object
file: No such file or directory)                                                              [ 49%]
tests/test_core.py::TestUPathMock::test_instance_type <- upath/tests/cases.py PASSED          [ 50%]
```

I wrote a function using python's built-in text wrap module that should be easy to drop in to accomplish this, along with a test.

```python
import textwrap

_DEFAULT_PROGRESS_MARGIN_SIZE = 7

def _pad_wrap_message(
    msg: str,
    width: int,
    margin: int = _DEFAULT_PROGRESS_MARGIN_SIZE,
    line_sep: str="",
):
    """Wrap and pad message with margin for progress info."""
    wrapped = textwrap.wrap(msg, width - margin, drop_whitespace=True)
    out_lines = [line.ljust(width) for line in wrapped[:-1]]
    out_lines.append(wrapped[-1].ljust(width - margin))
    msg_out = line_sep.join(out_lines)
    return msg_out

def test_pad_wrap_message():
    width = 80
    count = 200
    msg = "b"
    progress = " [100%]"
    margin = len(progress)
    result = _pad_wrap_message(msg * count, width=width, line_sep="\n")
    assert result.count(msg) == count
    lines = (result + progress).split("\n")
    expected_lines = (len(msg) * count) // (width - margin) + 1
    assert len(lines) == expected_lines
    expected_len = expected_lines * (width + 1) - margin - 1
    assert len(result) == expected_len
    for _ in lines:
        assert len(_) == width
        assert _[-len(progress)] == " "  # space before progress column
```

If this approach is acceptable, I can submit a PR.

Hints from issue discussion:
Hi @brl0,

Thanks for writing!

I would be glad to review/merge a PR in that regard, for sure.

Commit message: Improve changelog description

Patch to review:
diff --git a/AUTHORS b/AUTHORS
index dee73514a59..438be759814 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -56,6 +56,7 @@ Benjamin Peterson
 Bernard Pratz
 Bob Ippolito
 Brian Dorsey
+Brian Larsen
 Brian Maissy
 Brian Okken
 Brianna Laugher
diff --git a/changelog/10940.improvement.rst b/changelog/10940.improvement.rst
new file mode 100644
index 00000000000..30700028d64
--- /dev/null
+++ b/changelog/10940.improvement.rst
@@ -0,0 +1 @@
+Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>`, which improves verbose output by performing text wrapping on skip and xfail reasons while leaving a clear margin for progress output.
diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py
index dfc0fa98e18..b7793d398ce 100644
--- a/src/_pytest/terminal.py
+++ b/src/_pytest/terminal.py
@@ -8,6 +8,7 @@
 import inspect
 import platform
 import sys
+import textwrap
 import warnings
 from collections import Counter
 from functools import partial
@@ -426,6 +427,28 @@ def ensure_newline(self) -> None:
             self._tw.line()
             self.currentfspath = None
 
+    def wrap_write(
+        self,
+        content: str,
+        *,
+        flush: bool = False,
+        margin: int = 8,
+        line_sep: str = "\n",
+        **markup: bool,
+    ) -> None:
+        """Wrap message with margin for progress info."""
+        width_of_current_line = self._tw.width_of_current_line
+        wrapped = line_sep.join(
+            textwrap.wrap(
+                " " * width_of_current_line + content,
+                width=self._screen_width - margin,
+                drop_whitespace=True,
+                replace_whitespace=False,
+            ),
+        )
+        wrapped = wrapped[width_of_current_line:]
+        self._tw.write(wrapped, flush=flush, **markup)
+
     def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
         self._tw.write(content, flush=flush, **markup)
 
@@ -572,7 +595,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
                         formatted_reason = f" ({reason})"
 
                     if reason and formatted_reason is not None:
-                        self._tw.write(formatted_reason)
+                        self.wrap_write(formatted_reason)
                 if self._show_progress_info:
                     self._write_progress_information_filling_space()
             else:
diff --git a/testing/test_terminal.py b/testing/test_terminal.py
index 97ce2cb5674..c0acb600608 100644
--- a/testing/test_terminal.py
+++ b/testing/test_terminal.py
@@ -387,13 +387,13 @@ def test_10():
                 pytest.xfail("It's 🕙 o'clock")
 
             @pytest.mark.skip(
-                reason="cannot do foobar because baz is missing due to I don't know what"
+                reason="1 cannot do foobar because baz is missing due to I don't know what"
             )
             def test_long_skip():
                 pass
 
             @pytest.mark.xfail(
-                reason="cannot do foobar because baz is missing due to I don't know what"
+                reason="2 cannot do foobar because baz is missing due to I don't know what"
             )
             def test_long_xfail():
                 print(1 / 0)
@@ -417,8 +417,8 @@ def test_long_xfail():
         result.stdout.fnmatch_lines(
             common_output
             + [
-                "test_verbose_skip_reason.py::test_long_skip SKIPPED (cannot *...) *",
-                "test_verbose_skip_reason.py::test_long_xfail XFAIL (cannot *...) *",
+                "test_verbose_skip_reason.py::test_long_skip SKIPPED (1 cannot *...) *",
+                "test_verbose_skip_reason.py::test_long_xfail XFAIL (2 cannot *...) *",
             ]
         )
 
@@ -428,12 +428,14 @@ def test_long_xfail():
             + [
                 (
                     "test_verbose_skip_reason.py::test_long_skip SKIPPED"
-                    " (cannot do foobar because baz is missing due to I don't know what) *"
+                    " (1 cannot do foobar"
                 ),
+                "because baz is missing due to I don't know what) *",
                 (
                     "test_verbose_skip_reason.py::test_long_xfail XFAIL"
-                    " (cannot do foobar because baz is missing due to I don't know what) *"
+                    " (2 cannot do foobar"
                 ),
+                "because baz is missing due to I don't know what) *",
             ]
         )
 


Code review comments:
diff_hunk: @@ -0,0 +1 @@
+Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>`, which improves verbose output by performing text wrapping on skip and xfail reasons while leaving a clear margin for progress output.
path: changelog/10940.improvement.rst
text: @user1:
Perhaps it would be better to swap the statements, given the last one is more important to users than the addition of the internal method:

```suggestion
Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.

Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.
```