From 3b5bd6b481e32bc525566e3b1e37188f35650e34 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 13 Oct 2024 12:51:56 -0400 Subject: [PATCH] fix: nested context managers shouldn't cause a phantom missing branch #1876 --- CHANGES.rst | 5 +++++ coverage/parser.py | 12 ++++++++++-- tests/test_parser.py | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 594379095..7a8d99ace 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,10 +23,15 @@ upgrading your version of coverage.py. Unreleased ---------- +- Fix: nested context managers could incorrectly be analyzed to flag a missing + branch on the last context manager, as described in `issue 1876`_. This is + now fixed. + - Fix: the missing branch message about not exiting a module had an extra "didn't," as described in `issue 1873`_. This is now fixed. .. _issue 1873: https://github.com/nedbat/coveragepy/issues/1873 +.. _issue 1876: https://github.com/nedbat/coveragepy/issues/1876 .. start-releases diff --git a/coverage/parser.py b/coverage/parser.py index cf454eba1..f8efeea20 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -315,12 +315,20 @@ def fix_with_jumps(self, arcs: Iterable[TArc]) -> set[TArc]: to_add = set() for arc in arcs: if arc in self._with_jump_fixers: + start = arc[0] + to_remove.add(arc) start_next, prev_next = self._with_jump_fixers[arc] + prev = prev_next[0] + while start_next in self._with_jump_fixers: + to_remove.add(start_next) + start_next, prev_next = self._with_jump_fixers[start_next] + to_remove.add(prev_next) if start_next in arcs: - to_add.add(prev_next) + to_add.add((start, prev_next[1])) to_remove.add(arc) to_remove.add(start_next) - return (set(arcs) | to_add) - to_remove + arcs = (set(arcs) | to_add) - to_remove + return arcs @functools.lru_cache def exit_counts(self) -> dict[TLineNo, int]: diff --git a/tests/test_parser.py b/tests/test_parser.py index 62649e237..c6f3e6ec9 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -151,6 +151,24 @@ def bar(self): assert expected_arcs == parser.arcs() assert expected_exits == parser.exit_counts() + def test_nested_context_managers(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1876 + parser = self.parse_text("""\ + a = 1 + with suppress(ValueError): + with suppress(ValueError): + x = 4 + with suppress(ValueError): + x = 6 + with suppress(ValueError): + x = 8 + a = 9 + """) + + one_nine = set(range(1, 10)) + assert parser.statements == one_nine + assert parser.exit_counts() == dict.fromkeys(one_nine, 1) + def test_module_docstrings(self) -> None: parser = self.parse_text("""\ '''The docstring on line 1'''