diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 650dbcc9..009b96f6 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -5,6 +5,7 @@ import functools import os import random +import shutil import socket import sys import warnings @@ -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): @@ -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') diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 916efc8e..011ef704 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -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): @@ -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) @@ -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( diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 8aa2a339..7f3ade38 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -207,7 +207,7 @@ def test_central(pytester, testdir, prop): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -218,7 +218,7 @@ def test_annotate(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage annotated source written next to source', '*10 passed*', ] @@ -233,7 +233,7 @@ def test_annotate_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage annotated source written to dir ' + DEST_DIR, '*10 passed*', ] @@ -251,7 +251,7 @@ def test_html(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir htmlcov', '*10 passed*', ] @@ -269,7 +269,7 @@ def test_html_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir ' + DEST_DIR, '*10 passed*', ] @@ -289,7 +289,7 @@ def test_term_report_does_not_interact_with_html_output(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir ' + DEST_DIR, '*1 passed*', ] @@ -317,7 +317,7 @@ def test_html_configured_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir somewhere', '*10 passed*', ] @@ -335,7 +335,7 @@ def test_xml_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage XML written to file ' + XML_REPORT_NAME, '*10 passed*', ] @@ -351,7 +351,7 @@ def test_json_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage JSON written to file ' + JSON_REPORT_NAME, '*10 passed*', ] @@ -368,7 +368,7 @@ def test_lcov_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, '*10 passed*', ] @@ -490,7 +490,7 @@ def test_central_nonspecific(pytester, testdir, prop): testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) # multi-module coverage report assert any(line.startswith('TOTAL ') for line in result.stdout.lines) @@ -520,7 +520,7 @@ def test_central_coveragerc(pytester, testdir, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'test_central_coveragerc* {prop.result} *', '*10 passed*', ] @@ -557,7 +557,7 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'src[\\/]mod* {prop.result} *', '*10 passed*', ] @@ -599,7 +599,7 @@ def test_foobar(bad): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', '*mod* 100%', '*1 passed*', ] @@ -636,7 +636,7 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -661,7 +661,7 @@ def test_show_missing_coveragerc(pytester, testdir, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Name * Stmts * Miss * Cover * Missing', f'test_show_missing_coveragerc* {prop.result} * 11*', '*10 passed*', @@ -767,7 +767,7 @@ def test_dist_collocated(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -802,7 +802,7 @@ def test_dist_not_collocated(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -838,7 +838,7 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -850,7 +850,7 @@ def test_central_subprocess(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -876,7 +876,7 @@ def test_central_subprocess_change_cwd(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', '*parent_script* 100%*', ] @@ -904,7 +904,7 @@ def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkey result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', ] ) @@ -930,7 +930,7 @@ def test_foo(): result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-branch', script) result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', ] ) @@ -948,7 +948,7 @@ def test_dist_subprocess_collocated(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -988,7 +988,7 @@ def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -1061,7 +1061,7 @@ def test_funcarg(testdir): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_funcarg* 3 * 100%*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_funcarg* 3 * 100%*', '*1 passed*']) assert result.ret == 0 @@ -1111,7 +1111,7 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) assert result.ret == 0 @@ -1157,7 +1157,7 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 @@ -1201,7 +1201,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 @@ -1236,7 +1236,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) assert result.ret == 0 @@ -1273,7 +1273,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) assert result.ret == 0 @@ -1505,7 +1505,7 @@ def test_dist_boxed(testdir): result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--boxed', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1516,7 +1516,7 @@ def test_dist_bare_cov(testdir): result = testdir.runpytest('-v', '--cov', '-n', '1', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1747,7 +1747,7 @@ def test_append_coverage_subprocess(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -1781,7 +1781,7 @@ def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1789,7 +1789,7 @@ def test_double_cov2(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', '--cov', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1804,7 +1804,7 @@ def test_cov_reset_then_set(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-reset', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')