From 3656515c3ba261ddd6d4ae9f83e0b787c8dc896e Mon Sep 17 00:00:00 2001 From: David Szotten Date: Tue, 10 May 2016 14:33:51 +0100 Subject: [PATCH] allow failure reports even with insufficient coverage --- src/pytest_cov/plugin.py | 59 ++++++++++++++++++++++++++++------------ tests/test_pytest_cov.py | 2 +- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index f8434f87..7a474ec8 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -1,5 +1,7 @@ """Coverage plugin for pytest.""" import os +from StringIO import StringIO + import pytest import argparse from coverage.misc import CoverageException @@ -117,6 +119,8 @@ def __init__(self, options, pluginmanager, start=True): self.pid = None self.cov = None self.cov_controller = None + self.cov_report = StringIO() + self.cov_total = None self.failed = False self._started = False self.options = options @@ -180,30 +184,51 @@ def pytest_testnodedown(self, node, error): self.cov_controller.testnodedown(node, error) pytest_testnodedown.optionalhook = True - def pytest_sessionfinish(self, session, exitstatus): - """Delegate to our implementation.""" - self.failed = exitstatus != 0 + def _should_report(self): + return not (self.failed and self.options.no_cov_on_fail) + + def _failed_cov_total(self): + cov_fail_under = self.options.cov_fail_under + return cov_fail_under is not None and self.cov_total < cov_fail_under + + # we need to wrap pytest_runtestloop. by the time pytest_sessionfinish + # runs, it's too late to set testsfailed + @pytest.hookimpl(hookwrapper=True) + def pytest_runtestloop(self, session): + outcome = yield + + self.failed = bool(session.testsfailed) if self.cov_controller is not None: self.cov_controller.finish() - def pytest_terminal_summary(self, terminalreporter): - """Delegate to our implementation.""" - if self.cov_controller is None: - return - if not (self.failed and self.options.no_cov_on_fail): + if self._should_report(): try: - total = self.cov_controller.summary(terminalreporter.writer) + self.cov_total = self.cov_controller.summary(self.cov_report) except CoverageException as exc: - terminalreporter.writer.write('Failed to generate report: %s\n' % exc) - total = 0 - assert total is not None, 'Test coverage should never be `None`' - cov_fail_under = self.options.cov_fail_under - if cov_fail_under is not None and total < cov_fail_under: raise pytest.UsageError( - 'Required test coverage of %d%% not ' - 'reached. Total coverage: %.2f%%\n' - % (self.options.cov_fail_under, total) + 'Failed to generate report: %s\n' % exc ) + assert self.cov_total is not None, 'Test coverage should never be `None`' + if self._failed_cov_total(): + # make sure we get the EXIT_TESTSFAILED exit code + session.testsfailed += 1 + + def pytest_terminal_summary(self, terminalreporter): + if self.cov_controller is None: + return + if self.cov_total is None: + # report generation failed (error raised above) + return + + terminalreporter.write('\n' + self.cov_report.getvalue()) + if self._should_report() and self._failed_cov_total(): + markup = {'red': True, 'bold': True} + msg = ( + 'Required test coverage of %d%% not ' + 'reached. Total coverage: %.2f%%' + % (self.options.cov_fail_under, self.cov_total) + ) + terminalreporter.write(msg + '\n', **markup) def pytest_runtest_setup(self, item): if os.getpid() != self.pid: diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 8e42e7ae..0c065894 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -887,7 +887,7 @@ def test_dist_boxed(testdir): def test_not_started_plugin_does_not_fail(testdir): plugin = pytest_cov.plugin.CovPlugin(None, None, start=False) - plugin.pytest_sessionfinish(None, None) + plugin.pytest_runtestloop(None) plugin.pytest_terminal_summary(None)