diff options
-rw-r--r-- | coverage/parser.py | 143 | ||||
-rw-r--r-- | coverage/results.py | 5 | ||||
-rw-r--r-- | tests/coveragetest.py | 2 | ||||
-rw-r--r-- | tests/test_arcs.py | 79 |
4 files changed, 162 insertions, 67 deletions
diff --git a/coverage/parser.py b/coverage/parser.py index 65b1f0fb..ff2d2bec 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -11,7 +11,7 @@ import token import tokenize from coverage.backward import range # pylint: disable=redefined-builtin -from coverage.backward import bytes_to_ints +from coverage.backward import bytes_to_ints, string_class from coverage.bytecode import ByteCodes, CodeObjects from coverage.misc import contract, nice_pair, join_regex from coverage.misc import CoverageException, NoSource, NotPython @@ -245,7 +245,7 @@ class PythonParser(object): starts = self.raw_statements - ignore self.statements = self.first_lines(starts) - ignore - def arcs(self): + def old_arcs(self): """Get information about the arcs available in the code. Returns a set of line number pairs. Line numbers have been normalized @@ -261,7 +261,7 @@ class PythonParser(object): self._all_arcs.add((fl1, fl2)) return self._all_arcs - def ast_arcs(self): + def arcs(self): aaa = AstArcAnalyzer(self.text) arcs = aaa.collect_arcs() @@ -301,18 +301,36 @@ class PythonParser(object): return exit_counts +class LoopBlock(object): + def __init__(self, start): + self.start = start + self.break_exits = set() + +class FunctionBlock(object): + def __init__(self, start): + self.start = start + +class TryBlock(object): + def __init__(self, handler_start=None, final_start=None): + self.handler_start = handler_start # TODO: is this used? + self.final_start = final_start # TODO: is this used? + self.break_from = set([]) + self.continue_from = set([]) + self.return_from = set([]) + self.raise_from = set([]) + + class AstArcAnalyzer(object): def __init__(self, text): self.root_node = ast.parse(text) - ast_dump(self.root_node) + #ast_dump(self.root_node) self.arcs = None - # References to the nearest enclosing thing of its kind. - self.function_start = None - self.loop_start = None + self.block_stack = [] - # Break-exits from a loop - self.break_exits = None + def blocks(self): + """Yield the blocks in nearest-to-farthest order.""" + return reversed(self.block_stack) def line_for_node(self, node): """What is the right line number to use for this node?""" @@ -366,28 +384,70 @@ class AstArcAnalyzer(object): # TODO: nested function definitions # TODO: multiple `except` clauses + def process_break_exits(self, exits): + for block in self.blocks(): + if isinstance(block, LoopBlock): + # TODO: what if there is no loop? + block.break_exits.update(exits) + break + elif isinstance(block, TryBlock) and block.final_start: + block.break_from.update(exits) + break + + def process_continue_exits(self, exits): + for block in self.blocks(): + if isinstance(block, LoopBlock): + # TODO: what if there is no loop? + for exit in exits: + self.arcs.add((exit, block.start)) + break + elif isinstance(block, TryBlock) and block.final_start: + block.continue_from.update(exits) + break + + def process_raise_exits(self, exits): + for block in self.blocks(): + if isinstance(block, TryBlock): + if block.handler_start: + for exit in exits: + self.arcs.add((exit, block.handler_start)) + break + elif block.final_start: + block.raise_from.update(exits) + break + elif isinstance(block, FunctionBlock): + for exit in exits: + self.arcs.add((exit, -block.start)) + break + + def process_return_exits(self, exits): + for block in self.blocks(): + if isinstance(block, FunctionBlock): + # TODO: what if there is no enclosing function? + for exit in exits: + self.arcs.add((exit, -block.start)) + break + + ## Handlers + def handle_Break(self, node): here = self.line_for_node(node) - # TODO: what if self.break_exits is None? - self.break_exits.add(here) + self.process_break_exits([here]) return set([]) def handle_Continue(self, node): here = self.line_for_node(node) - # TODO: what if self.loop_start is None? - self.arcs.add((here, self.loop_start)) + self.process_continue_exits([here]) return set([]) def handle_For(self, node): start = self.line_for_node(node.iter) - loop_state = self.loop_start, self.break_exits - self.loop_start = start - self.break_exits = set() + self.block_stack.append(LoopBlock(start=start)) exits = self.add_body_arcs(node.body, from_line=start) for exit in exits: self.arcs.add((exit, start)) - exits = self.break_exits - self.loop_start, self.break_exits = loop_state + my_block = self.block_stack.pop() + exits = my_block.break_exits if node.orelse: else_start = self.line_for_node(node.orelse[0]) self.arcs.add((start, else_start)) @@ -415,15 +475,29 @@ class AstArcAnalyzer(object): def handle_Raise(self, node): # `raise` statement jumps away, no exits from here. + here = self.line_for_node(node) + self.process_raise_exits([here]) return set([]) def handle_Return(self, node): + # TODO: deal with returning through a finally. here = self.line_for_node(node) - # TODO: what if self.function_start is None? - self.arcs.add((here, -self.function_start)) + self.process_return_exits([here]) return set([]) def handle_Try(self, node): + # try/finally is tricky. If there's a finally clause, then we need a + # FinallyBlock to track what flows might go through the finally instead + # of their normal flow. + if node.handlers: + handler_start = self.line_for_node(node.handlers[0]) + else: + handler_start = None + if node.finalbody: + final_start = self.line_for_node(node.finalbody[0]) + else: + final_start = None + self.block_stack.append(TryBlock(handler_start=handler_start, final_start=final_start)) start = self.line_for_node(node) exits = self.add_body_arcs(node.body, from_line=start) handler_exits = set() @@ -434,7 +508,17 @@ class AstArcAnalyzer(object): # TODO: node.orelse exits |= handler_exits if node.finalbody: - exits = self.add_body_arcs(node.finalbody, prev_lines=exits) + final_block = self.block_stack.pop() + final_from = exits | final_block.break_from | final_block.continue_from | final_block.raise_from | final_block.return_from + exits = self.add_body_arcs(node.finalbody, prev_lines=final_from) + if final_block.break_from: + self.process_break_exits(exits) + if final_block.continue_from: + self.process_continue_exits(exits) + if final_block.raise_from: + self.process_raise_exits(exits) + if final_block.return_from: + self.process_return_exits(exits) return exits def handle_While(self, node): @@ -442,20 +526,19 @@ class AstArcAnalyzer(object): start = to_top = self.line_for_node(node.test) if constant_test: to_top = self.line_for_node(node.body[0]) - loop_state = self.loop_start, self.break_exits - self.loop_start = start - self.break_exits = set() + self.block_stack.append(LoopBlock(start=start)) exits = self.add_body_arcs(node.body, from_line=start) for exit in exits: self.arcs.add((exit, to_top)) - exits = self.break_exits - self.loop_start, self.break_exits = loop_state + # TODO: while loop that finishes? + my_block = self.block_stack.pop() + exits = my_block.break_exits # TODO: orelse return exits def handle_default(self, node): node_name = node.__class__.__name__ - if node_name not in ["Assign", "Assert", "AugAssign", "Expr"]: + if node_name not in ["Assign", "Assert", "AugAssign", "Expr", "Pass"]: print("*** Unhandled: {}".format(node)) return set([self.line_for_node(node)]) @@ -469,11 +552,11 @@ class AstArcAnalyzer(object): self.arcs.add((exit, -start)) elif node_name == "FunctionDef": start = self.line_for_node(node) - self.function_start = start + self.block_stack.append(FunctionBlock(start=start)) func_exits = self.add_body_arcs(node.body, from_line=-1) + self.block_stack.pop() for exit in func_exits: self.arcs.add((exit, -start)) - self.function_start = None elif node_name == "comprehension": start = self.line_for_node(node) self.arcs.add((-1, start)) @@ -917,7 +1000,7 @@ def ast_dump(node, depth=0): prefix = "{0}{1}:".format(indent, field_name) if value is None: print("{0} None".format(prefix)) - elif isinstance(value, (str, int)): + elif isinstance(value, (string_class, int, float)): print("{0} {1!r}".format(prefix, value)) elif isinstance(value, list): if value == []: diff --git a/coverage/results.py b/coverage/results.py index b80d5042..9627373d 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -26,7 +26,6 @@ class Analysis(object): if self.data.has_arcs(): self._arc_possibilities = sorted(self.file_reporter.arcs()) - self._ast_arc_possibilities = sorted(self.file_reporter.ast_arcs()) self.exit_counts = self.file_reporter.exit_counts() self.no_branch = self.file_reporter.no_branch_lines() n_branches = self.total_branches() @@ -37,7 +36,6 @@ class Analysis(object): n_missing_branches = sum(len(v) for k,v in iitems(mba)) else: self._arc_possibilities = [] - self._ast_arc_possibilities = [] self.exit_counts = {} self.no_branch = set() n_branches = n_partial_branches = n_missing_branches = 0 @@ -68,9 +66,6 @@ class Analysis(object): """Returns a sorted list of the arcs in the code.""" return self._arc_possibilities - def ast_arc_possibilities(self): - return self._ast_arc_possibilities - def arcs_executed(self): """Returns a sorted list of the arcs actually executed in the code.""" executed = self.data.arcs(self.filename) or [] diff --git a/tests/coveragetest.py b/tests/coveragetest.py index f3911e3b..5f85e75c 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -165,6 +165,7 @@ class CoverageTest( excludes=None, partials="", arcz=None, arcz_missing=None, arcz_unpredicted=None, arcs=None, arcs_missing=None, arcs_unpredicted=None, + ast_differs=False, ): """Check the coverage measurement of `text`. @@ -239,7 +240,6 @@ class CoverageTest( if arcs is not None: self.assert_equal_args(analysis.arc_possibilities(), arcs, "Possible arcs differ") - self.assert_equal_args(analysis.ast_arc_possibilities(), arcs, "Possible ast arcs differ") if arcs_missing is not None: self.assert_equal_args( diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 2d90b067..0407b560 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -365,44 +365,50 @@ class ExceptionArcTest(CoverageTest): b = 7 assert a == 3 and b == 7 """, - arcz=".1 12 23 34 58 67 78 8.", - arcz_missing="58", arcz_unpredicted="46") + arcz=".1 12 23 34 46 58 67 78 8.", + arcz_missing="58", + ast_differs=True, + ) def test_hidden_raise(self): self.check_coverage("""\ a, b = 1, 1 def oops(x): - if x % 2: raise Exception("odd") + if x % 2: + raise Exception("odd") try: - a = 5 + a = 6 oops(1) - a = 7 + a = 8 except: - b = 9 - assert a == 5 and b == 9 + b = 10 + assert a == 6 and b == 10 """, - arcz=".1 12 .3 3-2 24 45 56 67 7A 89 9A A.", - arcz_missing="67 7A", arcz_unpredicted="68") + arcz=".1 12 .3 34 3-2 4-2 25 56 67 78 8B 9A AB B.", + arcz_missing="3-2 78 8B", arcz_unpredicted="79", + ast_differs=True, + ) def test_except_with_type(self): self.check_coverage("""\ a, b = 1, 1 def oops(x): - if x % 2: raise ValueError("odd") + if x % 2: + raise ValueError("odd") def try_it(x): try: - a = 6 + a = 7 oops(x) - a = 8 + a = 9 except ValueError: - b = 10 + b = 11 return a - assert try_it(0) == 8 # C - assert try_it(1) == 6 # D + assert try_it(0) == 9 # C + assert try_it(1) == 7 # D """, - arcz=".1 12 .3 3-2 24 4C CD D. .5 56 67 78 8B 9A AB B-4", + arcz=".1 12 .3 34 3-2 4-2 25 5D DE E. .6 67 78 89 9C AB BC C-5", arcz_missing="", - arcz_unpredicted="79") + arcz_unpredicted="8A") def test_try_finally(self): self.check_coverage("""\ @@ -425,8 +431,8 @@ class ExceptionArcTest(CoverageTest): d = 8 assert a == 4 and c == 6 and d == 1 # 9 """, - arcz=".1 12 23 34 46 67 78 89 69 9.", - arcz_missing="67 78 89", arcz_unpredicted="") + arcz=".1 12 23 34 46 78 89 69 9.", + arcz_missing="78 89", arcz_unpredicted="") self.check_coverage("""\ a, c, d = 1, 1, 1 try: @@ -440,8 +446,8 @@ class ExceptionArcTest(CoverageTest): d = 10 # A assert a == 4 and c == 8 and d == 10 # B """, - arcz=".1 12 23 34 45 68 89 8B 9A AB B.", - arcz_missing="68 8B", arcz_unpredicted="58") + arcz=".1 12 23 34 45 58 68 89 8B 9A AB B.", + arcz_missing="68 8B", arcz_unpredicted="") def test_finally_in_loop(self): self.check_coverage("""\ @@ -459,8 +465,10 @@ class ExceptionArcTest(CoverageTest): d = 12 # C assert a == 5 and c == 10 and d == 12 # D """, - arcz=".1 12 23 34 3D 45 56 67 68 8A A3 AB BC CD D.", - arcz_missing="3D", arcz_unpredicted="7A") + arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AB BC CD D.", + arcz_missing="3D", + ast_differs=True, + ) self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 try: @@ -476,12 +484,12 @@ class ExceptionArcTest(CoverageTest): d = 12 # C assert a == 8 and c == 10 and d == 1 # D """, - arcz=".1 12 23 34 3D 45 56 67 68 8A A3 AB BC CD D.", - arcz_missing="67 AB BC CD", arcz_unpredicted="") + arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AB BC CD D.", + arcz_missing="67 7A AB BC CD", arcz_unpredicted="", + ) - def test_break_in_finally(self): - # TODO: the name and the code don't seem to match + def test_break_through_finally(self): self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 try: @@ -497,8 +505,12 @@ 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 AB BC CD D.", - arcz_missing="3D AB BC CD", arcz_unpredicted="AD") + arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D.", + arcz_missing="3D BC CD", arcz_unpredicted="", + ) + + # TODO: shouldn't arcz_unpredicted always be empty? + # NO: it has arcs due to exceptions. def test_finally_in_loop_bug_92(self): self.check_coverage("""\ @@ -608,7 +620,8 @@ class ExceptionArcTest(CoverageTest): assert a == 7 and b == 1 and c == 9 """, arcz=".1 12 23 45 39 59 67 79 9A A.", arcz_missing="39 45 59", - arcz_unpredicted="34 46") + arcz_unpredicted="34 46", + ) self.check_coverage("""\ a, b, c = 1, 1, 1 try: @@ -624,7 +637,11 @@ class ExceptionArcTest(CoverageTest): pass assert a == 1 and b == 1 and c == 10 """, - arcz=".1 12 23 34 4A 56 6A 78 8A AB AD BC CD D.", arcz_missing="45 56 57 78") + arcz=".1 12 23 34 4A 56 6A 78 8A AD BC CD D.", + arcz_missing="4A 56 6A 78 8A AD", + arcz_unpredicted="45 57 7A AB", + ast_differs=True, # TODO: get rid of all ast_differs + ) class YieldTest(CoverageTest): |