summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2021-05-31 19:37:53 -0400
committerNed Batchelder <ned@nedbatchelder.com>2021-05-31 19:37:53 -0400
commit1157999dc5c6c3aa3129a7cd4bf249fe73598501 (patch)
treecba407bacb180fb8fcb46a1b5eaf7da2e0fd88fb
parent65d9e0edbca0dc893ca70fbf64969e2ddc5a3680 (diff)
downloadpython-coveragepy-git-1157999dc5c6c3aa3129a7cd4bf249fe73598501.tar.gz
fix: --fail-under=100 could report 100 is less than 100.
Use the same rounding rules for the fail-under message that are used for totals everywhere else, so that it won't say: total of 100 is less than fail-under=100
-rw-r--r--CHANGES.rst5
-rw-r--r--coverage/cmdline.py6
-rw-r--r--coverage/results.py24
-rw-r--r--tests/test_process.py16
-rw-r--r--tests/test_results.py12
5 files changed, 53 insertions, 10 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 205ef0ab..b15d6298 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -35,11 +35,16 @@ Unreleased
- Warnings generated by coverage.py are now real Python warnings.
+- Using ``--fail-under=100`` with coverage near 100% could result in the
+ self-contradictory message :code:`total of 100 is less than fail-under=100`.
+ This bug (`issue 1168`_) is now fixed.
+
- The ``COVERAGE_DEBUG_FILE`` environment variable now accepts ``stdout`` and
``stderr`` to write to those destinations.
.. _Django coverage plugin: https://pypi.org/project/django-coverage-plugin/
.. _issue 1150: https://github.com/nedbat/coveragepy/issues/1150
+.. _issue 1168: https://github.com/nedbat/coveragepy/issues/1168
.. _changes_56b1:
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index d1e8f283..697d2960 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -20,7 +20,7 @@ from coverage.data import line_counts
from coverage.debug import info_formatter, info_header, short_stack
from coverage.exceptions import BaseCoverageException, ExceptionDuringRun, NoSource
from coverage.execfile import PyRunner
-from coverage.results import should_fail_under
+from coverage.results import Numbers, should_fail_under
class Opts:
@@ -655,8 +655,8 @@ class CoverageScript:
fail_under = self.coverage.get_option("report:fail_under")
precision = self.coverage.get_option("report:precision")
if should_fail_under(total, fail_under, precision):
- msg = "total of {total:.{p}f} is less than fail-under={fail_under:.{p}f}".format(
- total=total, fail_under=fail_under, p=precision,
+ msg = "total of {total} is less than fail-under={fail_under:.{p}f}".format(
+ total=Numbers.display_covered(total), fail_under=fail_under, p=precision,
)
print("Coverage failure:", msg)
return FAIL_UNDER
diff --git a/coverage/results.py b/coverage/results.py
index c60ccac2..f7331b41 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -219,14 +219,24 @@ class Numbers(SimpleReprMixin):
result in either "0" or "100".
"""
- pc = self.pc_covered
- if 0 < pc < self._near0:
- pc = self._near0
- elif self._near100 < pc < 100:
- pc = self._near100
+ return self.display_covered(self.pc_covered)
+
+ @classmethod
+ def display_covered(cls, pc):
+ """Return a displayable total percentage, as a string.
+
+ Note that "0" is only returned when the value is truly zero, and "100"
+ is only returned when the value is truly 100. Rounding can never
+ result in either "0" or "100".
+
+ """
+ if 0 < pc < cls._near0:
+ pc = cls._near0
+ elif cls._near100 < pc < 100:
+ pc = cls._near100
else:
- pc = round(pc, self._precision)
- return "%.*f" % (self._precision, pc)
+ pc = round(pc, cls._precision)
+ return "%.*f" % (cls._precision, pc)
@classmethod
def pc_str_width(cls):
diff --git a/tests/test_process.py b/tests/test_process.py
index 9c695b2a..04a8a2d5 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -1249,6 +1249,22 @@ class FailUnderTest(CoverageTest):
expected = "Coverage failure: total of 42.86 is less than fail-under=42.88"
assert expected == self.last_line_squeezed(out)
+ def test_report_99p9_is_not_ok(self):
+ # A file with 99.99% coverage:
+ self.make_file("ninety_nine_plus.py", """\
+ a = 1
+ """ + """
+ b = 2
+ """ * 20000 + """
+ if a > 3:
+ c = 4
+ """)
+ self.run_command("coverage run --source=. ninety_nine_plus.py")
+ st, out = self.run_command_status("coverage report --fail-under=100")
+ assert st == 2
+ expected = "Coverage failure: total of 99 is less than fail-under=100"
+ assert expected == self.last_line_squeezed(out)
+
class FailUnderNoFilesTest(CoverageTest):
"""Test that nothing to report results in an error exit status."""
diff --git a/tests/test_results.py b/tests/test_results.py
index 5811b0c2..fa239e92 100644
--- a/tests/test_results.py
+++ b/tests/test_results.py
@@ -68,6 +68,18 @@ class NumbersTest(CoverageTest):
assert n10000.pc_covered_str == "0.0"
Numbers.set_precision(0)
+ @pytest.mark.parametrize("prec, pc, res", [
+ (0, 47.87, "48"),
+ (1, 47.87, "47.9"),
+ (0, 99.995, "99"),
+ (2, 99.99995, "99.99"),
+ ])
+ def test_display_covered(self, prec, pc, res):
+ # Numbers._precision is a global, which is bad.
+ Numbers.set_precision(prec)
+ assert Numbers.display_covered(pc) == res
+ Numbers.set_precision(0)
+
def test_covered_ratio(self):
n = Numbers(n_files=1, n_statements=200, n_missing=47)
assert n.ratio_covered == (153, 200)