summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/parser.py143
-rw-r--r--coverage/results.py5
-rw-r--r--tests/coveragetest.py2
-rw-r--r--tests/test_arcs.py79
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):