From d1a916a2161a10c0fd345ce059f9d61f43e35428 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 23 Nov 2024 15:18:29 -0500
Subject: [PATCH] fix: a line that branches nowhere must always raise an
exception
---
CHANGES.rst | 8 +++++-
coverage/html.py | 39 ++++++++++++++++++------------
tests/gold/html/b_branch/b_py.html | 10 ++++----
tests/test_html.py | 11 +++------
4 files changed, 38 insertions(+), 30 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 3b1337d2d..7bddd369f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -23,11 +23,17 @@ upgrading your version of coverage.py.
Unreleased
----------
-- fix: the LCOV report code assumed that a branch line that took no branches
+- Fix: the LCOV report code assumed that a branch line that took no branches
meant that the entire line was unexecuted. This isn't true in a few cases:
the line might always raise an exception, or might have been optimized away.
Fixes `issue 1896`_.
+- Fix: similarly, the HTML report will now explain that a line that jumps to
+ none of its expected destinations must have always raised an exception.
+ Previously, it would say something nonsensical like, "line 4 didn't jump to
+ line 5 because line 4 was never true, and it didn't jump to line 7 because
+ line 4 was always true." This was also shown in `issue 1896`_.
+
.. _issue 1896: https://github.com/nedbat/coveragepy/issues/1896
diff --git a/coverage/html.py b/coverage/html.py
index 20a354b12..2cc68ac1d 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -137,6 +137,7 @@ def data_for_file(self, fr: FileReporter, analysis: Analysis) -> FileData:
contexts_by_lineno = self.data.contexts_by_lineno(analysis.filename)
lines = []
+ branch_stats = analysis.branch_stats()
for lineno, tokens in enumerate(fr.source_token_lines(), start=1):
# Figure out how to mark this line.
@@ -150,12 +151,22 @@ def data_for_file(self, fr: FileReporter, analysis: Analysis) -> FileData:
category = "mis"
elif self.has_arcs and lineno in missing_branch_arcs:
category = "par"
- for b in missing_branch_arcs[lineno]:
- if b < 0:
- short_annotations.append("exit")
- else:
- short_annotations.append(str(b))
- long_annotations.append(fr.missing_arc_description(lineno, b, arcs_executed))
+ mba = missing_branch_arcs[lineno]
+ if len(mba) == branch_stats[lineno][0]:
+ # None of the branches were taken from this line.
+ short_annotations.append("anywhere")
+ long_annotations.append(
+ f"line {lineno} didn't jump anywhere: it always raised an exception."
+ )
+ else:
+ for b in missing_branch_arcs[lineno]:
+ if b < 0:
+ short_annotations.append("exit")
+ else:
+ short_annotations.append(str(b))
+ long_annotations.append(
+ fr.missing_arc_description(lineno, b, arcs_executed)
+ )
elif lineno in analysis.statements:
category = "run"
@@ -486,16 +497,12 @@ def write_html_page(self, ftr: FileToReport) -> None:
if ldata.long_annotations:
longs = ldata.long_annotations
- if len(longs) == 1:
- ldata.annotate_long = longs[0]
- else:
- ldata.annotate_long = "{:d} missed branches: {}".format(
- len(longs),
- ", ".join(
- f"{num:d}) {ann_long}"
- for num, ann_long in enumerate(longs, start=1)
- ),
- )
+ # A line can only have two branch destinations. If there were
+ # two missing, we would have written one as "always raised."
+ assert len(longs) == 1, (
+ f"Had long annotations in {ftr.fr.relative_filename()}: {longs}"
+ )
+ ldata.annotate_long = longs[0]
else:
ldata.annotate_long = None
diff --git a/tests/gold/html/b_branch/b_py.html b/tests/gold/html/b_branch/b_py.html
index 62a849f1b..ff8938e6c 100644
--- a/tests/gold/html/b_branch/b_py.html
+++ b/tests/gold/html/b_branch/b_py.html
@@ -66,8 +66,8 @@