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..877bb5e55 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -315,12 +315,18 @@ 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] - if start_next in arcs: - to_add.add(prev_next) - to_remove.add(arc) + while start_next in self._with_jump_fixers: to_remove.add(start_next) - return (set(arcs) | to_add) - to_remove + start_next, prev_next = self._with_jump_fixers[start_next] + to_remove.add(prev_next) + to_add.add((start, prev_next[1])) + to_remove.add(arc) + to_remove.add(start_next) + 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'''