diff options
-rw-r--r-- | coverage/env.py | 10 | ||||
-rw-r--r-- | coverage/parser.py | 47 | ||||
-rw-r--r-- | tests/test_arcs.py | 70 | ||||
-rw-r--r-- | tests/test_parser.py | 69 |
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 |