summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2018-10-05 10:39:28 -0400
committerNed Batchelder <ned@nedbatchelder.com>2018-10-06 10:25:29 -0400
commit04ff188349df84f73167108314e9698059830279 (patch)
tree679969c306537f5becab63ec9edc6a00a459098c
parentcf7e8717d73e638d92838f8534712351dda9e0f1 (diff)
downloadpython-coveragepy-git-04ff188349df84f73167108314e9698059830279.tar.gz
Finally jumps back to exiting lines
In Python 3.8, when a finally clause is run because a line in the try block is exiting the block, the exiting line is visited again after the finally block.
-rw-r--r--coverage/env.py10
-rw-r--r--coverage/parser.py47
-rw-r--r--tests/test_arcs.py70
-rw-r--r--tests/test_parser.py69
4 files changed, 151 insertions, 45 deletions
diff --git a/coverage/env.py b/coverage/env.py
index e35d026b..aa8bb8f6 100644
--- a/coverage/env.py
+++ b/coverage/env.py
@@ -24,6 +24,16 @@ PYVERSION = sys.version_info
PY2 = PYVERSION < (3, 0)
PY3 = PYVERSION >= (3, 0)
+# Python behavior
+class PYBEHAVIOR(object):
+ """Flags indicating this Python's behavior."""
+
+ # When a break/continue/return statement in a try block jumps to a finally
+ # block, does the finally block do the break/continue/return (pre-3.8), or
+ # does the finally jump back to the break/continue/return (3.8) to do the
+ # work?
+ finally_jumps_back = (PYVERSION >= (3, 8))
+
# Coverage.py specifics.
# Are we using the C-implemented trace function?
diff --git a/coverage/parser.py b/coverage/parser.py
index 2eae4eb5..5ffcad8c 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -505,6 +505,10 @@ class NodeList(object):
self.lineno = body[0].lineno
+# TODO: some add_arcs methods here don't add arcs, they return them. Rename them.
+# TODO: the cause messages have too many commas.
+# TODO: Shouldn't the cause messages join with "and" instead of "or"?
+
class AstArcAnalyzer(object):
"""Analyze source text with an AST to find executable code paths."""
@@ -546,6 +550,7 @@ class AstArcAnalyzer(object):
if code_object_handler is not None:
code_object_handler(node)
+ @contract(start=int, end=int)
def add_arc(self, start, end, smsg=None, emsg=None):
"""Add an arc, including message fragments to use if it is missing."""
if self.debug: # pragma: debugging
@@ -970,21 +975,45 @@ class AstArcAnalyzer(object):
final_exits = self.add_body_arcs(node.finalbody, prev_starts=final_from)
if try_block.break_from:
- self.process_break_exits(
- self._combine_finally_starts(try_block.break_from, final_exits)
- )
+ if env.PYBEHAVIOR.finally_jumps_back:
+ for break_line in try_block.break_from:
+ lineno = break_line.lineno
+ cause = break_line.cause.format(lineno=lineno)
+ for final_exit in final_exits:
+ self.add_arc(final_exit.lineno, lineno, cause)
+ breaks = try_block.break_from
+ else:
+ breaks = self._combine_finally_starts(try_block.break_from, final_exits)
+ self.process_break_exits(breaks)
+
if try_block.continue_from:
- self.process_continue_exits(
- self._combine_finally_starts(try_block.continue_from, final_exits)
- )
+ if env.PYBEHAVIOR.finally_jumps_back:
+ for continue_line in try_block.continue_from:
+ lineno = continue_line.lineno
+ cause = continue_line.cause.format(lineno=lineno)
+ for final_exit in final_exits:
+ self.add_arc(final_exit.lineno, lineno, cause)
+ continues = try_block.continue_from
+ else:
+ continues = self._combine_finally_starts(try_block.continue_from, final_exits)
+ self.process_continue_exits(continues)
+
if try_block.raise_from:
self.process_raise_exits(
self._combine_finally_starts(try_block.raise_from, final_exits)
)
+
if try_block.return_from:
- self.process_return_exits(
- self._combine_finally_starts(try_block.return_from, final_exits)
- )
+ if env.PYBEHAVIOR.finally_jumps_back:
+ for return_line in try_block.return_from:
+ lineno = return_line.lineno
+ cause = return_line.cause.format(lineno=lineno)
+ for final_exit in final_exits:
+ self.add_arc(final_exit.lineno, lineno, cause)
+ returns = try_block.return_from
+ else:
+ returns = self._combine_finally_starts(try_block.return_from, final_exits)
+ self.process_return_exits(returns)
if exits:
# The finally clause's exits are only exits for the try block
diff --git a/tests/test_arcs.py b/tests/test_arcs.py
index c86147b7..f20c8cad 100644
--- a/tests/test_arcs.py
+++ b/tests/test_arcs.py
@@ -623,6 +623,10 @@ class ExceptionArcTest(CoverageTest):
def test_break_through_finally(self):
+ if env.PYBEHAVIOR.finally_jumps_back:
+ arcz = ".1 12 23 34 3D 45 56 67 68 7A 7D 8A A3 A7 BC CD D."
+ else:
+ arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D."
self.check_coverage("""\
a, c, d, i = 1, 1, 1, 99
try:
@@ -638,11 +642,15 @@ class ExceptionArcTest(CoverageTest):
d = 12 # C
assert a == 5 and c == 10 and d == 1 # D
""",
- arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D.",
+ arcz=arcz,
arcz_missing="3D BC CD",
)
def test_continue_through_finally(self):
+ if env.PYBEHAVIOR.finally_jumps_back:
+ arcz = ".1 12 23 34 3D 45 56 67 68 73 7A 8A A3 A7 BC CD D."
+ else:
+ arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D."
self.check_coverage("""\
a, b, c, d, i = 1, 1, 1, 1, 99
try:
@@ -658,7 +666,7 @@ class ExceptionArcTest(CoverageTest):
d = 12 # C
assert (a, b, c, d) == (5, 8, 10, 1) # D
""",
- arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D.",
+ arcz=arcz,
arcz_missing="BC CD",
)
@@ -794,6 +802,10 @@ class ExceptionArcTest(CoverageTest):
)
def test_return_finally(self):
+ if env.PYBEHAVIOR.finally_jumps_back:
+ arcz = ".1 12 29 9A AB BC C-1 -23 34 45 5-2 57 75 38 8-2"
+ else:
+ arcz = ".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2"
self.check_coverage("""\
a = [1]
def check_token(data):
@@ -808,10 +820,26 @@ class ExceptionArcTest(CoverageTest):
assert check_token(True) == 5
assert a == [1, 7]
""",
- arcz=".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2",
+ arcz=arcz,
)
def test_except_jump_finally(self):
+ if env.PYBEHAVIOR.finally_jumps_back:
+ arcz = (
+ ".1 1Q QR RS ST TU U. "
+ ".2 23 34 45 56 4O 6L "
+ "78 89 9A AL LA AO 8B BC CD DL LD D4 BE EF FG GL LG G. EH HI IJ JL HL "
+ "L4 LM "
+ "MN NO O."
+ )
+ else:
+ arcz = (
+ ".1 1Q QR RS ST TU U. "
+ ".2 23 34 45 56 4O 6L "
+ "78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL "
+ "LO L4 L. LM "
+ "MN NO O."
+ )
self.check_coverage("""\
def func(x):
a = f = g = 2
@@ -842,18 +870,30 @@ class ExceptionArcTest(CoverageTest):
assert func('continue') == (12, 21, 2, 3) # R
assert func('return') == (15, 2, 2, 0) # S
assert func('raise') == (18, 21, 23, 0) # T
+ assert func('other') == (2, 21, 2, 3) # U 30
""",
- arcz=
- ".1 1Q QR RS ST T. "
- ".2 23 34 45 56 4O 6L "
- "78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL "
- "LO L4 L. LM "
- "MN NO O.",
- arcz_missing="6L HL",
+ arcz=arcz,
+ arcz_missing="6L",
arcz_unpredicted="67",
)
def test_else_jump_finally(self):
+ if env.PYBEHAVIOR.finally_jumps_back:
+ arcz = (
+ ".1 1S ST TU UV VW W. "
+ ".2 23 34 45 56 6A 78 8N 4Q "
+ "AB BC CN NC CQ AD DE EF FN NF F4 DG GH HI IN NI I. GJ JK KL LN JN "
+ "N4 NO "
+ "OP PQ Q."
+ )
+ else:
+ arcz = (
+ ".1 1S ST TU UV VW W. "
+ ".2 23 34 45 56 6A 78 8N 4Q "
+ "AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN "
+ "N4 NQ N. NO "
+ "OP PQ Q."
+ )
self.check_coverage("""\
def func(x):
a = f = g = 2
@@ -886,14 +926,10 @@ class ExceptionArcTest(CoverageTest):
assert func('continue') == (14, 23, 2, 3) # T
assert func('return') == (17, 2, 2, 0) # U
assert func('raise') == (20, 23, 25, 0) # V
+ assert func('other') == (2, 23, 2, 3) # W 32
""",
- arcz=
- ".1 1S ST TU UV V. "
- ".2 23 34 45 56 6A 78 8N 4Q "
- "AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN "
- "NQ N4 N. NO "
- "OP PQ Q.",
- arcz_missing="78 8N JN",
+ arcz=arcz,
+ arcz_missing="78 8N",
arcz_unpredicted="",
)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index c2d70ee5..6340a44b 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -326,26 +326,57 @@ class ParserMissingArcDescriptionTest(CoverageTest):
this_thing(16)
that_thing(17)
""")
- self.assertEqual(
- parser.missing_arc_description(16, 17),
- "line 16 didn't jump to line 17, because the break on line 5 wasn't executed"
- )
- self.assertEqual(
- parser.missing_arc_description(16, 2),
- "line 16 didn't jump to line 2, "
- "because the continue on line 8 wasn't executed"
+ if env.PYBEHAVIOR.finally_jumps_back:
+ self.assertEqual(
+ parser.missing_arc_description(16, 5),
+ "line 16 didn't jump to line 5, because the break on line 5 wasn't executed"
+ )
+ self.assertEqual(
+ parser.missing_arc_description(5, 17),
+ "line 5 didn't jump to line 17, because the break on line 5 wasn't executed"
+ )
+ self.assertEqual(
+ parser.missing_arc_description(16, 8),
+ "line 16 didn't jump to line 8, because the continue on line 8 wasn't executed"
+ )
+ self.assertEqual(
+ parser.missing_arc_description(8, 2),
+ "line 8 didn't jump to line 2, because the continue on line 8 wasn't executed"
+ )
+ self.assertEqual(
+ parser.missing_arc_description(16, 12),
+ "line 16 didn't jump to line 12, because the return on line 12 wasn't executed"
+ )
+ self.assertEqual(
+ parser.missing_arc_description(12, -1),
+ "line 12 didn't return from function 'function', "
+ "because the return on line 12 wasn't executed"
+ )
+ self.assertEqual(
+ parser.missing_arc_description(16, -1),
+ "line 16 didn't except from function 'function', "
+ "because the raise on line 14 wasn't executed"
+ )
+ else:
+ self.assertEqual(
+ parser.missing_arc_description(16, 17),
+ "line 16 didn't jump to line 17, because the break on line 5 wasn't executed"
+ )
+ self.assertEqual(
+ parser.missing_arc_description(16, 2),
+ "line 16 didn't jump to line 2, "
+ "because the continue on line 8 wasn't executed"
+ " or "
+ "the continue on line 10 wasn't executed"
+ )
+ self.assertEqual(
+ parser.missing_arc_description(16, -1),
+ "line 16 didn't except from function 'function', "
+ "because the raise on line 14 wasn't executed"
" or "
- "the continue on line 10 wasn't executed"
- )
- self.assertEqual(
- parser.missing_arc_description(16, -1),
- "line 16 didn't except from function 'function', "
- "because the raise on line 14 wasn't executed"
- " or "
- "line 16 didn't return from function 'function', "
- "because the return on line 12 wasn't executed"
- )
-
+ "line 16 didn't return from function 'function', "
+ "because the return on line 12 wasn't executed"
+ )
def test_missing_arc_descriptions_bug460(self):
parser = self.parse_text(u"""\
x = 1