Skip to content

Commit c85eaba

Browse files
committed
fix: multi-line statements no longer confuse branch target descriptions. #1874 #1875
1 parent 73e58fa commit c85eaba

File tree

4 files changed

+43
-10
lines changed

4 files changed

+43
-10
lines changed

CHANGES.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ upgrading your version of coverage.py.
2323
Unreleased
2424
----------
2525

26+
- Fix: some descriptions of missing branches in HTML and LCOV reports were
27+
incorrect when multi-line statements were involved (`issue 1874`_ and `issue
28+
1875`_). These are now fixed.
29+
2630
- Fix: Python 3.14 `defers evaluation of annotations <pep649_>`_ by moving them
2731
into separate code objects. That code is rarely executed, so coverage.py
2832
would mark them as missing, as reported in `issue 1908`_. Now they are
@@ -33,6 +37,8 @@ Unreleased
3337
understand the problem or the solution, but ``git bisect`` helped find it,
3438
and now it's fixed.
3539

40+
.. _issue 1874: https://github.com/nedbat/coveragepy/issues/1874
41+
.. _issue 1875: https://github.com/nedbat/coveragepy/issues/1875
3642
.. _issue 1902: https://github.com/nedbat/coveragepy/issues/1902
3743
.. _issue 1908: https://github.com/nedbat/coveragepy/issues/1908
3844
.. _pep649: https://docs.python.org/3.14/whatsnew/3.14.html#pep-649-deferred-evaluation-of-annotations

coverage/parser.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,6 @@ def __init__(
684684
) -> None:
685685
self.filename = filename
686686
self.root_node = root_node
687-
# TODO: I think this is happening in too many places.
688687
self.statements = {multiline.get(l, l) for l in statements}
689688
self.multiline = multiline
690689

@@ -812,9 +811,10 @@ def line_for_node(self, node: ast.AST) -> TLineNo:
812811
getattr(self, "_line__" + node_name, None),
813812
)
814813
if handler is not None:
815-
return handler(node)
814+
line = handler(node)
816815
else:
817-
return node.lineno # type: ignore[attr-defined, no-any-return]
816+
line = node.lineno # type: ignore[attr-defined]
817+
return self.multiline.get(line, line)
818818

819819
# First lines: _line__*
820820
#
@@ -936,8 +936,7 @@ def process_body(
936936
# the next node.
937937
for body_node in body:
938938
lineno = self.line_for_node(body_node)
939-
first_line = self.multiline.get(lineno, lineno)
940-
if first_line not in self.statements:
939+
if lineno not in self.statements:
941940
maybe_body_node = self.find_non_missing_node(body_node)
942941
if maybe_body_node is None:
943942
continue
@@ -961,8 +960,7 @@ def find_non_missing_node(self, node: ast.AST) -> ast.AST | None:
961960
# means we can avoid a function call in the 99.9999% case of not
962961
# optimizing away statements.
963962
lineno = self.line_for_node(node)
964-
first_line = self.multiline.get(lineno, lineno)
965-
if first_line in self.statements:
963+
if lineno in self.statements:
966964
return node
967965

968966
missing_fn = cast(
@@ -1110,8 +1108,6 @@ def _handle_decorated(self, node: ast.FunctionDef) -> set[ArcStart]:
11101108
# not what we'd think of as the first line in the statement, so map
11111109
# it to the first one.
11121110
assert node.body, f"Oops: {node.body = } in {self.filename}@{node.lineno}"
1113-
body_start = self.line_for_node(node.body[0])
1114-
body_start = self.multiline.get(body_start, body_start)
11151111
# The body is handled in collect_arcs.
11161112
assert last is not None
11171113
return {ArcStart(last)}

coverage/results.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def analysis_from_file_reporter(
3434

3535
if has_arcs:
3636
arc_possibilities_set = file_reporter.arcs()
37-
arcs = data.arcs(filename) or []
37+
arcs: Iterable[TArc] = data.arcs(filename) or []
38+
arcs = file_reporter.translate_arcs(arcs)
3839

3940
# Reduce the set of arcs to the ones that could be branches.
4041
dests = collections.defaultdict(set)

tests/test_lcov.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,3 +558,33 @@ def test_always_raise(self) -> None:
558558
""")
559559
actual_result = self.get_lcov_report_content()
560560
assert expected_result == actual_result
561+
562+
def test_multiline_conditions(self) -> None:
563+
self.make_file("multi.py", """\
564+
def fun(x):
565+
if (
566+
x
567+
):
568+
print("got here")
569+
""")
570+
cov = coverage.Coverage(source=".", branch=True)
571+
self.start_import_stop(cov, "multi")
572+
cov.lcov_report()
573+
lcov = self.get_lcov_report_content()
574+
assert "BRDA:2,0,return from function 'fun',-" in lcov
575+
576+
def test_module_exit(self) -> None:
577+
self.make_file("modexit.py", """\
578+
#! /usr/bin/env python
579+
def foo():
580+
return bar(
581+
)
582+
if "x" == "y": # line 5
583+
foo()
584+
""")
585+
cov = coverage.Coverage(source=".", branch=True)
586+
self.start_import_stop(cov, "modexit")
587+
cov.lcov_report()
588+
lcov = self.get_lcov_report_content()
589+
print(lcov)
590+
assert "BRDA:5,0,exit the module,1" in lcov

0 commit comments

Comments
 (0)