Skip to content

Improve Terminal Output Formatting in pytest-cov #678

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

Merged
merged 10 commits into from
Mar 24, 2025
43 changes: 34 additions & 9 deletions src/pytest_cov/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import functools
import os
import random
import shutil
import socket
import sys
import warnings
Expand Down Expand Up @@ -134,15 +135,39 @@ def get_node_desc(platform, version_info):
return 'platform {}, python {}'.format(platform, '{}.{}.{}-{}-{}'.format(*version_info[:5]))

@staticmethod
def sep(stream, s, txt):
def get_width():
# taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L26
width, _ = shutil.get_terminal_size(fallback=(80, 24))
# The Windows get_terminal_size may be bogus, let's sanify a bit.
if width < 40:
width = 80
return width

def sep(self, stream, s, txt):
if hasattr(stream, 'sep'):
stream.sep(s, txt)
else:
sep_total = max((70 - 2 - len(txt)), 2)
sep_len = sep_total // 2
sep_extra = sep_total % 2
out = f'{s * sep_len} {txt} {s * (sep_len + sep_extra)}\n'
stream.write(out)
fullwidth = self.get_width()
# taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L126
# The goal is to have the line be as long as possible
# under the condition that len(line) <= fullwidth.
if sys.platform == 'win32':
# If we print in the last column on windows we are on a
# new line but there is no way to verify/neutralize this
# (we may not know the exact line width).
# So let's be defensive to avoid empty lines in the output.
fullwidth -= 1
N = max((fullwidth - len(txt) - 2) // (2 * len(s)), 1)
fill = s * N
line = f'{fill} {txt} {fill}'
# In some situations there is room for an extra sepchar at the right,
# in particular if we consider that with a sepchar like "_ " the
# trailing space is not important at the end of the line.
if len(line) + len(s.rstrip()) <= fullwidth:
line += s.rstrip()
# (end of terminalwriter borrowed code)
line += '\n\n'
stream.write(line)

@_ensure_topdir
def summary(self, stream):
Expand All @@ -155,15 +180,15 @@ def summary(self, stream):

# Output coverage section header.
if len(self.node_descs) == 1:
self.sep(stream, '-', f"coverage: {''.join(self.node_descs)}")
self.sep(stream, '_', f"coverage: {''.join(self.node_descs)}")
else:
self.sep(stream, '-', 'coverage')
self.sep(stream, '_', 'coverage')
for node_desc in sorted(self.node_descs):
self.sep(stream, ' ', f'{node_desc}')

# Report on any failed workers.
if self.failed_workers:
self.sep(stream, '-', 'coverage: failed workers')
self.sep(stream, '_', 'coverage: failed workers')
stream.write('The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.\n')
for node in self.failed_workers:
stream.write(f'{node.gateway.id}\n')
Expand Down
12 changes: 10 additions & 2 deletions src/pytest_cov/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False)
self._start_path = None
self._disabled = False
self.options = options
self._wrote_heading = False

is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no'
if getattr(options, 'no_cov', False):
Expand Down Expand Up @@ -356,9 +357,15 @@ def pytest_runtestloop(self, session):
# make sure we get the EXIT_TESTSFAILED exit code
compat_session.testsfailed += 1

def write_heading(self, terminalreporter):
if not self._wrote_heading:
terminalreporter.write_sep('=', 'tests coverage')
self._wrote_heading = True

def pytest_terminal_summary(self, terminalreporter):
if self._disabled:
if self.options.no_cov_should_warn:
self.write_heading(terminalreporter)
message = 'Coverage disabled via --no-cov switch!'
terminalreporter.write(f'WARNING: {message}\n', red=True, bold=True)
warnings.warn(CovDisabledWarning(message), stacklevel=1)
Expand All @@ -372,11 +379,12 @@ def pytest_terminal_summary(self, terminalreporter):

report = self.cov_report.getvalue()

# Avoid undesirable new lines when output is disabled with "--cov-report=".
if report:
terminalreporter.write('\n' + report + '\n')
self.write_heading(terminalreporter)
terminalreporter.write(report)

if self.options.cov_fail_under is not None and self.options.cov_fail_under > 0:
self.write_heading(terminalreporter)
failed = self.cov_total < self.options.cov_fail_under
markup = {'red': True, 'bold': True} if failed else {'green': True}
message = '{fail}Required test coverage of {required}% {reached}. ' 'Total coverage: {actual:.2f}%\n'.format(
Expand Down
Loading