Skip to content

Commit

Permalink
fix: multi-line statements no longer confuse branch target descriptio…
Browse files Browse the repository at this point in the history
…ns. #1874 #1875
  • Loading branch information
nedbat committed Dec 24, 2024
1 parent 73e58fa commit c85eaba
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 10 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ upgrading your version of coverage.py.
Unreleased
----------

- Fix: some descriptions of missing branches in HTML and LCOV reports were
incorrect when multi-line statements were involved (`issue 1874`_ and `issue
1875`_). These are now fixed.

- Fix: Python 3.14 `defers evaluation of annotations <pep649_>`_ by moving them
into separate code objects. That code is rarely executed, so coverage.py
would mark them as missing, as reported in `issue 1908`_. Now they are
Expand All @@ -33,6 +37,8 @@ Unreleased
understand the problem or the solution, but ``git bisect`` helped find it,
and now it's fixed.

.. _issue 1874: https://github.com/nedbat/coveragepy/issues/1874
.. _issue 1875: https://github.com/nedbat/coveragepy/issues/1875
.. _issue 1902: https://github.com/nedbat/coveragepy/issues/1902
.. _issue 1908: https://github.com/nedbat/coveragepy/issues/1908
.. _pep649: https://docs.python.org/3.14/whatsnew/3.14.html#pep-649-deferred-evaluation-of-annotations
Expand Down
14 changes: 5 additions & 9 deletions coverage/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,6 @@ def __init__(
) -> None:
self.filename = filename
self.root_node = root_node
# TODO: I think this is happening in too many places.
self.statements = {multiline.get(l, l) for l in statements}
self.multiline = multiline

Expand Down Expand Up @@ -812,9 +811,10 @@ def line_for_node(self, node: ast.AST) -> TLineNo:
getattr(self, "_line__" + node_name, None),
)
if handler is not None:
return handler(node)
line = handler(node)
else:
return node.lineno # type: ignore[attr-defined, no-any-return]
line = node.lineno # type: ignore[attr-defined]
return self.multiline.get(line, line)

# First lines: _line__*
#
Expand Down Expand Up @@ -936,8 +936,7 @@ def process_body(
# the next node.
for body_node in body:
lineno = self.line_for_node(body_node)
first_line = self.multiline.get(lineno, lineno)
if first_line not in self.statements:
if lineno not in self.statements:
maybe_body_node = self.find_non_missing_node(body_node)
if maybe_body_node is None:
continue
Expand All @@ -961,8 +960,7 @@ def find_non_missing_node(self, node: ast.AST) -> ast.AST | None:
# means we can avoid a function call in the 99.9999% case of not
# optimizing away statements.
lineno = self.line_for_node(node)
first_line = self.multiline.get(lineno, lineno)
if first_line in self.statements:
if lineno in self.statements:
return node

missing_fn = cast(
Expand Down Expand Up @@ -1110,8 +1108,6 @@ def _handle_decorated(self, node: ast.FunctionDef) -> set[ArcStart]:
# not what we'd think of as the first line in the statement, so map
# it to the first one.
assert node.body, f"Oops: {node.body = } in {self.filename}@{node.lineno}"
body_start = self.line_for_node(node.body[0])
body_start = self.multiline.get(body_start, body_start)
# The body is handled in collect_arcs.
assert last is not None
return {ArcStart(last)}
Expand Down
3 changes: 2 additions & 1 deletion coverage/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def analysis_from_file_reporter(

if has_arcs:
arc_possibilities_set = file_reporter.arcs()
arcs = data.arcs(filename) or []
arcs: Iterable[TArc] = data.arcs(filename) or []
arcs = file_reporter.translate_arcs(arcs)

# Reduce the set of arcs to the ones that could be branches.
dests = collections.defaultdict(set)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_lcov.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,3 +558,33 @@ def test_always_raise(self) -> None:
""")
actual_result = self.get_lcov_report_content()
assert expected_result == actual_result

def test_multiline_conditions(self) -> None:
self.make_file("multi.py", """\
def fun(x):
if (
x
):
print("got here")
""")
cov = coverage.Coverage(source=".", branch=True)
self.start_import_stop(cov, "multi")
cov.lcov_report()
lcov = self.get_lcov_report_content()
assert "BRDA:2,0,return from function 'fun',-" in lcov

def test_module_exit(self) -> None:
self.make_file("modexit.py", """\
#! /usr/bin/env python
def foo():
return bar(
)
if "x" == "y": # line 5
foo()
""")
cov = coverage.Coverage(source=".", branch=True)
self.start_import_stop(cov, "modexit")
cov.lcov_report()
lcov = self.get_lcov_report_content()
print(lcov)
assert "BRDA:5,0,exit the module,1" in lcov

0 comments on commit c85eaba

Please sign in to comment.