From b17f27672208a07318f8aa62a1bd64b18e9961d1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 24 Dec 2015 08:49:50 -0500 Subject: WIP: measure branches with ast instead of bytecode --HG-- branch : ast-branch --- coverage/parser.py | 230 +++++++++++++++++++++++++++++++++++++++++++++++++- coverage/python.py | 4 + coverage/results.py | 5 ++ lab/parser.py | 25 +++--- tests/coveragetest.py | 1 + tests/test_arcs.py | 178 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 431 insertions(+), 12 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 7b8a60f1..fb2cf955 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -3,6 +3,7 @@ """Code parsing for coverage.py.""" +import ast import collections import dis import re @@ -260,6 +261,18 @@ class PythonParser(object): self._all_arcs.add((fl1, fl2)) return self._all_arcs + def ast_arcs(self): + aaa = AstArcAnalyzer(self.text) + arcs = aaa.collect_arcs() + + arcs_ = set() + for l1, l2 in arcs: + fl1 = self.first_line(l1) + fl2 = self.first_line(l2) + if fl1 != fl2: + arcs_.add((fl1, fl2)) + return arcs_ + def exit_counts(self): """Get a count of exits from that each line. @@ -288,6 +301,168 @@ class PythonParser(object): return exit_counts +class AstArcAnalyzer(object): + def __init__(self, text): + self.root_node = ast.parse(text) + 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 + + # Break-exits from a loop + self.break_exits = None + + def line_for_node(self, node): + """What is the right line number to use for this node?""" + node_name = node.__class__.__name__ + if node_name == "Assign": + return node.value.lineno + elif node_name == "comprehension": + # TODO: is this how to get the line number for a comprehension? + return node.target.lineno + else: + return node.lineno + + def collect_arcs(self): + self.arcs = set() + self.add_arcs_for_code_objects(self.root_node) + return self.arcs + + def add_arcs(self, node): + """add the arcs for `node`. + + Return a set of line numbers, exits from this node to the next. + """ + node_name = node.__class__.__name__ + #print("Adding arcs for {}".format(node_name)) + + handler = getattr(self, "handle_" + node_name, self.handle_default) + return handler(node) + + def add_body_arcs(self, body, from_line): + prev_lines = set([from_line]) + for body_node in body: + lineno = self.line_for_node(body_node) + for prev_lineno in prev_lines: + self.arcs.add((prev_lineno, lineno)) + prev_lines = self.add_arcs(body_node) + return prev_lines + + def is_constant_expr(self, node): + """Is this a compile-time constant?""" + node_name = node.__class__.__name__ + return node_name in ["NameConstant", "Num"] + + # tests to write: + # TODO: while EXPR: + # TODO: while False: + # TODO: multi-target assignment with computed targets + # TODO: listcomps hidden deep in other expressions + # TODO: listcomps hidden in lists: x = [[i for i in range(10)]] + # TODO: multi-line listcomps + # TODO: nested function definitions + + def handle_Break(self, node): + here = self.line_for_node(node) + # TODO: what if self.break_exits is None? + self.break_exits.add(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)) + 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() + 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 + if node.orelse: + else_start = self.line_for_node(node.orelse[0]) + self.arcs.add((start, else_start)) + else_exits = self.add_body_arcs(node.orelse, from_line=start) + exits |= else_exits + else: + # no else clause: exit from the for line. + exits.add(start) + return exits + + def handle_FunctionDef(self, node): + start = self.line_for_node(node) + # the body is handled in add_arcs_for_code_objects. + exits = set([start]) + return exits + + def handle_If(self, node): + start = self.line_for_node(node.test) + exits = self.add_body_arcs(node.body, from_line=start) + exits |= self.add_body_arcs(node.orelse, from_line=start) + return exits + + def handle_Module(self, node): + raise Exception("TODO: this shouldn't happen") + + def handle_Return(self, node): + here = self.line_for_node(node) + # TODO: what if self.function_start is None? + self.arcs.add((here, -self.function_start)) + return set([]) + + def handle_While(self, node): + constant_test = self.is_constant_expr(node.test) + 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() + 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: orelse + return exits + + def handle_default(self, node): + node_name = node.__class__.__name__ + if node_name not in ["Assign", "Assert", "AugAssign", "Expr"]: + print("*** Unhandled: {}".format(node)) + return set([self.line_for_node(node)]) + + def add_arcs_for_code_objects(self, root_node): + for node in ast.walk(root_node): + node_name = node.__class__.__name__ + if node_name == "Module": + start = self.line_for_node(node.body[0]) + exits = self.add_body_arcs(node.body, from_line=-1) + for exit in exits: + self.arcs.add((exit, -start)) + elif node_name == "FunctionDef": + start = self.line_for_node(node) + self.function_start = start + func_exits = self.add_body_arcs(node.body, from_line=-1) + 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)) + self.arcs.add((start, -start)) + # TODO: guaranteed this won't work for multi-line comps. + + + + ## Opcodes that guide the ByteParser. def _opcode(name): @@ -321,7 +496,7 @@ OPS_CHUNK_BEGIN = _opcode_set('JUMP_ABSOLUTE', 'JUMP_FORWARD') # Opcodes that push a block on the block stack. OPS_PUSH_BLOCK = _opcode_set( - 'SETUP_LOOP', 'SETUP_EXCEPT', 'SETUP_FINALLY', 'SETUP_WITH' + 'SETUP_LOOP', 'SETUP_EXCEPT', 'SETUP_FINALLY', 'SETUP_WITH', 'SETUP_ASYNC_WITH', ) # Block types for exception handling. @@ -330,6 +505,8 @@ OPS_EXCEPT_BLOCKS = _opcode_set('SETUP_EXCEPT', 'SETUP_FINALLY') # Opcodes that pop a block from the block stack. OPS_POP_BLOCK = _opcode_set('POP_BLOCK') +OPS_GET_AITER = _opcode_set('GET_AITER') + # Opcodes that have a jump destination, but aren't really a jump. OPS_NO_JUMP = OPS_PUSH_BLOCK @@ -449,6 +626,8 @@ class ByteParser(object): # is a count of how many ignores are left. ignore_branch = 0 + ignore_pop_block = 0 + # We have to handle the last two bytecodes specially. ult = penult = None @@ -507,7 +686,10 @@ class ByteParser(object): block_stack.append((bc.op, bc.jump_to)) if bc.op in OPS_POP_BLOCK: # The opcode pops a block from the block stack. - block_stack.pop() + if ignore_pop_block: + ignore_pop_block -= 1 + else: + block_stack.pop() if bc.op in OPS_CHUNK_END: # This opcode forces the end of the chunk. if bc.op == OP_BREAK_LOOP: @@ -527,6 +709,15 @@ class ByteParser(object): # branch, so that except's don't count as branches. ignore_branch += 1 + if bc.op in OPS_GET_AITER: + # GET_AITER is weird: First, it seems to generate one more + # POP_BLOCK than SETUP_*, so we have to prepare to ignore one + # of the POP_BLOCKS. Second, we don't have a clear branch to + # the exit of the loop, so we peek into the block stack to find + # it. + ignore_pop_block += 1 + chunk.exits.add(block_stack[-1][1]) + penult = ult ult = bc @@ -686,3 +877,38 @@ class Chunk(object): "v" if self.entrance else "", list(self.exits), ) + + +SKIP_FIELDS = ["ctx"] + +def ast_dump(node, depth=0): + indent = " " * depth + lineno = getattr(node, "lineno", None) + if lineno is not None: + linemark = " @ {0}".format(lineno) + else: + linemark = "" + print("{0}<{1}{2}".format(indent, node.__class__.__name__, linemark)) + + indent += " " + for field_name, value in ast.iter_fields(node): + if field_name in SKIP_FIELDS: + continue + prefix = "{0}{1}:".format(indent, field_name) + if value is None: + print("{0} None".format(prefix)) + elif isinstance(value, (str, int)): + print("{0} {1!r}".format(prefix, value)) + elif isinstance(value, list): + if value == []: + print("{0} []".format(prefix)) + else: + print("{0} [".format(prefix)) + for n in value: + ast_dump(n, depth + 8) + print("{0}]".format(indent)) + else: + print(prefix) + ast_dump(value, depth + 8) + + print("{0}>".format(" " * depth)) diff --git a/coverage/python.py b/coverage/python.py index 5e563828..bf19cb22 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -159,6 +159,10 @@ class PythonFileReporter(FileReporter): def arcs(self): return self.parser.arcs() + @expensive + def ast_arcs(self): + return self.parser.ast_arcs() + @expensive def exit_counts(self): return self.parser.exit_counts() diff --git a/coverage/results.py b/coverage/results.py index 9627373d..b80d5042 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -26,6 +26,7 @@ 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() @@ -36,6 +37,7 @@ 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 @@ -66,6 +68,9 @@ 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/lab/parser.py b/lab/parser.py index 1a679e8c..9a064257 100644 --- a/lab/parser.py +++ b/lab/parser.py @@ -82,7 +82,7 @@ class ParserMain(object): if options.dis: print("Main code:") - self.disassemble(bp, histogram=options.histogram) + self.disassemble(bp, chunks=options.chunks, histogram=options.histogram) arcs = bp._all_arcs() if options.chunks: @@ -123,15 +123,20 @@ class ParserMain(object): m2 = 'C' if lineno in cp.raw_excluded: m3 = 'x' - a = arc_chars[lineno].ljust(arc_width) + + if arc_chars: + a = arc_chars[lineno].ljust(arc_width) + else: + a = "" + print("%4d %s%s%s%s%s %s" % (lineno, m0, m1, m2, m3, a, ltext)) - def disassemble(self, byte_parser, histogram=False): + def disassemble(self, byte_parser, chunks=False, histogram=False): """Disassemble code, for ad-hoc experimenting.""" for bp in byte_parser.child_parsers(): - chunks = bp._split_into_chunks() - chunkd = dict((chunk.byte, chunk) for chunk in chunks) + if chunks: + chunkd = dict((chunk.byte, chunk) for chunk in bp._split_into_chunks()) if bp.text: srclines = bp.text.splitlines() else: @@ -151,11 +156,11 @@ class ParserMain(object): elif disline.offset > 0: print("") line = disgen.format_dis_line(disline) - chunk = chunkd.get(disline.offset) - if chunk: - chunkstr = ":: %r" % chunk - else: - chunkstr = "" + chunkstr = "" + if chunks: + chunk = chunkd.get(disline.offset) + if chunk: + chunkstr = ":: %r" % chunk print("%-70s%s" % (line, chunkstr)) print("") diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 3468b794..f3911e3b 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -239,6 +239,7 @@ 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 df303d8b..f136b755 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -3,6 +3,10 @@ """Tests for coverage.py's arc measurement.""" +import collections +from itertools import cycle, product +import re + from tests.coveragetest import CoverageTest import coverage @@ -715,6 +719,180 @@ class MiscArcTest(CoverageTest): ) +class AsyncTest(CoverageTest): + def setUp(self): + if env.PYVERSION < (3, 5): + self.skip("No point testing 3.5 syntax below 3.5") + super(AsyncTest, self).setUp() + + def test_async(self): + self.check_coverage("""\ + import asyncio + + async def compute(x, y): + print("Compute %s + %s ..." % (x, y)) + await asyncio.sleep(0.001) + return x + y + + async def print_sum(x, y): + result = await compute(x, y) + print("%s + %s = %s" % (x, y, result)) + + loop = asyncio.get_event_loop() + loop.run_until_complete(print_sum(1, 2)) + loop.close() + """, + arcz= + ".1 13 38 8C CD DE E. " + ".4 45 56 6-3 " + ".9 9A A-8", + arcz_missing="", + ) + self.assertEqual(self.stdout(), "Compute 1 + 2 ...\n1 + 2 = 3\n") + + def test_async_for(self): + self.check_coverage("""\ + import asyncio + + class AsyncIteratorWrapper: # 3 + def __init__(self, obj): # 4 + self._it = iter(obj) + + async def __aiter__(self): # 7 + return self + + async def __anext__(self): # A + try: + return next(self._it) + except StopIteration: + raise StopAsyncIteration + + async def doit(): # G + async for letter in AsyncIteratorWrapper("abc"): + print(letter) + print(".") + + loop = asyncio.get_event_loop() # L + loop.run_until_complete(doit()) + loop.close() + """, + arcz= + ".1 13 3G GL LM MN N. " # module main line + ".3 34 47 7A A-3 " # class definition + ".H HI IH HJ J-G " # doit + ".5 5-4 " # __init__ + ".8 8-7 " # __aiter__ + ".B BC C-A DE ", # __anext__ + arcz_missing="", + ) + self.assertEqual(self.stdout(), "a\nb\nc\n.\n") + + def test_async_for2(self): + self.check_coverage("""\ + async def go1(): + async for x2 in y2: + try: + async for x4 in y4: + if a5: + break + else: + x8 = 1 + except: + x10 = 1 + x11 = 1 + x12 = 1 + """, + arcz=".1 1. .2 23 2C 34 45 56 6B", + ) + + def test_async_with(self): + self.check_coverage("""\ + async def go(): + async with x: + pass + """, + arcz=".1 1. .2 23 3.", + ) + + def test_async_it(self): + self.check_coverage("""\ + async def func(): + for x in g2: + x = 3 + else: + x = 5 + """, + arcz=".1 1. .2 23 32 25 5.", + ) + self.check_coverage("""\ + async def func(): + async for x in g2: + x = 3 + else: + x = 5 + """, + arcz=".1 1. .2 23 32 25 5.", + ) + + def xxxx_async_is_same_flow(self): + SOURCE = """\ + async def func(): + for x in g2: + try: + x = g4 + finally: + x = g6 + try: + with g8 as x: + x = g9 + continue + finally: + x = g12 + for x in g13: + continue + else: + break + while g17: + x = g18 + continue + else: + x = g21 + for x in g22: + x = g23 + continue + """ + + parts = re.split(r"(for |with )", SOURCE) + nchoices = len(parts) // 2 + + def only(s): + return [s] + + def maybe_async(s): + return [s, "async "+s] + + all_all_arcs = collections.defaultdict(list) + choices = [f(x) for f, x in zip(cycle([only, maybe_async]), parts)] + for i, result in enumerate(product(*choices)): + source = "".join(result) + self.make_file("async.py", source) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "async") + analysis = cov._analyze("async.py") + all_all_arcs[tuple(analysis.arc_possibilities())].append((i, source)) + + import pprint + pprint.pprint(list(all_all_arcs.keys())) + for arcs, numbers in all_all_arcs.items(): + print(" ".join("{0:0{1}b}".format(x[0], nchoices) for x in numbers)) + print(" {}".format(arcs)) + for i, source in numbers: + print("-" * 80) + print(source) + + assert len(all_all_arcs) == 1 + + class ExcludeTest(CoverageTest): """Tests of exclusions to indicate known partial branches.""" -- cgit v1.2.1 From b7a35186425cfef265548afc75b527752bed0c9a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 24 Dec 2015 19:46:00 -0500 Subject: A start on try/except/finally --HG-- branch : ast-branch --- coverage/parser.py | 24 ++++++++++++++++++++++-- tests/test_arcs.py | 18 ++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index fb2cf955..4b920f10 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -341,8 +341,9 @@ class AstArcAnalyzer(object): handler = getattr(self, "handle_" + node_name, self.handle_default) return handler(node) - def add_body_arcs(self, body, from_line): - prev_lines = set([from_line]) + def add_body_arcs(self, body, from_line=None, prev_lines=None): + if prev_lines is None: + prev_lines = set([from_line]) for body_node in body: lineno = self.line_for_node(body_node) for prev_lineno in prev_lines: @@ -363,6 +364,7 @@ class AstArcAnalyzer(object): # TODO: listcomps hidden in lists: x = [[i for i in range(10)]] # TODO: multi-line listcomps # TODO: nested function definitions + # TODO: multiple `except` clauses def handle_Break(self, node): here = self.line_for_node(node) @@ -411,12 +413,30 @@ class AstArcAnalyzer(object): def handle_Module(self, node): raise Exception("TODO: this shouldn't happen") + def handle_Raise(self, node): + # `raise` statement jumps away, no exits from here. + return set([]) + def handle_Return(self, node): here = self.line_for_node(node) # TODO: what if self.function_start is None? self.arcs.add((here, -self.function_start)) return set([]) + def handle_Try(self, node): + start = self.line_for_node(node) + exits = self.add_body_arcs(node.body, from_line=start) + handler_exits = set() + for handler_node in node.handlers: + handler_start = self.line_for_node(handler_node) + # TODO: handler_node.name and handler_node.type + handler_exits |= self.add_body_arcs(handler_node.body, from_line=handler_start) + # TODO: node.orelse + # TODO: node.finalbody + if node.finalbody: + exits = self.add_body_arcs(node.finalbody, prev_lines=exits|handler_exits) + return exits + def handle_While(self, node): constant_test = self.is_constant_expr(node.test) start = to_top = self.line_for_node(node.test) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index f136b755..04c0df6e 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -481,6 +481,7 @@ class ExceptionArcTest(CoverageTest): def test_break_in_finally(self): + # TODO: the name and the code don't seem to match self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 try: @@ -566,6 +567,23 @@ class ExceptionArcTest(CoverageTest): arcz=".1 12 .3 3-2 24 45 56 67 7B 89 9B BC C.", arcz_missing="67 7B", arcz_unpredicted="68") + def test_multiple_except_clauses(self): + self.check_coverage("""\ + a, b, c = 1, 1, 1 + try: + a = 3 + except ValueError: + b = 5 + except IndexError: + a = 7 + finally: + c = 9 + assert a == 3 and b == 1 and c == 9 + """, + arcz=".1 12 23 45 39 59 67 79 9A A.", arcz_missing="45 59 67 79") + # TODO: do it again, with line 3 raising a caught exception + # TODO: do it again, with line 3 raising an uncaught exception. + class YieldTest(CoverageTest): """Arc tests for generators.""" -- cgit v1.2.1 From 35c09545a39e70065ce55264f2688ac87dd6a725 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 28 Dec 2015 16:48:05 -0500 Subject: Execution flows from the end of exception handlers to the finally --HG-- branch : ast-branch --- coverage/parser.py | 4 ++-- tests/test_arcs.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 4b920f10..65b1f0fb 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -432,9 +432,9 @@ class AstArcAnalyzer(object): # TODO: handler_node.name and handler_node.type handler_exits |= self.add_body_arcs(handler_node.body, from_line=handler_start) # TODO: node.orelse - # TODO: node.finalbody + exits |= handler_exits if node.finalbody: - exits = self.add_body_arcs(node.finalbody, prev_lines=exits|handler_exits) + exits = self.add_body_arcs(node.finalbody, prev_lines=exits) return exits def handle_While(self, node): diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 04c0df6e..2d90b067 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -581,8 +581,50 @@ class ExceptionArcTest(CoverageTest): assert a == 3 and b == 1 and c == 9 """, arcz=".1 12 23 45 39 59 67 79 9A A.", arcz_missing="45 59 67 79") - # TODO: do it again, with line 3 raising a caught exception - # TODO: do it again, with line 3 raising an uncaught exception. + self.check_coverage("""\ + a, b, c = 1, 1, 1 + try: + a = int("xyz") # ValueError + except ValueError: + b = 5 + except IndexError: + a = 7 + finally: + c = 9 + assert a == 1 and b == 5 and c == 9 + """, + arcz=".1 12 23 45 39 59 67 79 9A A.", arcz_missing="39 67 79", + arcz_unpredicted="34") + self.check_coverage("""\ + a, b, c = 1, 1, 1 + try: + a = [1][3] # IndexError + except ValueError: + b = 5 + except IndexError: + a = 7 + finally: + c = 9 + 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") + self.check_coverage("""\ + a, b, c = 1, 1, 1 + try: + try: + a = 4/0 # ZeroDivisionError + except ValueError: + b = 6 + except IndexError: + a = 8 + finally: + c = 10 + except ZeroDivisionError: + 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") class YieldTest(CoverageTest): -- cgit v1.2.1 From 4b33f09a3d46e5dd051d060a1926567fd418cbb7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 Dec 2015 15:39:30 -0500 Subject: Exception tests pass on py3 --HG-- branch : ast-branch --- coverage/parser.py | 143 +++++++++++++++++++++++++++++++++++++++----------- coverage/results.py | 5 -- tests/coveragetest.py | 2 +- 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): -- cgit v1.2.1 From 5a6627ce5050d331095c4b03aed8e540f3ed651f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 Dec 2015 16:20:09 -0500 Subject: Make other comprehensions work on py2 and py3 --HG-- branch : ast-branch --- coverage/parser.py | 20 ++++++++++++-------- tests/test_arcs.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index ff2d2bec..36fa729c 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -10,6 +10,7 @@ import re import token import tokenize +from coverage import env from coverage.backward import range # pylint: disable=redefined-builtin from coverage.backward import bytes_to_ints, string_class from coverage.bytecode import ByteCodes, CodeObjects @@ -323,7 +324,7 @@ class TryBlock(object): 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 self.block_stack = [] @@ -542,6 +543,10 @@ class AstArcAnalyzer(object): print("*** Unhandled: {}".format(node)) return set([self.line_for_node(node)]) + CODE_COMPREHENSIONS = set(["GeneratorExp", "DictComp", "SetComp"]) + if env.PY3: + CODE_COMPREHENSIONS.add("ListComp") + def add_arcs_for_code_objects(self, root_node): for node in ast.walk(root_node): node_name = node.__class__.__name__ @@ -557,13 +562,12 @@ class AstArcAnalyzer(object): self.block_stack.pop() for exit in func_exits: self.arcs.add((exit, -start)) - elif node_name == "comprehension": - start = self.line_for_node(node) - self.arcs.add((-1, start)) - self.arcs.add((start, -start)) - # TODO: guaranteed this won't work for multi-line comps. - - + elif node_name in self.CODE_COMPREHENSIONS: + for gen in node.generators: + start = self.line_for_node(gen) + self.arcs.add((-1, start)) + self.arcs.add((start, -start)) + # TODO: guaranteed this won't work for multi-line comps. ## Opcodes that guide the ByteParser. diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 0407b560..a371401f 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -260,7 +260,7 @@ class LoopArcTest(CoverageTest): if env.PY3: arcz = ".1 12 23 34 45 36 63 57 7." else: - arcz = ".1 12 23 27 34 45 36 62 57 7." + arcz = ".1 12 23 34 45 36 62 57 7." self.check_coverage("""\ a, i = 1, 0 while True: @@ -341,6 +341,41 @@ class LoopArcTest(CoverageTest): """, arcz=arcz, arcz_missing="", arcz_unpredicted="") + def test_other_comprehensions(self): + # Generator expression: + self.check_coverage("""\ + o = ((1,2), (3,4)) + o = (a for a in o) + for tup in o: + x = tup[0] + y = tup[1] + """, + arcz=".1 .2 2-2 12 23 34 45 53 3.", + arcz_missing="", arcz_unpredicted="" + ) + # Set comprehension: + self.check_coverage("""\ + o = ((1,2), (3,4)) + o = {a for a in o} + for tup in o: + x = tup[0] + y = tup[1] + """, + arcz=".1 .2 2-2 12 23 34 45 53 3.", + arcz_missing="", arcz_unpredicted="" + ) + # Dict comprehension: + self.check_coverage("""\ + o = ((1,2), (3,4)) + o = {a:1 for a in o} + for tup in o: + x = tup[0] + y = tup[1] + """, + arcz=".1 .2 2-2 12 23 34 45 53 3.", + arcz_missing="", arcz_unpredicted="" + ) + class ExceptionArcTest(CoverageTest): """Arc-measuring tests involving exception handling.""" -- cgit v1.2.1 From f5acc8c5651287022e5b7d7d98e1be9393674c47 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 Dec 2015 16:39:17 -0500 Subject: Support exception arcs on py2, where the ast still has separate TryExcept and TryFinally nodes --HG-- branch : ast-branch --- coverage/parser.py | 44 +++++++++++++++++++++++++++----------------- tests/test_arcs.py | 8 ++------ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 36fa729c..d8b0beea 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -487,38 +487,48 @@ class AstArcAnalyzer(object): return set([]) def handle_Try(self, node): + return self.try_work(node, node.body, node.handlers, node.orelse, node.finalbody) + + def handle_TryExcept(self, node): + return self.try_work(node, node.body, node.handlers, node.orelse, None) + + def handle_TryFinally(self, node): + return self.try_work(node, node.body, None, None, node.finalbody) + + def try_work(self, node, body, handlers, orelse, finalbody): # 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]) + if handlers: + handler_start = self.line_for_node(handlers[0]) else: handler_start = None - if node.finalbody: - final_start = self.line_for_node(node.finalbody[0]) + if finalbody: + final_start = self.line_for_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) + exits = self.add_body_arcs(body, from_line=start) + try_block = self.block_stack.pop() handler_exits = set() - for handler_node in node.handlers: - handler_start = self.line_for_node(handler_node) - # TODO: handler_node.name and handler_node.type - handler_exits |= self.add_body_arcs(handler_node.body, from_line=handler_start) + if handlers: + for handler_node in handlers: + handler_start = self.line_for_node(handler_node) + # TODO: handler_node.name and handler_node.type + handler_exits |= self.add_body_arcs(handler_node.body, from_line=handler_start) # TODO: node.orelse exits |= handler_exits - if node.finalbody: - 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: + if finalbody: + final_from = exits | try_block.break_from | try_block.continue_from | try_block.raise_from | try_block.return_from + exits = self.add_body_arcs(finalbody, prev_lines=final_from) + if try_block.break_from: self.process_break_exits(exits) - if final_block.continue_from: + if try_block.continue_from: self.process_continue_exits(exits) - if final_block.raise_from: + if try_block.raise_from: self.process_raise_exits(exits) - if final_block.return_from: + if try_block.return_from: self.process_return_exits(exits) return exits diff --git a/tests/test_arcs.py b/tests/test_arcs.py index a371401f..fd4bd109 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -562,10 +562,6 @@ class ExceptionArcTest(CoverageTest): # "except Exception as e" is crucial here. def test_bug_212(self): - # Run this test only on Py2 for now. I hope to fix it on Py3 - # eventually... - if env.PY3: - self.skip("This doesn't work on Python 3") self.check_coverage("""\ def b(exc): try: @@ -582,8 +578,8 @@ class ExceptionArcTest(CoverageTest): except: pass """, - arcz=".1 .2 1A 23 34 56 67 68 8. AB BC C. DE E.", - arcz_missing="C.", arcz_unpredicted="45 7. CD") + arcz=".1 .2 1A 23 34 45 56 67 68 7. 8. AB BC C. DE E.", + arcz_missing="C.", arcz_unpredicted="CD") def test_except_finally(self): self.check_coverage("""\ -- cgit v1.2.1 From 704fa07b52715720da0f7b2d264ea41fce7441e8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 Dec 2015 18:24:36 -0500 Subject: Support classdef and some async keywords --HG-- branch : ast-branch --- coverage/parser.py | 58 ++++++++++++++++++++++++++++++++++++++---------------- tests/test_arcs.py | 8 +++++--- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index d8b0beea..d599bef9 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -6,6 +6,7 @@ import ast import collections import dis +import os import re import token import tokenize @@ -315,16 +316,17 @@ 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([]) + 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) + if int(os.environ.get("COVERAGE_ASTDUMP", 0)): + ast_dump(self.root_node) self.arcs = None self.block_stack = [] @@ -434,12 +436,17 @@ class AstArcAnalyzer(object): def handle_Break(self, node): here = self.line_for_node(node) self.process_break_exits([here]) - return set([]) + return set() + + def handle_ClassDef(self, node): + start = self.line_for_node(node) + # the body is handled in add_arcs_for_code_objects. + return set([start]) def handle_Continue(self, node): here = self.line_for_node(node) self.process_continue_exits([here]) - return set([]) + return set() def handle_For(self, node): start = self.line_for_node(node.iter) @@ -459,11 +466,14 @@ class AstArcAnalyzer(object): exits.add(start) return exits + handle_AsyncFor = handle_For + def handle_FunctionDef(self, node): start = self.line_for_node(node) # the body is handled in add_arcs_for_code_objects. - exits = set([start]) - return exits + return set([start]) + + handle_AsyncFunctionDef = handle_FunctionDef def handle_If(self, node): start = self.line_for_node(node.test) @@ -478,13 +488,13 @@ class AstArcAnalyzer(object): # `raise` statement jumps away, no exits from here. here = self.line_for_node(node) self.process_raise_exits([here]) - return set([]) + return set() def handle_Return(self, node): # TODO: deal with returning through a finally. here = self.line_for_node(node) self.process_return_exits([here]) - return set([]) + return set() def handle_Try(self, node): return self.try_work(node, node.body, node.handlers, node.orelse, node.finalbody) @@ -541,15 +551,17 @@ class AstArcAnalyzer(object): exits = self.add_body_arcs(node.body, from_line=start) for exit in exits: self.arcs.add((exit, to_top)) - # TODO: while loop that finishes? + exits = set() + if not constant_test: + exits.add(start) my_block = self.block_stack.pop() - exits = my_block.break_exits + exits.update(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", "Pass"]: + if node_name not in ["Assign", "Assert", "AugAssign", "Expr", "Import", "Pass", "Print"]: print("*** Unhandled: {}".format(node)) return set([self.line_for_node(node)]) @@ -565,19 +577,31 @@ class AstArcAnalyzer(object): exits = self.add_body_arcs(node.body, from_line=-1) for exit in exits: self.arcs.add((exit, -start)) - elif node_name == "FunctionDef": + elif node_name in ["FunctionDef", "AsyncFunctionDef"]: start = self.line_for_node(node) self.block_stack.append(FunctionBlock(start=start)) - func_exits = self.add_body_arcs(node.body, from_line=-1) + exits = self.add_body_arcs(node.body, from_line=-1) self.block_stack.pop() - for exit in func_exits: + for exit in exits: + self.arcs.add((exit, -start)) + elif node_name == "ClassDef": + start = self.line_for_node(node) + self.arcs.add((-1, start)) + exits = self.add_body_arcs(node.body, from_line=start) + for exit in exits: self.arcs.add((exit, -start)) elif node_name in self.CODE_COMPREHENSIONS: + # TODO: tests for when generators is more than one? for gen in node.generators: start = self.line_for_node(gen) self.arcs.add((-1, start)) self.arcs.add((start, -start)) # TODO: guaranteed this won't work for multi-line comps. + elif node_name == "Lambda": + start = self.line_for_node(node) + self.arcs.add((-1, start)) + self.arcs.add((start, -start)) + # TODO: test multi-line lambdas ## Opcodes that guide the ByteParser. diff --git a/tests/test_arcs.py b/tests/test_arcs.py index fd4bd109..08325f6b 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -260,7 +260,7 @@ class LoopArcTest(CoverageTest): if env.PY3: arcz = ".1 12 23 34 45 36 63 57 7." else: - arcz = ".1 12 23 34 45 36 62 57 7." + arcz = ".1 12 23 27 34 45 36 62 57 7." self.check_coverage("""\ a, i = 1, 0 while True: @@ -271,6 +271,7 @@ class LoopArcTest(CoverageTest): assert a == 4 and i == 3 """, arcz=arcz, + arcz_missing="", ) def test_for_if_else_for(self): @@ -764,7 +765,7 @@ class YieldTest(CoverageTest): def test_coroutines(self): self.check_coverage("""\ def double_inputs(): - while [1]: # avoid compiler differences + while len([1]): # avoid compiler differences x = yield x *= 2 yield x @@ -890,8 +891,9 @@ class AsyncTest(CoverageTest): ".H HI IH HJ J-G " # doit ".5 5-4 " # __init__ ".8 8-7 " # __aiter__ - ".B BC C-A DE ", # __anext__ + ".B BC C-A DE E-A ", # __anext__ arcz_missing="", + arcz_unpredicted="CD", ) self.assertEqual(self.stdout(), "a\nb\nc\n.\n") -- cgit v1.2.1 From 334f95902f91e54e60600072d7e1816670627718 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 1 Jan 2016 10:53:45 -0500 Subject: Support 'with' --HG-- branch : ast-branch --- coverage/parser.py | 26 +++++++++++++++++--------- tests/test_arcs.py | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index d599bef9..a5e12d35 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -264,16 +264,17 @@ class PythonParser(object): return self._all_arcs def arcs(self): - aaa = AstArcAnalyzer(self.text) - arcs = aaa.collect_arcs() + if self._all_arcs is None: + aaa = AstArcAnalyzer(self.text) + arcs = aaa.collect_arcs() - arcs_ = set() - for l1, l2 in arcs: - fl1 = self.first_line(l1) - fl2 = self.first_line(l2) - if fl1 != fl2: - arcs_.add((fl1, fl2)) - return arcs_ + self._all_arcs = set() + for l1, l2 in arcs: + fl1 = self.first_line(l1) + fl2 = self.first_line(l2) + if fl1 != fl2: + self._all_arcs.add((fl1, fl2)) + return self._all_arcs def exit_counts(self): """Get a count of exits from that each line. @@ -559,6 +560,13 @@ class AstArcAnalyzer(object): # TODO: orelse return exits + def handle_With(self, node): + start = self.line_for_node(node) + exits = self.add_body_arcs(node.body, from_line=start) + return exits + + handle_AsyncWith = handle_With + def handle_default(self, node): node_name = node.__class__.__name__ if node_name not in ["Assign", "Assert", "AugAssign", "Expr", "Import", "Pass", "Print"]: diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 08325f6b..3dc05c9c 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -912,7 +912,7 @@ class AsyncTest(CoverageTest): x11 = 1 x12 = 1 """, - arcz=".1 1. .2 23 2C 34 45 56 6B", + arcz=".1 1. .2 23 2C 34 45 48 54 56 6B 8B 9A AB B2 C.", ) def test_async_with(self): -- cgit v1.2.1 From 9edd625b8fdb09b5494471d460eba11148104e28 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 1 Jan 2016 12:18:57 -0500 Subject: All test_arcs.py tests pass on py27 and py35 --HG-- branch : ast-branch --- coverage/parser.py | 30 +++++++++++++++++++++--------- tests/test_arcs.py | 25 ++++++++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index a5e12d35..b2618921 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -339,13 +339,27 @@ class AstArcAnalyzer(object): def line_for_node(self, node): """What is the right line number to use for this node?""" node_name = node.__class__.__name__ - if node_name == "Assign": - return node.value.lineno - elif node_name == "comprehension": - # TODO: is this how to get the line number for a comprehension? - return node.target.lineno - else: - return node.lineno + handler = getattr(self, "line_" + node_name, self.line_default) + return handler(node) + + def line_Assign(self, node): + return self.line_for_node(node.value) + + def line_Dict(self, node): + # Python 3.5 changed how dict literals are made. + if env.PYVERSION >= (3, 5): + return node.keys[0].lineno + return node.lineno + + def line_List(self, node): + return self.line_for_node(node.elts[0]) + + def line_comprehension(self, node): + # TODO: is this how to get the line number for a comprehension? + return node.target.lineno + + def line_default(self, node): + return node.lineno def collect_arcs(self): self.arcs = set() @@ -358,8 +372,6 @@ class AstArcAnalyzer(object): Return a set of line numbers, exits from this node to the next. """ node_name = node.__class__.__name__ - #print("Adding arcs for {}".format(node_name)) - handler = getattr(self, "handle_" + node_name, self.handle_default) return handler(node) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 3dc05c9c..a9533e78 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -804,7 +804,21 @@ class MiscArcTest(CoverageTest): } assert d """, - arcz=arcz) + arcz=arcz, + ) + self.check_coverage("""\ + d = \\ + { 'a': 2, + 'b': 3, + 'c': { + 'd': 5, + 'e': 6, + } + } + assert d + """, + arcz=".1 19 9-2", + ) def test_pathologically_long_code_object(self): # https://bitbucket.org/ned/coveragepy/issue/359 @@ -814,17 +828,18 @@ class MiscArcTest(CoverageTest): code = """\ data = [ """ + "".join("""\ - [{i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}], + [ + {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}], """.format(i=i) for i in range(2000) ) + """\ ] - if __name__ == "__main__": - print(len(data)) + print(len(data)) """ self.check_coverage( code, - arcs=[(-1, 1), (1, 2004), (2004, -2), (2004, 2005), (2005, -2)], + arcs=[(-1, 1), (1, 4004), (4004, -3)], + arcs_missing=[], arcs_unpredicted=[], ) -- cgit v1.2.1 From d6e221c8058259460cadfe62d5ca1bb0d93822cc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 1 Jan 2016 13:38:06 -0500 Subject: test_arcs now passes for all Python versions --HG-- branch : ast-branch --- coverage/parser.py | 7 ++++++- tests/test_arcs.py | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index b2618921..33480924 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -388,7 +388,12 @@ class AstArcAnalyzer(object): def is_constant_expr(self, node): """Is this a compile-time constant?""" node_name = node.__class__.__name__ - return node_name in ["NameConstant", "Num"] + if node_name in ["NameConstant", "Num"]: + return True + elif node_name == "Name": + if env.PY3 and node.id in ["True", "False", "None"]: + return True + return False # tests to write: # TODO: while EXPR: diff --git a/tests/test_arcs.py b/tests/test_arcs.py index a9533e78..ea79d495 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -342,7 +342,7 @@ class LoopArcTest(CoverageTest): """, arcz=arcz, arcz_missing="", arcz_unpredicted="") - def test_other_comprehensions(self): + def test_generator_expression(self): # Generator expression: self.check_coverage("""\ o = ((1,2), (3,4)) @@ -354,6 +354,10 @@ class LoopArcTest(CoverageTest): arcz=".1 .2 2-2 12 23 34 45 53 3.", arcz_missing="", arcz_unpredicted="" ) + + def test_other_comprehensions(self): + if env.PYVERSION < (2, 7): + self.skip("Don't have set or dict comprehensions before 2.7") # Set comprehension: self.check_coverage("""\ o = ((1,2), (3,4)) -- cgit v1.2.1 From c9464bbc696d393799c0989e4ca132987cc2fbb3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 1 Jan 2016 13:39:29 -0500 Subject: Remove temporary ast_differs --HG-- branch : ast-branch --- tests/coveragetest.py | 1 - tests/test_arcs.py | 4 ---- 2 files changed, 5 deletions(-) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 5f85e75c..3468b794 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -165,7 +165,6 @@ 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`. diff --git a/tests/test_arcs.py b/tests/test_arcs.py index ea79d495..a64ab89e 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -407,7 +407,6 @@ class ExceptionArcTest(CoverageTest): """, arcz=".1 12 23 34 46 58 67 78 8.", arcz_missing="58", - ast_differs=True, ) def test_hidden_raise(self): @@ -426,7 +425,6 @@ class ExceptionArcTest(CoverageTest): """, 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): @@ -507,7 +505,6 @@ class ExceptionArcTest(CoverageTest): """, 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 @@ -676,7 +673,6 @@ class ExceptionArcTest(CoverageTest): 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 ) -- cgit v1.2.1 From f1e583f91035983237d248b417b8ca9831ceac39 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 1 Jan 2016 16:10:50 -0500 Subject: check_coverage now assumes empty missing and unpredicted, and uses branch always --HG-- branch : ast-branch --- coverage/parser.py | 8 ++++++-- tests/coveragetest.py | 8 ++++---- tests/test_arcs.py | 22 ++++++++++++++++++++-- tests/test_coverage.py | 17 +++++++++++------ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 33480924..2396fb8c 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -347,7 +347,7 @@ class AstArcAnalyzer(object): def line_Dict(self, node): # Python 3.5 changed how dict literals are made. - if env.PYVERSION >= (3, 5): + if env.PYVERSION >= (3, 5) and node.keys: return node.keys[0].lineno return node.lineno @@ -587,7 +587,7 @@ class AstArcAnalyzer(object): def handle_default(self, node): node_name = node.__class__.__name__ if node_name not in ["Assign", "Assert", "AugAssign", "Expr", "Import", "Pass", "Print"]: - print("*** Unhandled: {}".format(node)) + print("*** Unhandled: {0}".format(node)) return set([self.line_for_node(node)]) CODE_COMPREHENSIONS = set(["GeneratorExp", "DictComp", "SetComp"]) @@ -1049,6 +1049,10 @@ SKIP_FIELDS = ["ctx"] def ast_dump(node, depth=0): indent = " " * depth + if not isinstance(node, ast.AST): + print("{0}<{1} {2!r}>".format(indent, node.__class__.__name__, node)) + return + lineno = getattr(node, "lineno", None) if lineno is not None: linemark = " @ {0}".format(lineno) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 3468b794..28d6616b 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -191,10 +191,10 @@ class CoverageTest( if arcs is None and arcz is not None: arcs = self.arcz_to_arcs(arcz) - if arcs_missing is None and arcz_missing is not None: - arcs_missing = self.arcz_to_arcs(arcz_missing) - if arcs_unpredicted is None and arcz_unpredicted is not None: - arcs_unpredicted = self.arcz_to_arcs(arcz_unpredicted) + if arcs_missing is None:# and arcz_missing is not None: + arcs_missing = self.arcz_to_arcs(arcz_missing or "") + if arcs_unpredicted is None:# and arcz_unpredicted is not None: + arcs_unpredicted = self.arcz_to_arcs(arcz_unpredicted or "") branch = any(x is not None for x in [arcs, arcs_missing, arcs_unpredicted]) # Start up coverage.py. diff --git a/tests/test_arcs.py b/tests/test_arcs.py index a64ab89e..1f1bdd1d 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -653,7 +653,7 @@ 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", # TODO: 46 can be predicted. ) self.check_coverage("""\ a, b, c = 1, 1, 1 @@ -672,7 +672,7 @@ class ExceptionArcTest(CoverageTest): """, 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", + arcz_unpredicted="45 57 7A AB", # TODO: 57 7A can be predicted. ) @@ -783,6 +783,24 @@ class YieldTest(CoverageTest): arcz_unpredicted="") self.assertEqual(self.stdout(), "20\n12\n") + def test_yield_from(self): + if env.PYVERSION < (3, 3): + self.skip("Python before 3.3 doesn't have 'yield from'") + self.check_coverage("""\ + def gen(inp): + i = 2 + for n in inp: + i = 4 + yield from range(3) + i = 6 + i = 7 + + list(gen([1,2,3])) + """, + arcz=".1 19 9. .2 23 34 45 56 5. 63 37 7.", + arcz_missing="", + arcz_unpredicted="") + class MiscArcTest(CoverageTest): """Miscellaneous arc-measuring tests.""" diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 9e2a444c..3a27fab3 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -100,7 +100,7 @@ class BasicCoverageTest(CoverageTest): # Nothing here d = 6 """, - [1,2,4,6], report="4 0 100%") + [1,2,4,6], report="4 0 0 0 100%") def test_indentation_wackiness(self): # Partial final lines are OK. @@ -617,7 +617,8 @@ class CompoundStatementTest(CoverageTest): z = 7 assert x == 3 """, - [1,2,3,4,5,7,8], "4-7", report="7 3 57% 4-7") + [1,2,3,4,5,7,8], "4-7", report="7 3 4 1 45% 4-7, 2->4", + ) self.check_coverage("""\ a = 1; b = 2; c = 3; if a != 1: @@ -628,7 +629,8 @@ class CompoundStatementTest(CoverageTest): z = 7 assert y == 5 """, - [1,2,3,4,5,7,8], "3, 7", report="7 2 71% 3, 7") + [1,2,3,4,5,7,8], "3, 7", report="7 2 4 2 64% 3, 7, 2->3, 4->7", + ) self.check_coverage("""\ a = 1; b = 2; c = 3; if a != 1: @@ -639,7 +641,8 @@ class CompoundStatementTest(CoverageTest): z = 7 assert z == 7 """, - [1,2,3,4,5,7,8], "3, 5", report="7 2 71% 3, 5") + [1,2,3,4,5,7,8], "3, 5", report="7 2 4 2 64% 3, 5, 2->3, 4->5", + ) def test_elif_no_else(self): self.check_coverage("""\ @@ -650,7 +653,8 @@ class CompoundStatementTest(CoverageTest): y = 5 assert x == 3 """, - [1,2,3,4,5,6], "4-5", report="6 2 67% 4-5") + [1,2,3,4,5,6], "4-5", report="6 2 4 1 50% 4-5, 2->4", + ) self.check_coverage("""\ a = 1; b = 2; c = 3; if a != 1: @@ -659,7 +663,8 @@ class CompoundStatementTest(CoverageTest): y = 5 assert y == 5 """, - [1,2,3,4,5,6], "3", report="6 1 83% 3") + [1,2,3,4,5,6], "3", report="6 1 4 2 70% 3, 2->3, 4->6", + ) def test_elif_bizarre(self): self.check_coverage("""\ -- cgit v1.2.1 From 6f69dc8997ba560a7d8e7b820d692d452b5d24e7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 1 Jan 2016 16:29:01 -0500 Subject: Clean up after making arcz_missing and arcz_unpredicted default to empty. --HG-- branch : ast-branch --- tests/coveragetest.py | 40 ++++++++++++-------------- tests/test_arcs.py | 80 ++++++++++++++++++++++++--------------------------- 2 files changed, 57 insertions(+), 63 deletions(-) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 28d6616b..9d2ae1a2 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -163,7 +163,7 @@ class CoverageTest( def check_coverage( self, text, lines=None, missing="", report="", excludes=None, partials="", - arcz=None, arcz_missing=None, arcz_unpredicted=None, + arcz=None, arcz_missing="", arcz_unpredicted="", arcs=None, arcs_missing=None, arcs_unpredicted=None, ): """Check the coverage measurement of `text`. @@ -175,10 +175,11 @@ class CoverageTest( of the measurement report. For arc measurement, `arcz` is a string that can be decoded into arcs - in the code (see `arcz_to_arcs` for the encoding scheme), + in the code (see `arcz_to_arcs` for the encoding scheme). `arcz_missing` are the arcs that are not executed, and - `arcs_unpredicted` are the arcs executed in the code, but not deducible - from the code. + `arcz_unpredicted` are the arcs executed in the code, but not deducible + from the code. These last two default to "", meaning we explicitly + check that there are no missing or unpredicted arcs. Returns the Coverage object, in case you want to poke at it some more. @@ -191,14 +192,13 @@ class CoverageTest( if arcs is None and arcz is not None: arcs = self.arcz_to_arcs(arcz) - if arcs_missing is None:# and arcz_missing is not None: - arcs_missing = self.arcz_to_arcs(arcz_missing or "") - if arcs_unpredicted is None:# and arcz_unpredicted is not None: - arcs_unpredicted = self.arcz_to_arcs(arcz_unpredicted or "") - branch = any(x is not None for x in [arcs, arcs_missing, arcs_unpredicted]) + if arcs_missing is None: + arcs_missing = self.arcz_to_arcs(arcz_missing) + if arcs_unpredicted is None: + arcs_unpredicted = self.arcz_to_arcs(arcz_unpredicted) # Start up coverage.py. - cov = coverage.Coverage(branch=branch) + cov = coverage.Coverage(branch=True) cov.erase() for exc in excludes or []: cov.exclude(exc) @@ -240,17 +240,15 @@ class CoverageTest( if arcs is not None: self.assert_equal_args(analysis.arc_possibilities(), arcs, "Possible arcs differ") - if arcs_missing is not None: - self.assert_equal_args( - analysis.arcs_missing(), arcs_missing, - "Missing arcs differ" - ) - - if arcs_unpredicted is not None: - self.assert_equal_args( - analysis.arcs_unpredicted(), arcs_unpredicted, - "Unpredicted arcs differ" - ) + self.assert_equal_args( + analysis.arcs_missing(), arcs_missing, + "Missing arcs differ" + ) + + self.assert_equal_args( + analysis.arcs_unpredicted(), arcs_unpredicted, + "Unpredicted arcs differ" + ) if report: frep = StringIO() diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 1f1bdd1d..a55b5d39 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -87,7 +87,8 @@ class SimpleArcTest(CoverageTest): if len([]) == 0: a = 2 assert a == 2 """, - arcz=".1 12 23 3.", arcz_missing="") + arcz=".1 12 23 3.", + ) self.check_coverage("""\ def fn(x): if x % 2: return True @@ -106,7 +107,8 @@ class SimpleArcTest(CoverageTest): b = \\ 6 """, - arcz=".1 15 5-2", arcz_missing="") + arcz=".1 15 5-2", + ) def test_if_return(self): self.check_coverage("""\ @@ -118,8 +120,8 @@ class SimpleArcTest(CoverageTest): x = if_ret(0) + if_ret(1) assert x == 8 """, - arcz=".1 16 67 7. .2 23 24 3. 45 5.", arcz_missing="" - ) + arcz=".1 16 67 7. .2 23 24 3. 45 5.", + ) def test_dont_confuse_exit_and_else(self): self.check_coverage("""\ @@ -192,7 +194,8 @@ class LoopArcTest(CoverageTest): a = i assert a == 9 """, - arcz=".1 12 21 13 3.", arcz_missing="") + arcz=".1 12 21 13 3.", + ) self.check_coverage("""\ a = -1 for i in range(0): @@ -208,7 +211,8 @@ class LoopArcTest(CoverageTest): a = i + j assert a == 4 """, - arcz=".1 12 23 32 21 14 4.", arcz_missing="") + arcz=".1 12 23 32 21 14 4.", + ) def test_break(self): self.check_coverage("""\ @@ -271,8 +275,7 @@ class LoopArcTest(CoverageTest): assert a == 4 and i == 3 """, arcz=arcz, - arcz_missing="", - ) + ) def test_for_if_else_for(self): self.check_coverage("""\ @@ -329,7 +332,8 @@ class LoopArcTest(CoverageTest): x = tup[0] y = tup[1] """, - arcz=arcz, arcz_missing="", arcz_unpredicted="") + arcz=arcz, + ) if env.PY3: arcz = ".1 12 .2 2-2 23 34 42 2." else: @@ -340,7 +344,8 @@ class LoopArcTest(CoverageTest): x = tup[0] y = tup[1] """, - arcz=arcz, arcz_missing="", arcz_unpredicted="") + arcz=arcz, + ) def test_generator_expression(self): # Generator expression: @@ -352,7 +357,6 @@ class LoopArcTest(CoverageTest): y = tup[1] """, arcz=".1 .2 2-2 12 23 34 45 53 3.", - arcz_missing="", arcz_unpredicted="" ) def test_other_comprehensions(self): @@ -367,7 +371,6 @@ class LoopArcTest(CoverageTest): y = tup[1] """, arcz=".1 .2 2-2 12 23 34 45 53 3.", - arcz_missing="", arcz_unpredicted="" ) # Dict comprehension: self.check_coverage("""\ @@ -378,7 +381,6 @@ class LoopArcTest(CoverageTest): y = tup[1] """, arcz=".1 .2 2-2 12 23 34 45 53 3.", - arcz_missing="", arcz_unpredicted="" ) @@ -445,8 +447,8 @@ class ExceptionArcTest(CoverageTest): assert try_it(1) == 7 # D """, 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="8A") + arcz_unpredicted="8A", + ) def test_try_finally(self): self.check_coverage("""\ @@ -457,7 +459,8 @@ class ExceptionArcTest(CoverageTest): c = 5 assert a == 3 and c == 5 """, - arcz=".1 12 23 35 56 6.", arcz_missing="") + arcz=".1 12 23 35 56 6.", + ) self.check_coverage("""\ a, c, d = 1, 1, 1 try: @@ -470,7 +473,8 @@ class ExceptionArcTest(CoverageTest): assert a == 4 and c == 6 and d == 1 # 9 """, arcz=".1 12 23 34 46 78 89 69 9.", - arcz_missing="78 89", arcz_unpredicted="") + arcz_missing="78 89", + ) self.check_coverage("""\ a, c, d = 1, 1, 1 try: @@ -485,7 +489,8 @@ class ExceptionArcTest(CoverageTest): assert a == 4 and c == 8 and d == 10 # B """, arcz=".1 12 23 34 45 58 68 89 8B 9A AB B.", - arcz_missing="68 8B", arcz_unpredicted="") + arcz_missing="68 8B", + ) def test_finally_in_loop(self): self.check_coverage("""\ @@ -522,7 +527,7 @@ class ExceptionArcTest(CoverageTest): assert a == 8 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="67 7A AB BC CD", arcz_unpredicted="", + arcz_missing="67 7A AB BC CD", ) @@ -543,12 +548,9 @@ class ExceptionArcTest(CoverageTest): 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_missing="3D BC CD", arcz_unpredicted="", + arcz_missing="3D BC CD", ) - # 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("""\ for i in range(5): @@ -560,7 +562,7 @@ class ExceptionArcTest(CoverageTest): h = 7 """, arcz=".1 12 23 35 56 61 17 7.", - arcz_missing="", arcz_unpredicted="") + ) # "except Exception as e" is crucial here. def test_bug_212(self): @@ -688,8 +690,7 @@ class YieldTest(CoverageTest): list(gen([1,2,3])) """, arcz=".1 .2 23 2. 32 15 5.", - arcz_missing="", - arcz_unpredicted="") + ) def test_padded_yield_in_loop(self): self.check_coverage("""\ @@ -704,8 +705,7 @@ class YieldTest(CoverageTest): list(gen([1,2,3])) """, arcz=".1 19 9. .2 23 34 45 56 63 37 7.", - arcz_missing="", - arcz_unpredicted="") + ) def test_bug_308(self): self.check_coverage("""\ @@ -717,8 +717,7 @@ class YieldTest(CoverageTest): print(f()) """, arcz=".1 15 56 65 5. .2 23 32 2. .3 3-3", - arcz_missing="", - arcz_unpredicted="") + ) self.check_coverage("""\ def run(): @@ -730,8 +729,7 @@ class YieldTest(CoverageTest): print(f()) """, arcz=".1 16 67 76 6. .2 23 34 43 3. 2-2 .4 4-4", - arcz_missing="", - arcz_unpredicted="") + ) self.check_coverage("""\ def run(): @@ -741,8 +739,7 @@ class YieldTest(CoverageTest): print(f()) """, arcz=".1 14 45 54 4. .2 2. 2-2", - arcz_missing="", - arcz_unpredicted="") + ) def test_bug_324(self): # This code is tricky: the list() call pulls all the values from gen(), @@ -760,7 +757,7 @@ class YieldTest(CoverageTest): ".2 23 32 2. " # The gen() function ".3 3-3", # The generator expression arcz_missing=".3 3-3", - arcz_unpredicted="") + ) def test_coroutines(self): self.check_coverage("""\ @@ -780,7 +777,7 @@ class YieldTest(CoverageTest): ".1 17 78 89 9A AB B. " ".2 23 34 45 52 2.", arcz_missing="2.", - arcz_unpredicted="") + ) self.assertEqual(self.stdout(), "20\n12\n") def test_yield_from(self): @@ -798,8 +795,7 @@ class YieldTest(CoverageTest): list(gen([1,2,3])) """, arcz=".1 19 9. .2 23 34 45 56 5. 63 37 7.", - arcz_missing="", - arcz_unpredicted="") + ) class MiscArcTest(CoverageTest): @@ -888,7 +884,6 @@ class AsyncTest(CoverageTest): ".1 13 38 8C CD DE E. " ".4 45 56 6-3 " ".9 9A A-8", - arcz_missing="", ) self.assertEqual(self.stdout(), "Compute 1 + 2 ...\n1 + 2 = 3\n") @@ -925,7 +920,6 @@ class AsyncTest(CoverageTest): ".5 5-4 " # __init__ ".8 8-7 " # __aiter__ ".B BC C-A DE E-A ", # __anext__ - arcz_missing="", arcz_unpredicted="CD", ) self.assertEqual(self.stdout(), "a\nb\nc\n.\n") @@ -1053,7 +1047,8 @@ class ExcludeTest(CoverageTest): f = 9 """, [1,2,3,4,5,6,7,8,9], - arcz=".1 12 23 24 34 45 56 57 67 78 89 9. 8.", arcz_missing="") + arcz=".1 12 23 24 34 45 56 57 67 78 89 9. 8.", + ) def test_custom_pragmas(self): self.check_coverage("""\ @@ -1065,7 +1060,8 @@ class ExcludeTest(CoverageTest): """, [1,2,3,4,5], partials=["only some"], - arcz=".1 12 23 34 45 25 5.", arcz_missing="") + arcz=".1 12 23 34 45 25 5.", + ) class LineDataTest(CoverageTest): -- cgit v1.2.1 From d7cae8f7aeafd87a1e665b60035ac173c5de2187 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 1 Jan 2016 17:51:30 -0500 Subject: Remove some async tests we aren't going to use --HG-- branch : ast-branch --- tests/test_arcs.py | 100 ++--------------------------------------------------- 1 file changed, 3 insertions(+), 97 deletions(-) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index a55b5d39..303b10e6 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -860,7 +860,7 @@ class MiscArcTest(CoverageTest): class AsyncTest(CoverageTest): def setUp(self): if env.PYVERSION < (3, 5): - self.skip("No point testing 3.5 syntax below 3.5") + self.skip("Async features are new in Python 3.5") super(AsyncTest, self).setUp() def test_async(self): @@ -924,111 +924,17 @@ class AsyncTest(CoverageTest): ) self.assertEqual(self.stdout(), "a\nb\nc\n.\n") - def test_async_for2(self): - self.check_coverage("""\ - async def go1(): - async for x2 in y2: - try: - async for x4 in y4: - if a5: - break - else: - x8 = 1 - except: - x10 = 1 - x11 = 1 - x12 = 1 - """, - arcz=".1 1. .2 23 2C 34 45 48 54 56 6B 8B 9A AB B2 C.", - ) - def test_async_with(self): self.check_coverage("""\ async def go(): async with x: pass """, + # TODO: we don't run any code, so many arcs are missing. arcz=".1 1. .2 23 3.", + arcz_missing=".2 23 3.", ) - def test_async_it(self): - self.check_coverage("""\ - async def func(): - for x in g2: - x = 3 - else: - x = 5 - """, - arcz=".1 1. .2 23 32 25 5.", - ) - self.check_coverage("""\ - async def func(): - async for x in g2: - x = 3 - else: - x = 5 - """, - arcz=".1 1. .2 23 32 25 5.", - ) - - def xxxx_async_is_same_flow(self): - SOURCE = """\ - async def func(): - for x in g2: - try: - x = g4 - finally: - x = g6 - try: - with g8 as x: - x = g9 - continue - finally: - x = g12 - for x in g13: - continue - else: - break - while g17: - x = g18 - continue - else: - x = g21 - for x in g22: - x = g23 - continue - """ - - parts = re.split(r"(for |with )", SOURCE) - nchoices = len(parts) // 2 - - def only(s): - return [s] - - def maybe_async(s): - return [s, "async "+s] - - all_all_arcs = collections.defaultdict(list) - choices = [f(x) for f, x in zip(cycle([only, maybe_async]), parts)] - for i, result in enumerate(product(*choices)): - source = "".join(result) - self.make_file("async.py", source) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "async") - analysis = cov._analyze("async.py") - all_all_arcs[tuple(analysis.arc_possibilities())].append((i, source)) - - import pprint - pprint.pprint(list(all_all_arcs.keys())) - for arcs, numbers in all_all_arcs.items(): - print(" ".join("{0:0{1}b}".format(x[0], nchoices) for x in numbers)) - print(" {}".format(arcs)) - for i, source in numbers: - print("-" * 80) - print(source) - - assert len(all_all_arcs) == 1 - class ExcludeTest(CoverageTest): """Tests of exclusions to indicate known partial branches.""" -- cgit v1.2.1 From 6eb046c5937b9c78dab3451fae9348c4c721d6f9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 10:18:04 -0500 Subject: Handle yield-from and await. All tests pass --HG-- branch : ast-branch --- coverage/parser.py | 88 +++++++++++++++++++++++++++++++++--------------- coverage/test_helpers.py | 12 +++---- tests/test_arcs.py | 20 ++++++----- 3 files changed, 77 insertions(+), 43 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 2396fb8c..0462802e 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -327,11 +327,17 @@ class AstArcAnalyzer(object): def __init__(self, text): self.root_node = ast.parse(text) if int(os.environ.get("COVERAGE_ASTDUMP", 0)): + # Dump the AST so that failing tests have helpful output. ast_dump(self.root_node) self.arcs = None self.block_stack = [] + def collect_arcs(self): + self.arcs = set() + self.add_arcs_for_code_objects(self.root_node) + return self.arcs + def blocks(self): """Yield the blocks in nearest-to-farthest order.""" return reversed(self.block_stack) @@ -361,16 +367,19 @@ class AstArcAnalyzer(object): def line_default(self, node): return node.lineno - def collect_arcs(self): - self.arcs = set() - self.add_arcs_for_code_objects(self.root_node) - return self.arcs - def add_arcs(self, node): - """add the arcs for `node`. + """Add the arcs for `node`. Return a set of line numbers, exits from this node to the next. """ + # Yield-froms and awaits can appear anywhere. + # TODO: this is probably over-doing it, and too expensive. Can we + # instrument the ast walking to see how many nodes we are revisiting? + if isinstance(node, ast.stmt): + for name, value in ast.iter_fields(node): + if isinstance(value, ast.expr) and self.contains_return_expression(value): + self.process_return_exits([self.line_for_node(node)]) + break node_name = node.__class__.__name__ handler = getattr(self, "handle_" + node_name, self.handle_default) return handler(node) @@ -404,6 +413,7 @@ class AstArcAnalyzer(object): # TODO: multi-line listcomps # TODO: nested function definitions # TODO: multiple `except` clauses + # TODO: return->finally def process_break_exits(self, exits): for block in self.blocks(): @@ -443,6 +453,7 @@ class AstArcAnalyzer(object): def process_return_exits(self, exits): for block in self.blocks(): + # TODO: need a check here for TryBlock if isinstance(block, FunctionBlock): # TODO: what if there is no enclosing function? for exit in exits: @@ -587,6 +598,7 @@ class AstArcAnalyzer(object): def handle_default(self, node): node_name = node.__class__.__name__ if node_name not in ["Assign", "Assert", "AugAssign", "Expr", "Import", "Pass", "Print"]: + # TODO: put 1/0 here to find unhandled nodes. print("*** Unhandled: {0}".format(node)) return set([self.line_for_node(node)]) @@ -628,6 +640,14 @@ class AstArcAnalyzer(object): self.arcs.add((start, -start)) # TODO: test multi-line lambdas + def contains_return_expression(self, node): + """Is there a yield-from or await in `node` someplace?""" + for child in ast.walk(node): + if child.__class__.__name__ in ["YieldFrom", "Await"]: + return True + + return False + ## Opcodes that guide the ByteParser. @@ -1045,7 +1065,13 @@ class Chunk(object): ) -SKIP_FIELDS = ["ctx"] +SKIP_DUMP_FIELDS = ["ctx"] + +def is_simple_value(value): + return ( + value in [None, [], (), {}, set()] or + isinstance(value, (string_class, int, float)) + ) def ast_dump(node, depth=0): indent = " " * depth @@ -1055,30 +1081,36 @@ def ast_dump(node, depth=0): lineno = getattr(node, "lineno", None) if lineno is not None: - linemark = " @ {0}".format(lineno) + linemark = " @ {0}".format(node.lineno) else: linemark = "" - print("{0}<{1}{2}".format(indent, node.__class__.__name__, linemark)) - - indent += " " - for field_name, value in ast.iter_fields(node): - if field_name in SKIP_FIELDS: - continue - prefix = "{0}{1}:".format(indent, field_name) - if value is None: - print("{0} None".format(prefix)) - elif isinstance(value, (string_class, int, float)): - print("{0} {1!r}".format(prefix, value)) - elif isinstance(value, list): - if value == []: - print("{0} []".format(prefix)) - else: + head = "{0}<{1}{2}".format(indent, node.__class__.__name__, linemark) + + named_fields = [ + (name, value) + for name, value in ast.iter_fields(node) + if name not in SKIP_DUMP_FIELDS + ] + if not named_fields: + print("{0}>".format(head)) + elif len(named_fields) == 1 and is_simple_value(named_fields[0][1]): + field_name, value = named_fields[0] + print("{0} {1}: {2!r}>".format(head, field_name, value)) + else: + print(head) + print("{0}# mro: {1}".format(indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]))) + next_indent = indent + " " + for field_name, value in named_fields: + prefix = "{0}{1}:".format(next_indent, field_name) + if is_simple_value(value): + print("{0} {1!r}".format(prefix, value)) + elif isinstance(value, list): print("{0} [".format(prefix)) for n in value: ast_dump(n, depth + 8) - print("{0}]".format(indent)) - else: - print(prefix) - ast_dump(value, depth + 8) + print("{0}]".format(next_indent)) + else: + print(prefix) + ast_dump(value, depth + 8) - print("{0}>".format(" " * depth)) + print("{0}>".format(indent)) diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py index 50cc3298..092daa07 100644 --- a/coverage/test_helpers.py +++ b/coverage/test_helpers.py @@ -162,20 +162,20 @@ class StdStreamCapturingMixin(TestCase): # nose keeps stdout from littering the screen, so we can safely Tee it, # but it doesn't capture stderr, so we don't want to Tee stderr to the # real stderr, since it will interfere with our nice field of dots. - self.old_stdout = sys.stdout + old_stdout = sys.stdout self.captured_stdout = StringIO() sys.stdout = Tee(sys.stdout, self.captured_stdout) - self.old_stderr = sys.stderr + old_stderr = sys.stderr self.captured_stderr = StringIO() sys.stderr = self.captured_stderr - self.addCleanup(self.cleanup_std_streams) + self.addCleanup(self.cleanup_std_streams, old_stdout, old_stderr) - def cleanup_std_streams(self): + def cleanup_std_streams(self, old_stdout, old_stderr): """Restore stdout and stderr.""" - sys.stdout = self.old_stdout - sys.stderr = self.old_stderr + sys.stdout = old_stdout + sys.stderr = old_stderr def stdout(self): """Return the data written to stdout during the test.""" diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 303b10e6..6ba663bc 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -867,23 +867,25 @@ class AsyncTest(CoverageTest): self.check_coverage("""\ import asyncio - async def compute(x, y): + async def compute(x, y): # 3 print("Compute %s + %s ..." % (x, y)) await asyncio.sleep(0.001) - return x + y + return x + y # 6 - async def print_sum(x, y): - result = await compute(x, y) + async def print_sum(x, y): # 8 + result = (0 + + await compute(x, y) # A + ) print("%s + %s = %s" % (x, y, result)) - loop = asyncio.get_event_loop() + loop = asyncio.get_event_loop() # E loop.run_until_complete(print_sum(1, 2)) - loop.close() + loop.close() # G """, arcz= - ".1 13 38 8C CD DE E. " - ".4 45 56 6-3 " - ".9 9A A-8", + ".1 13 38 8E EF FG G. " + ".4 45 56 5-3 6-3 " + ".9 9-8 9C C-8", ) self.assertEqual(self.stdout(), "Compute 1 + 2 ...\n1 + 2 = 3\n") -- cgit v1.2.1 From fa02e8b1d5f985c468d9c15869e092394298a41b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 11:09:10 -0500 Subject: Deal with a few more cases the test suite didn't turn up --HG-- branch : ast-branch --- coverage/parser.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 0462802e..fc631fcc 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -355,10 +355,23 @@ class AstArcAnalyzer(object): # Python 3.5 changed how dict literals are made. if env.PYVERSION >= (3, 5) and node.keys: return node.keys[0].lineno - return node.lineno + else: + return node.lineno def line_List(self, node): - return self.line_for_node(node.elts[0]) + if node.elts: + return self.line_for_node(node.elts[0]) + else: + # TODO: test case for this branch: x = [] + return node.lineno + + def line_Module(self, node): + if node.body: + return self.line_for_node(node.body[0]) + else: + # Modules have no line number, they always start at 1. + # TODO: test case for empty module. + return 1 def line_comprehension(self, node): # TODO: is this how to get the line number for a comprehension? @@ -595,9 +608,14 @@ class AstArcAnalyzer(object): handle_AsyncWith = handle_With + OK_TO_DEFAULT = set([ + "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global", + "Import", "ImportFrom", "Pass", "Print", + ]) + def handle_default(self, node): node_name = node.__class__.__name__ - if node_name not in ["Assign", "Assert", "AugAssign", "Expr", "Import", "Pass", "Print"]: + if node_name not in self.OK_TO_DEFAULT: # TODO: put 1/0 here to find unhandled nodes. print("*** Unhandled: {0}".format(node)) return set([self.line_for_node(node)]) @@ -610,7 +628,7 @@ class AstArcAnalyzer(object): for node in ast.walk(root_node): node_name = node.__class__.__name__ if node_name == "Module": - start = self.line_for_node(node.body[0]) + start = self.line_for_node(node) exits = self.add_body_arcs(node.body, from_line=-1) for exit in exits: self.arcs.add((exit, -start)) -- cgit v1.2.1 From 4361522532396635a593a3892dedd8955848d250 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 11:19:45 -0500 Subject: Coding declarations are a pain in the ass --HG-- branch : ast-branch --- coverage/parser.py | 5 +++-- tests/test_coverage.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index fc631fcc..262a78e3 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -17,7 +17,7 @@ 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 -from coverage.phystokens import compile_unicode, generate_tokens +from coverage.phystokens import compile_unicode, generate_tokens, neuter_encoding_declaration class PythonParser(object): @@ -324,8 +324,9 @@ class TryBlock(object): class AstArcAnalyzer(object): + @contract(text='unicode') def __init__(self, text): - self.root_node = ast.parse(text) + self.root_node = ast.parse(neuter_encoding_declaration(text)) if int(os.environ.get("COVERAGE_ASTDUMP", 0)): # Dump the AST so that failing tests have helpful output. ast_dump(self.root_node) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 3a27fab3..78a5dc86 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -549,6 +549,15 @@ class SimpleStatementTest(CoverageTest): """, ([1,3,6,7], [1,3,5,6,7], [1,3,4,5,6,7]), "") + def test_nonascii(self): + self.check_coverage("""\ + # coding: utf8 + a = 2 + b = 3 + """, + [2, 3] + ) + class CompoundStatementTest(CoverageTest): """Testing coverage of multi-line compound statements.""" -- cgit v1.2.1 From f98d5bfb6e939f046478b502e2041ac82f91632d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 14:30:28 -0500 Subject: Better exception support, include except-except arcs, and except-else --HG-- branch : ast-branch --- coverage/parser.py | 81 +++++++++++++++++++++++++++++++++++++------------- tests/test_arcs.py | 19 +++++++----- tests/test_coverage.py | 57 +++++++++++++++++++++++++++-------- 3 files changed, 117 insertions(+), 40 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 262a78e3..44cb1559 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -540,41 +540,56 @@ class AstArcAnalyzer(object): return set() def handle_Try(self, node): - return self.try_work(node, node.body, node.handlers, node.orelse, node.finalbody) - - def handle_TryExcept(self, node): - return self.try_work(node, node.body, node.handlers, node.orelse, None) - - def handle_TryFinally(self, node): - return self.try_work(node, node.body, None, None, node.finalbody) - - def try_work(self, node, body, handlers, orelse, finalbody): # 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 handlers: - handler_start = self.line_for_node(handlers[0]) + if node.handlers: + handler_start = self.line_for_node(node.handlers[0]) else: handler_start = None - if finalbody: - final_start = self.line_for_node(finalbody[0]) + + 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(body, from_line=start) + exits = self.add_body_arcs(node.body, from_line=start) + try_block = self.block_stack.pop() handler_exits = set() - if handlers: - for handler_node in handlers: + last_handler_start = None + if node.handlers: + for handler_node in node.handlers: handler_start = self.line_for_node(handler_node) - # TODO: handler_node.name and handler_node.type + if last_handler_start is not None: + self.arcs.add((last_handler_start, handler_start)) + last_handler_start = handler_start handler_exits |= self.add_body_arcs(handler_node.body, from_line=handler_start) - # TODO: node.orelse + if handler_node.type is None: + # "except:" doesn't jump to subsequent handlers, or + # "finally:". + last_handler_start = None + # TODO: should we break here? Handlers after "except:" + # won't be run. Should coverage know that code can't be + # run, or should it flag it as not run? + + if node.orelse: + exits = self.add_body_arcs(node.orelse, prev_lines=exits) + exits |= handler_exits - if finalbody: - final_from = exits | try_block.break_from | try_block.continue_from | try_block.raise_from | try_block.return_from - exits = self.add_body_arcs(finalbody, prev_lines=final_from) + if node.finalbody: + final_from = exits | try_block.break_from | try_block.continue_from | try_block.return_from + if node.handlers and last_handler_start is not None: + # If there was an "except X:" clause, then a "raise" in the + # body goes to the "except X:" before the "finally", but the + # "except" go to the finally. + final_from.add(last_handler_start) + else: + final_from |= try_block.raise_from + exits = self.add_body_arcs(node.finalbody, prev_lines=final_from) if try_block.break_from: self.process_break_exits(exits) if try_block.continue_from: @@ -585,6 +600,30 @@ class AstArcAnalyzer(object): self.process_return_exits(exits) return exits + def handle_TryExcept(self, node): + # Python 2.7 uses separate TryExcept and TryFinally nodes. If we get + # TryExcept, it means there was no finally, so fake it, and treat as + # a general Try node. + node.finalbody = [] + return self.handle_Try(node) + + def handle_TryFinally(self, node): + # Python 2.7 uses separate TryExcept and TryFinally nodes. If we get + # TryFinally, see if there's a TryExcept nested inside. If so, merge + # them. Otherwise, fake fields to complete a Try node. + node.handlers = [] + node.orelse = [] + + if node.body: + first = node.body[0] + if first.__class__.__name__ == "TryExcept" and node.lineno == first.lineno: + assert len(node.body) == 1 + node.body = first.body + node.handlers = first.handlers + node.orelse = first.orelse + + return self.handle_Try(node) + def handle_While(self, node): constant_test = self.is_constant_expr(node.test) start = to_top = self.line_for_node(node.test) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 6ba663bc..cd3aafff 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -627,7 +627,9 @@ class ExceptionArcTest(CoverageTest): c = 9 assert a == 3 and b == 1 and c == 9 """, - arcz=".1 12 23 45 39 59 67 79 9A A.", arcz_missing="45 59 67 79") + arcz=".1 12 23 45 46 39 59 67 79 69 9A A.", + arcz_missing="45 59 46 67 79 69", + ) self.check_coverage("""\ a, b, c = 1, 1, 1 try: @@ -640,8 +642,10 @@ class ExceptionArcTest(CoverageTest): c = 9 assert a == 1 and b == 5 and c == 9 """, - arcz=".1 12 23 45 39 59 67 79 9A A.", arcz_missing="39 67 79", - arcz_unpredicted="34") + arcz=".1 12 23 45 46 69 39 59 67 79 9A A.", + arcz_missing="39 46 67 79 69", + arcz_unpredicted="34", + ) self.check_coverage("""\ a, b, c = 1, 1, 1 try: @@ -654,8 +658,9 @@ class ExceptionArcTest(CoverageTest): c = 9 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", # TODO: 46 can be predicted. + arcz=".1 12 23 45 46 39 59 67 79 69 9A A.", + arcz_missing="39 45 59 69", + arcz_unpredicted="34", ) self.check_coverage("""\ a, b, c = 1, 1, 1 @@ -672,9 +677,9 @@ class ExceptionArcTest(CoverageTest): pass assert a == 1 and b == 1 and c == 10 """, - arcz=".1 12 23 34 4A 56 6A 78 8A AD BC CD D.", + arcz=".1 12 23 34 4A 56 6A 57 78 8A 7A AD BC CD D.", arcz_missing="4A 56 6A 78 8A AD", - arcz_unpredicted="45 57 7A AB", # TODO: 57 7A can be predicted. + arcz_unpredicted="45 AB", ) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 78a5dc86..9bb0f488 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -1021,7 +1021,10 @@ class CompoundStatementTest(CoverageTest): a = 123 assert a == 123 """, - [1,2,3,4,5,7,8], "4-5") + [1,2,3,4,5,7,8], "4-5", + arcz=".1 12 23 45 58 37 78 8.", + arcz_missing="45 58", + ) self.check_coverage("""\ a = 0 try: @@ -1033,7 +1036,10 @@ class CompoundStatementTest(CoverageTest): a = 123 assert a == 99 """, - [1,2,3,4,5,6,8,9], "8") + [1,2,3,4,5,6,8,9], "8", + arcz=".1 12 23 34 45 56 69 89 9.", + arcz_missing="89", + ) def test_try_finally(self): self.check_coverage("""\ @@ -1379,7 +1385,10 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 123 """, - [1,2,3,7,8], "", excludes=['#pragma: NO COVER']) + [1,2,3,7,8], "", excludes=['#pragma: NO COVER'], + arcz=".1 12 23 37 45 58 78 8.", + arcz_missing="45 58", + ) self.check_coverage("""\ a = 0 try: @@ -1391,7 +1400,10 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 99 """, - [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER']) + [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER'], + arcz=".1 12 23 34 45 56 69 89 9.", + arcz_missing="89", + ) def test_excluding_try_except_pass(self): self.check_coverage("""\ @@ -1425,7 +1437,10 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 123 """, - [1,2,3,7,8], "", excludes=['#pragma: NO COVER']) + [1,2,3,7,8], "", excludes=['#pragma: NO COVER'], + arcz=".1 12 23 37 45 58 78 8.", + arcz_missing="45 58", + ) self.check_coverage("""\ a = 0 try: @@ -1437,7 +1452,10 @@ class ExcludeTest(CoverageTest): x = 2 assert a == 99 """, - [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER']) + [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER'], + arcz=".1 12 23 34 45 56 69 89 9.", + arcz_missing="89", + ) def test_excluding_if_pass(self): # From a comment on the coverage.py page by Michael McNeil Forbes: @@ -1600,7 +1618,9 @@ class Py25Test(CoverageTest): b = 2 assert a == 1 and b == 2 """, - [1,2,3,4,5,7,8], "4-5") + [1,2,3,4,5,7,8], "4-5", + arcz=".1 12 23 37 45 57 78 8.", arcz_missing="45 57", + ) self.check_coverage("""\ a = 0; b = 0 try: @@ -1612,7 +1632,9 @@ class Py25Test(CoverageTest): b = 2 assert a == 99 and b == 2 """, - [1,2,3,4,5,6,8,9], "") + [1,2,3,4,5,6,8,9], "", + arcz=".1 12 23 34 45 56 68 89 9.", + ) self.check_coverage("""\ a = 0; b = 0 try: @@ -1626,7 +1648,9 @@ class Py25Test(CoverageTest): b = 2 assert a == 123 and b == 2 """, - [1,2,3,4,5,6,7,8,10,11], "6") + [1,2,3,4,5,6,7,8,10,11], "6", + arcz=".1 12 23 34 45 56 57 78 6A 8A AB B.", arcz_missing="56 6A", + ) self.check_coverage("""\ a = 0; b = 0 try: @@ -1642,7 +1666,10 @@ class Py25Test(CoverageTest): b = 2 assert a == 17 and b == 2 """, - [1,2,3,4,5,6,7,8,9,10,12,13], "6, 9-10") + [1,2,3,4,5,6,7,8,9,10,12,13], "6, 9-10", + arcz=".1 12 23 34 45 56 6C 57 78 8C 79 9A AC CD D.", + arcz_missing="56 6C 79 9A AC", + ) self.check_coverage("""\ a = 0; b = 0 try: @@ -1655,7 +1682,10 @@ class Py25Test(CoverageTest): b = 2 assert a == 123 and b == 2 """, - [1,2,3,4,5,7,9,10], "4-5") + [1,2,3,4,5,7,9,10], "4-5", + arcz=".1 12 23 37 45 59 79 9A A.", + arcz_missing="45 59", + ) self.check_coverage("""\ a = 0; b = 0 try: @@ -1669,7 +1699,10 @@ class Py25Test(CoverageTest): b = 2 assert a == 99 and b == 2 """, - [1,2,3,4,5,6,8,10,11], "8") + [1,2,3,4,5,6,8,10,11], "8", + arcz=".1 12 23 34 45 56 6A 8A AB B.", + arcz_missing="8A", + ) class ModuleTest(CoverageTest): -- cgit v1.2.1 From c116e3e2030e1d85897091f8f7ef7796471a1b5b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 15:38:24 -0500 Subject: Fix another test changed by better try-except measurement --HG-- branch : ast-branch --- tests/test_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 44a261d9..e6f28737 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -72,7 +72,7 @@ class PythonParserTest(CoverageTest): b = 9 """) self.assertEqual(parser.exit_counts(), { - 1: 1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 + 1: 1, 2:1, 3:2, 4:1, 5:2, 6:1, 7:1, 8:1, 9:1 }) def test_excluded_classes(self): -- cgit v1.2.1 From 9ee958be82e8e4f1cb958862cb29fa5b3d5f2523 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 16:03:40 -0500 Subject: Start the changelog entry for ast branch measurement. --HG-- branch : ast-branch --- CHANGES.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 76b71a5f..cc8b7a03 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,11 +9,22 @@ Change history for Coverage.py Unreleased ---------- +- Branch coverage has been rewritten: it used to be based on bytecode analysis, + but now uses AST analysis. This has changed a number of things: + + - More code paths are now considered runnable, especially in `try`/`except` + structures. This may mean that coverage.py will identify more code paths + as uncovered. + + - Python 3.5's `async` and `await` keywords are properly supported, fixing + `issue 434`_. + - Pragmas to disable coverage measurement can now be used on decorator lines, and they will apply to the entire function or class being decorated. This implements the feature requested in `issue 131`_. .. _issue 131: https://bitbucket.org/ned/coveragepy/issues/131/pragma-on-a-decorator-line-should-affect +.. _issue 434: https://bitbucket.org/ned/coveragepy/issues/434/indexerror-in-python-35 Version 4.0.3 --- 2015-11-24 -- cgit v1.2.1 From 3440e214df5ddd0f507ecd76c2350eb8d9dd6a75 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 16:14:35 -0500 Subject: Support returning through a finally --HG-- branch : ast-branch --- coverage/parser.py | 8 ++++---- tests/test_arcs.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 44cb1559..d85f0b57 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -426,8 +426,6 @@ class AstArcAnalyzer(object): # TODO: listcomps hidden in lists: x = [[i for i in range(10)]] # TODO: multi-line listcomps # TODO: nested function definitions - # TODO: multiple `except` clauses - # TODO: return->finally def process_break_exits(self, exits): for block in self.blocks(): @@ -467,8 +465,10 @@ class AstArcAnalyzer(object): def process_return_exits(self, exits): for block in self.blocks(): - # TODO: need a check here for TryBlock - if isinstance(block, FunctionBlock): + if isinstance(block, TryBlock) and block.final_start: + block.return_from.update(exits) + break + elif isinstance(block, FunctionBlock): # TODO: what if there is no enclosing function? for exit in exits: self.arcs.add((exit, -block.start)) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index cd3aafff..88442049 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -682,6 +682,21 @@ class ExceptionArcTest(CoverageTest): arcz_unpredicted="45 AB", ) + def test_return_finally(self): + self.check_coverage("""\ + a = [1] + def func(): + try: + return 10 + finally: + a.append(6) + + assert func() == 10 + assert a == [1, 6] + """, + arcz=".1 12 28 89 9. .3 34 46 6-2", + ) + class YieldTest(CoverageTest): """Arc tests for generators.""" -- cgit v1.2.1 From 5698ff871885333897392483d845fb7ce12f680f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 16:33:59 -0500 Subject: Remove unused imports --HG-- branch : ast-branch --- CHANGES.rst | 2 +- tests/test_arcs.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f100c805..5bc6eb2c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,7 @@ Unreleased but now uses AST analysis. This has changed a number of things: - More code paths are now considered runnable, especially in `try`/`except` - structures. This may mean that coverage.py will identify more code paths + structures. This may mean that coverage.py will identify more code paths as uncovered. - Python 3.5's `async` and `await` keywords are properly supported, fixing diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 37e8b9b7..91811e45 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -3,10 +3,6 @@ """Tests for coverage.py's arc measurement.""" -import collections -from itertools import cycle, product -import re - from tests.coveragetest import CoverageTest import coverage -- cgit v1.2.1 From e2ac7b25ec59a7146c9a9b29e9b5b07df101b0b5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 16:46:34 -0500 Subject: No reason to skip this test --HG-- branch : ast-branch --- tests/test_arcs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 91811e45..607ff68e 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -143,13 +143,12 @@ class SimpleArcTest(CoverageTest): ) def test_unused_lambdas_are_confusing_bug_90(self): - self.skip("Expected failure: bug 90") self.check_coverage("""\ a = 1 fn = lambda x: x b = 3 """, - arcz=".1 12 .2 2-2 23 3." + arcz=".1 12 .2 2-2 23 3.", arcz_missing=".2 2-2", ) -- cgit v1.2.1 From 25e74d4547a784a96420bb7e5b0653376f3568a6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2016 20:14:34 -0500 Subject: Four branch bugs fixed\! --HG-- branch : ast-branch --- CHANGES.rst | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5bc6eb2c..ffdd1a27 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,11 +14,27 @@ Unreleased - More code paths are now considered runnable, especially in `try`/`except` structures. This may mean that coverage.py will identify more code paths - as uncovered. + as uncovered. This could either raise or lower your overall coverage + number. - Python 3.5's `async` and `await` keywords are properly supported, fixing `issue 434`_. + - A some long-standing branch coverage bugs were fixed: + + - `issue 129`_: functions with only a docstring for a body would incorrectly + report a missing branch on the ``def`` line. + + - `issue 212`_: code in an ``except`` block could be incorrectly marked as + a missing branch. + + - `issue 146`_: context manages (``with`` statements) in a loop or ``try`` + block could confuse the branch measurement, reporting incorrect partial + branches. + + - `issue 422`_: in Python 3.5, an actual partial branch could be marked as + complete. + - Pragmas to disable coverage measurement can now be used on decorator lines, and they will apply to the entire function or class being decorated. This implements the feature requested in `issue 131`_. @@ -32,7 +48,11 @@ Unreleased - Non-ascii characters in regexes in the configuration file worked in 3.7, but stopped working in 4.0. Now they work again, closing `issue 455`_. +.. _issue 129: https://bitbucket.org/ned/coveragepy/issues/129/misleading-branch-coverage-of-empty .. _issue 131: https://bitbucket.org/ned/coveragepy/issues/131/pragma-on-a-decorator-line-should-affect +.. _issue 146: https://bitbucket.org/ned/coveragepy/issues/146/context-managers-confuse-branch-coverage +.. _issue 212: https://bitbucket.org/ned/coveragepy/issues/212/coverage-erroneously-reports-partial +.. _issue 422: https://bitbucket.org/ned/coveragepy/issues/422/python35-partial-branch-marked-as-fully .. _issue 434: https://bitbucket.org/ned/coveragepy/issues/434/indexerror-in-python-35 .. _issue 453: https://bitbucket.org/ned/coveragepy/issues/453/source-code-encoding-can-only-be-specified .. _issue 455: https://bitbucket.org/ned/coveragepy/issues/455/unusual-exclusions-stopped-working-in -- cgit v1.2.1 From a2cb815e890f092822fa211713ff3d33887afd86 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 3 Jan 2016 11:08:06 -0500 Subject: Fix arcs for function and class decorators --HG-- branch : ast-branch --- coverage/parser.py | 43 +++++++++++++++++++++++++++++++++---------- tests/test_arcs.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index c11bc222..d3fbad83 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -67,8 +67,9 @@ class PythonParser(object): # The raw line numbers of excluded lines of code, as marked by pragmas. self.raw_excluded = set() - # The line numbers of class definitions. + # The line numbers of class and function definitions. self.raw_classdefs = set() + self.raw_funcdefs = set() # The line numbers of docstring lines. self.raw_docstrings = set() @@ -146,6 +147,8 @@ class PythonParser(object): # we need to exclude them. The simplest way is to note the # lines with the 'class' keyword. self.raw_classdefs.add(slineno) + elif ttext == 'def': + self.raw_funcdefs.add(slineno) elif toktype == token.OP: if ttext == ':': should_exclude = (elineno in self.raw_excluded) or excluding_decorators @@ -268,7 +271,7 @@ class PythonParser(object): def arcs(self): if self._all_arcs is None: - aaa = AstArcAnalyzer(self.text) + aaa = AstArcAnalyzer(self.text, self.raw_funcdefs, self.raw_classdefs) arcs = aaa.collect_arcs() self._all_arcs = set() @@ -327,9 +330,12 @@ class TryBlock(object): class AstArcAnalyzer(object): - @contract(text='unicode') - def __init__(self, text): + @contract(text='unicode', funcdefs=set, classdefs=set) + def __init__(self, text, funcdefs, classdefs): self.root_node = ast.parse(neuter_encoding_declaration(text)) + self.funcdefs = funcdefs + self.classdefs = classdefs + if int(os.environ.get("COVERAGE_ASTDUMP", 0)): # Dump the AST so that failing tests have helpful output. ast_dump(self.root_node) @@ -485,9 +491,25 @@ class AstArcAnalyzer(object): return set() def handle_ClassDef(self, node): - start = self.line_for_node(node) + return self.do_decorated(node, self.classdefs) + + def do_decorated(self, node, defs): + first = last = self.line_for_node(node) + if node.decorator_list: + for dec_node in node.decorator_list: + dec_start = self.line_for_node(dec_node) + if dec_start != last: + self.arcs.add((last, dec_start)) + last = dec_start + # The definition line may have been missed, but we should have it in + # `defs`. + body_start = self.line_for_node(node.body[0]) + for lineno in range(last+1, body_start): + if lineno in defs: + self.arcs.add((last, lineno)) + last = lineno # the body is handled in add_arcs_for_code_objects. - return set([start]) + return set([last]) def handle_Continue(self, node): here = self.line_for_node(node) @@ -515,9 +537,7 @@ class AstArcAnalyzer(object): handle_AsyncFor = handle_For def handle_FunctionDef(self, node): - start = self.line_for_node(node) - # the body is handled in add_arcs_for_code_objects. - return set([start]) + return self.do_decorated(node, self.funcdefs) handle_AsyncFunctionDef = handle_FunctionDef @@ -1159,7 +1179,10 @@ def ast_dump(node, depth=0): print("{0} {1}: {2!r}>".format(head, field_name, value)) else: print(head) - print("{0}# mro: {1}".format(indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]))) + if 0: + print("{0}# mro: {1}".format( + indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]), + )) next_indent = indent + " " for field_name, value in named_fields: prefix = "{0}{1}:".format(next_indent, field_name) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 607ff68e..8b524db7 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -871,6 +871,53 @@ class MiscArcTest(CoverageTest): arcs_missing=[], arcs_unpredicted=[], ) + def test_function_decorator(self): + self.check_coverage("""\ + def decorator(arg): + def _dec(f): + return f + return _dec + + @decorator(6) + @decorator( + len([8]), + ) + def my_function( + a=len([11]), + ): + x = 13 + a = 14 + my_function() + """, + arcz= + ".1 16 67 7A AE EF F. " # main line + ".2 24 4. .3 3-2 " # decorators + ".D D-6 ", # my_function + ) + + def test_class_decorator(self): + self.check_coverage("""\ + def decorator(arg): + def _dec(c): + return c + return _dec + + @decorator(6) + @decorator( + len([8]), + ) + class MyObject( + object + ): + X = 13 + a = 14 + """, + arcz= + ".1 16 67 6D 7A AE E. " # main line + ".2 24 4. .3 3-2 " # decorators + ".6 D-6 ", # MyObject + ) + class AsyncTest(CoverageTest): def setUp(self): -- cgit v1.2.1 From 1aa9abd82ecde6d5181a17082f666baca00198ef Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 3 Jan 2016 12:31:58 -0500 Subject: Clean up some lint --HG-- branch : ast-branch --- coverage/parser.py | 56 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index d3fbad83..39e23d23 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -399,7 +399,7 @@ class AstArcAnalyzer(object): # TODO: this is probably over-doing it, and too expensive. Can we # instrument the ast walking to see how many nodes we are revisiting? if isinstance(node, ast.stmt): - for name, value in ast.iter_fields(node): + for _, value in ast.iter_fields(node): if isinstance(value, ast.expr) and self.contains_return_expression(value): self.process_return_exits([self.line_for_node(node)]) break @@ -450,8 +450,8 @@ class AstArcAnalyzer(object): 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)) + for xit in exits: + self.arcs.add((xit, block.start)) break elif isinstance(block, TryBlock) and block.final_start: block.continue_from.update(exits) @@ -461,15 +461,15 @@ class AstArcAnalyzer(object): for block in self.blocks(): if isinstance(block, TryBlock): if block.handler_start: - for exit in exits: - self.arcs.add((exit, block.handler_start)) + for xit in exits: + self.arcs.add((xit, 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)) + for xit in exits: + self.arcs.add((xit, -block.start)) break def process_return_exits(self, exits): @@ -479,8 +479,8 @@ class AstArcAnalyzer(object): break elif isinstance(block, FunctionBlock): # TODO: what if there is no enclosing function? - for exit in exits: - self.arcs.add((exit, -block.start)) + for xit in exits: + self.arcs.add((xit, -block.start)) break ## Handlers @@ -491,10 +491,10 @@ class AstArcAnalyzer(object): return set() def handle_ClassDef(self, node): - return self.do_decorated(node, self.classdefs) + return self.process_decorated(node, self.classdefs) - def do_decorated(self, node, defs): - first = last = self.line_for_node(node) + def process_decorated(self, node, defs): + last = self.line_for_node(node) if node.decorator_list: for dec_node in node.decorator_list: dec_start = self.line_for_node(dec_node) @@ -520,8 +520,8 @@ class AstArcAnalyzer(object): start = self.line_for_node(node.iter) 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)) + for xit in exits: + self.arcs.add((xit, start)) my_block = self.block_stack.pop() exits = my_block.break_exits if node.orelse: @@ -537,7 +537,7 @@ class AstArcAnalyzer(object): handle_AsyncFor = handle_For def handle_FunctionDef(self, node): - return self.do_decorated(node, self.funcdefs) + return self.process_decorated(node, self.funcdefs) handle_AsyncFunctionDef = handle_FunctionDef @@ -547,9 +547,6 @@ class AstArcAnalyzer(object): exits |= self.add_body_arcs(node.orelse, from_line=start) return exits - def handle_Module(self, node): - raise Exception("TODO: this shouldn't happen") - def handle_Raise(self, node): # `raise` statement jumps away, no exits from here. here = self.line_for_node(node) @@ -604,7 +601,12 @@ class AstArcAnalyzer(object): exits |= handler_exits if node.finalbody: - final_from = exits | try_block.break_from | try_block.continue_from | try_block.return_from + final_from = ( # You can get to the `finally` clause from: + exits | # the exits of the body or `else` clause, + try_block.break_from | # or a `break` in the body, + try_block.continue_from | # or a `continue` in the body, + try_block.return_from # or a `return` in the body. + ) if node.handlers and last_handler_start is not None: # If there was an "except X:" clause, then a "raise" in the # body goes to the "except X:" before the "finally", but the @@ -654,8 +656,8 @@ class AstArcAnalyzer(object): to_top = self.line_for_node(node.body[0]) 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)) + for xit in exits: + self.arcs.add((xit, to_top)) exits = set() if not constant_test: exits.add(start) @@ -693,21 +695,21 @@ class AstArcAnalyzer(object): if node_name == "Module": start = self.line_for_node(node) exits = self.add_body_arcs(node.body, from_line=-1) - for exit in exits: - self.arcs.add((exit, -start)) + for xit in exits: + self.arcs.add((xit, -start)) elif node_name in ["FunctionDef", "AsyncFunctionDef"]: start = self.line_for_node(node) self.block_stack.append(FunctionBlock(start=start)) exits = self.add_body_arcs(node.body, from_line=-1) self.block_stack.pop() - for exit in exits: - self.arcs.add((exit, -start)) + for xit in exits: + self.arcs.add((xit, -start)) elif node_name == "ClassDef": start = self.line_for_node(node) self.arcs.add((-1, start)) exits = self.add_body_arcs(node.body, from_line=start) - for exit in exits: - self.arcs.add((exit, -start)) + for xit in exits: + self.arcs.add((xit, -start)) elif node_name in self.CODE_COMPREHENSIONS: # TODO: tests for when generators is more than one? for gen in node.generators: -- cgit v1.2.1 From 78177f0bcdba89b74292405220463e9cb65d4f7a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 4 Jan 2016 07:18:08 -0500 Subject: Add a delayed_assertions context manager --HG-- branch : ast-branch --- coverage/test_helpers.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_testing.py | 43 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py index 092daa07..84e2f1cf 100644 --- a/coverage/test_helpers.py +++ b/coverage/test_helpers.py @@ -186,6 +186,55 @@ class StdStreamCapturingMixin(TestCase): return self.captured_stderr.getvalue() +class DelayedAssertionMixin(TestCase): + """A test case mixin that provides a `delayed_assertions` context manager. + + Use it like this:: + + with self.delayed_assertions(): + self.assertEqual(x, y) + self.assertEqual(z, w) + + All of the assertions will run. The failures will be displayed at the end + of the with-statement. + + NOTE: only works with some assert methods, I'm not sure which! + + """ + def __init__(self, *args, **kwargs): + super(DelayedAssertionMixin, self).__init__(*args, **kwargs) + # This mixin only works with assert methods that call `self.fail`. In + # Python 2.7, `assertEqual` didn't, but we can do what Python 3 does, + # and use `assertMultiLineEqual` for comparing strings. + self.addTypeEqualityFunc(str, 'assertMultiLineEqual') + self._delayed_assertions = None + + @contextlib.contextmanager + def delayed_assertions(self): + """The context manager: assert that we didn't collect any assertions.""" + self._delayed_assertions = [] + old_fail = self.fail + self.fail = self._delayed_fail + try: + yield + finally: + self.fail = old_fail + if self._delayed_assertions: + if len(self._delayed_assertions) == 1: + self.fail(self._delayed_assertions[0]) + else: + self.fail( + "{} failed assertions:\n{}".format( + len(self._delayed_assertions), + "\n".join(self._delayed_assertions), + ) + ) + + def _delayed_fail(self, msg=None): + """The stand-in for TestCase.fail during delayed_assertions.""" + self._delayed_assertions.append(msg) + + class TempDirMixin(SysPathAwareMixin, ModuleAwareMixin, TestCase): """A test case mixin that creates a temp directory and files in it. diff --git a/tests/test_testing.py b/tests/test_testing.py index 9fc7f11d..1dafdd0d 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -6,13 +6,15 @@ import datetime import os +import re import sys +import textwrap import coverage from coverage.backunittest import TestCase from coverage.backward import to_bytes from coverage.files import actual_path -from coverage.test_helpers import EnvironmentAwareMixin, TempDirMixin +from coverage.test_helpers import EnvironmentAwareMixin, TempDirMixin, DelayedAssertionMixin from tests.coveragetest import CoverageTest @@ -97,6 +99,45 @@ class EnvironmentAwareMixinTest(EnvironmentAwareMixin, TestCase): self.assertNotIn("XYZZY_PLUGH", os.environ) +class DelayedAssertionMixinTest(DelayedAssertionMixin, TestCase): + """Test the `delayed_assertions` method.""" + + def test_delayed_assertions(self): + # Two assertions can be shown at once: + msg = re.escape(textwrap.dedent("""\ + 2 failed assertions: + 'x' != 'y' + - x + + y + + 'w' != 'z' + - w + + z + """)) + with self.assertRaisesRegex(AssertionError, msg): + with self.delayed_assertions(): + self.assertEqual("x", "y") + self.assertEqual("w", "z") + + # It's also OK if only one fails: + msg = re.escape(textwrap.dedent("""\ + 'w' != 'z' + - w + + z + """)) + with self.assertRaisesRegex(AssertionError, msg): + with self.delayed_assertions(): + self.assertEqual("x", "x") + self.assertEqual("w", "z") + + # If an error happens, it gets reported immediately, no special + # handling: + with self.assertRaises(ZeroDivisionError): + with self.delayed_assertions(): + self.assertEqual("x", "y") + self.assertEqual("w", 1/0) + + class CoverageTestTest(CoverageTest): """Test the methods in `CoverageTest`.""" -- cgit v1.2.1 From 3cf8dd01dcef141902604567b537224d6918ba67 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 4 Jan 2016 07:21:03 -0500 Subject: Fix 2.6, as usual --HG-- branch : ast-branch --- coverage/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py index 84e2f1cf..1d606aae 100644 --- a/coverage/test_helpers.py +++ b/coverage/test_helpers.py @@ -224,7 +224,7 @@ class DelayedAssertionMixin(TestCase): self.fail(self._delayed_assertions[0]) else: self.fail( - "{} failed assertions:\n{}".format( + "{0} failed assertions:\n{1}".format( len(self._delayed_assertions), "\n".join(self._delayed_assertions), ) -- cgit v1.2.1 From 6e50c427ea3be052cb976cfadda31f25bd06181c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 4 Jan 2016 09:31:55 -0500 Subject: Clarify when delayed_assertions is known to work. --HG-- branch : ast-branch --- coverage/test_helpers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py index 1d606aae..a76bed35 100644 --- a/coverage/test_helpers.py +++ b/coverage/test_helpers.py @@ -198,7 +198,11 @@ class DelayedAssertionMixin(TestCase): All of the assertions will run. The failures will be displayed at the end of the with-statement. - NOTE: only works with some assert methods, I'm not sure which! + NOTE: this only works with some assertions. These are known to work: + + - `assertEqual(str, str)` + + - `assertMultilineEqual(str, str)` """ def __init__(self, *args, **kwargs): -- cgit v1.2.1 From 7d4c3be902f65a53634efc67e2224b5641dae5a8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 4 Jan 2016 19:32:39 -0500 Subject: Use delayed_assertions() when checking arcs --HG-- branch : ast-branch --- tests/coveragetest.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 9d2ae1a2..d79aee7f 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -20,6 +20,7 @@ from coverage.cmdline import CoverageScript from coverage.debug import _TEST_NAME_FILE, DebugControl from coverage.test_helpers import ( EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, + DelayedAssertionMixin, ) from nose.plugins.skip import SkipTest @@ -35,6 +36,7 @@ class CoverageTest( EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, + DelayedAssertionMixin, TestCase ): """A base class for coverage.py test cases.""" @@ -238,17 +240,21 @@ class CoverageTest( self.fail("None of the missing choices matched %r" % missing_formatted) if arcs is not None: - self.assert_equal_args(analysis.arc_possibilities(), arcs, "Possible arcs differ") - - self.assert_equal_args( - analysis.arcs_missing(), arcs_missing, - "Missing arcs differ" - ) - - self.assert_equal_args( - analysis.arcs_unpredicted(), arcs_unpredicted, - "Unpredicted arcs differ" - ) + with self.delayed_assertions(): + self.assert_equal_args( + analysis.arc_possibilities(), arcs, + "Possible arcs differ", + ) + + self.assert_equal_args( + analysis.arcs_missing(), arcs_missing, + "Missing arcs differ" + ) + + self.assert_equal_args( + analysis.arcs_unpredicted(), arcs_unpredicted, + "Unpredicted arcs differ" + ) if report: frep = StringIO() -- cgit v1.2.1 From eda903304b8ea1fd72d2a33fe794df45c7d92127 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 4 Jan 2016 19:40:12 -0500 Subject: lab/parser.py shows arcs more usefully One-plus lines (that just go to the next line) now show + Raw statements (-) and official statements (=) don't collide. --HG-- branch : ast-branch --- lab/parser.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/lab/parser.py b/lab/parser.py index 9a064257..717fbbf9 100644 --- a/lab/parser.py +++ b/lab/parser.py @@ -72,7 +72,6 @@ class ParserMain(object): def one_file(self, options, filename): """Process just one file.""" - try: text = get_python_source(filename) bp = ByteParser(text, filename=filename) @@ -109,27 +108,30 @@ class ParserMain(object): exit_counts = cp.exit_counts() for lineno, ltext in enumerate(cp.lines, start=1): - m0 = m1 = m2 = m3 = a = ' ' + marks = [' ', ' ', ' ', ' ', ' '] + a = ' ' + if lineno in cp.raw_statements: + marks[0] = '-' if lineno in cp.statements: - m0 = '=' - elif lineno in cp.raw_statements: - m0 = '-' + marks[1] = '=' exits = exit_counts.get(lineno, 0) if exits > 1: - m1 = str(exits) + marks[2] = str(exits) if lineno in cp.raw_docstrings: - m2 = '"' + marks[3] = '"' if lineno in cp.raw_classdefs: - m2 = 'C' + marks[3] = 'C' + if lineno in cp.raw_funcdefs: + marks[3] = 'f' if lineno in cp.raw_excluded: - m3 = 'x' + marks[4] = 'x' if arc_chars: a = arc_chars[lineno].ljust(arc_width) else: a = "" - print("%4d %s%s%s%s%s %s" % (lineno, m0, m1, m2, m3, a, ltext)) + print("%4d %s%s %s" % (lineno, "".join(marks), a, ltext)) def disassemble(self, byte_parser, chunks=False, histogram=False): """Disassemble code, for ad-hoc experimenting.""" @@ -173,6 +175,7 @@ class ParserMain(object): """ + plus_ones = set() arc_chars = collections.defaultdict(str) for lfrom, lto in sorted(arcs): if lfrom < 0: @@ -181,13 +184,12 @@ class ParserMain(object): arc_chars[lfrom] += '^' else: if lfrom == lto - 1: - # Don't show obvious arcs. + plus_ones.add(lfrom) continue if lfrom < lto: l1, l2 = lfrom, lto else: l1, l2 = lto, lfrom - #w = max(len(arc_chars[l]) for l in range(l1, l2+1)) w = first_all_blanks(arc_chars[l] for l in range(l1, l2+1)) for l in range(l1, l2+1): if l == lfrom: @@ -198,6 +200,13 @@ class ParserMain(object): ch = '|' arc_chars[l] = set_char(arc_chars[l], w, ch) + # Add the plusses as the first character + for lineno, arcs in arc_chars.items(): + arc_chars[lineno] = ( + ("+" if lineno in plus_ones else " ") + + arcs + ) + return arc_chars -- cgit v1.2.1 From 4074315ac65ed79e94bc331a8059859781b5b12b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 4 Jan 2016 20:12:55 -0500 Subject: Support comprehensions better --HG-- branch : ast-branch --- coverage/parser.py | 15 +++------------ tests/test_arcs.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 39e23d23..c680f63b 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -383,10 +383,6 @@ class AstArcAnalyzer(object): # TODO: test case for empty module. return 1 - def line_comprehension(self, node): - # TODO: is this how to get the line number for a comprehension? - return node.target.lineno - def line_default(self, node): return node.lineno @@ -433,7 +429,6 @@ class AstArcAnalyzer(object): # TODO: multi-target assignment with computed targets # TODO: listcomps hidden deep in other expressions # TODO: listcomps hidden in lists: x = [[i for i in range(10)]] - # TODO: multi-line listcomps # TODO: nested function definitions def process_break_exits(self, exits): @@ -554,7 +549,6 @@ class AstArcAnalyzer(object): return set() def handle_Return(self, node): - # TODO: deal with returning through a finally. here = self.line_for_node(node) self.process_return_exits([here]) return set() @@ -711,12 +705,9 @@ class AstArcAnalyzer(object): for xit in exits: self.arcs.add((xit, -start)) elif node_name in self.CODE_COMPREHENSIONS: - # TODO: tests for when generators is more than one? - for gen in node.generators: - start = self.line_for_node(gen) - self.arcs.add((-1, start)) - self.arcs.add((start, -start)) - # TODO: guaranteed this won't work for multi-line comps. + start = self.line_for_node(node) + self.arcs.add((-1, start)) + self.arcs.add((start, -start)) elif node_name == "Lambda": start = self.line_for_node(node) self.arcs.add((-1, start)) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 8b524db7..fb4b99eb 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -378,6 +378,54 @@ class LoopArcTest(CoverageTest): arcz=".1 .2 2-2 12 23 34 45 53 3.", ) + def test_multiline_dict_comp(self): + if env.PYVERSION < (2, 7): + self.skip("Don't have set or dict comprehensions before 2.7") + if env.PY2: + arcz = ".2 2B B-4 2-4" + else: + arcz = ".2 2B B-3 2-3" + # Multiline dict comp: + self.check_coverage("""\ + # comment + d = \\ + { + i: + str(i) + for + i + in + range(9) + } + x = 11 + """, + arcz=arcz, + ) + # Multi dict comp: + if env.PY2: + arcz = ".2 2F F-4 2-4" + else: + arcz = ".2 2F F-3 2-3" + self.check_coverage("""\ + # comment + d = \\ + { + (i, j): + str(i+j) + for + i + in + range(9) + for + j + in + range(13) + } + x = 15 + """, + arcz=arcz, + ) + class ExceptionArcTest(CoverageTest): """Arc-measuring tests involving exception handling.""" -- cgit v1.2.1 From f7f56ec9adaa531019a27ef7c634db816f30040a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jan 2016 06:54:07 -0500 Subject: Support while-else --HG-- branch : ast-branch --- coverage/parser.py | 12 +++++++----- tests/test_arcs.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index c680f63b..b0e7371f 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -520,8 +520,6 @@ class AstArcAnalyzer(object): 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)) else_exits = self.add_body_arcs(node.orelse, from_line=start) exits |= else_exits else: @@ -653,11 +651,15 @@ class AstArcAnalyzer(object): for xit in exits: self.arcs.add((xit, to_top)) exits = set() - if not constant_test: - exits.add(start) my_block = self.block_stack.pop() exits.update(my_block.break_exits) - # TODO: orelse + if node.orelse: + else_exits = self.add_body_arcs(node.orelse, from_line=start) + exits |= else_exits + else: + # No `else` clause: you can exit from the start. + if not constant_test: + exits.add(start) return exits def handle_With(self, node): diff --git a/tests/test_arcs.py b/tests/test_arcs.py index fb4b99eb..cab95c8f 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -314,6 +314,22 @@ class LoopArcTest(CoverageTest): arcz=".1 .2 23 32 34 47 26 67 7. 18 89 9." ) + def test_while_else(self): + self.check_coverage("""\ + def whileelse(seq): + while seq: + n = seq.pop() + if n > 4: + break + else: + n = 99 + return n + assert whileelse([1, 2]) == 99 + assert whileelse([1, 5]) == 5 + """, + arcz=".1 19 9A A. .2 23 34 45 58 42 27 78 8.", + ) + def test_confusing_for_loop_bug_175(self): if env.PY3: # Py3 counts the list comp as a separate code object. -- cgit v1.2.1 From e0cc720dad16bed5673a1e7d11ccdceeab200fc3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jan 2016 07:17:27 -0500 Subject: Tweak the conditional for the start-point of dictcomps --HG-- branch : ast-branch --- tests/test_arcs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index cab95c8f..9af4a083 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -397,7 +397,7 @@ class LoopArcTest(CoverageTest): def test_multiline_dict_comp(self): if env.PYVERSION < (2, 7): self.skip("Don't have set or dict comprehensions before 2.7") - if env.PY2: + if env.PYVERSION < (3, 5): arcz = ".2 2B B-4 2-4" else: arcz = ".2 2B B-3 2-3" @@ -418,7 +418,7 @@ class LoopArcTest(CoverageTest): arcz=arcz, ) # Multi dict comp: - if env.PY2: + if env.PYVERSION < (3, 5): arcz = ".2 2F F-4 2-4" else: arcz = ".2 2F F-3 2-3" -- cgit v1.2.1 From 8f9b4f9d596ef4a5c0d26b4e54acfcd0558ece39 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2016 07:11:32 -0500 Subject: Add some tests for uncovered cases --HG-- branch : ast-branch --- coverage/parser.py | 17 ++++++++++------- tests/test_arcs.py | 7 +++++++ tests/test_coverage.py | 17 ++++++++++++++++- tests/test_summary.py | 6 +++--- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index b0e7371f..a6a8ad65 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -336,7 +336,7 @@ class AstArcAnalyzer(object): self.funcdefs = funcdefs self.classdefs = classdefs - if int(os.environ.get("COVERAGE_ASTDUMP", 0)): + if int(os.environ.get("COVERAGE_ASTDUMP", 0)): # pragma: debugging # Dump the AST so that failing tests have helpful output. ast_dump(self.root_node) @@ -372,7 +372,6 @@ class AstArcAnalyzer(object): if node.elts: return self.line_for_node(node.elts[0]) else: - # TODO: test case for this branch: x = [] return node.lineno def line_Module(self, node): @@ -380,7 +379,6 @@ class AstArcAnalyzer(object): return self.line_for_node(node.body[0]) else: # Modules have no line number, they always start at 1. - # TODO: test case for empty module. return 1 def line_default(self, node): @@ -426,7 +424,6 @@ class AstArcAnalyzer(object): # tests to write: # TODO: while EXPR: # TODO: while False: - # TODO: multi-target assignment with computed targets # TODO: listcomps hidden deep in other expressions # TODO: listcomps hidden in lists: x = [[i for i in range(10)]] # TODO: nested function definitions @@ -688,11 +685,17 @@ class AstArcAnalyzer(object): def add_arcs_for_code_objects(self, root_node): for node in ast.walk(root_node): node_name = node.__class__.__name__ + # TODO: should this be broken into separate methods? if node_name == "Module": start = self.line_for_node(node) - exits = self.add_body_arcs(node.body, from_line=-1) - for xit in exits: - self.arcs.add((xit, -start)) + if node.body: + exits = self.add_body_arcs(node.body, from_line=-1) + for xit in exits: + self.arcs.add((xit, -start)) + else: + # Empty module. + self.arcs.add((-1, start)) + self.arcs.add((start, -1)) elif node_name in ["FunctionDef", "AsyncFunctionDef"]: start = self.line_for_node(node) self.block_stack.append(FunctionBlock(start=start)) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 9af4a083..60fdea37 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -151,6 +151,13 @@ class SimpleArcTest(CoverageTest): arcz=".1 12 .2 2-2 23 3.", arcz_missing=".2 2-2", ) + def test_what_is_the_sound_of_no_lines_clapping(self): + self.check_coverage("""\ + # __init__.py + """, + arcz=".1 1.", + ) + class WithTest(CoverageTest): """Arc-measuring tests involving context managers.""" diff --git a/tests/test_coverage.py b/tests/test_coverage.py index ea7604b1..c0991b04 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -109,7 +109,7 @@ class BasicCoverageTest(CoverageTest): import sys if not sys.path: a = 1 - """, + """, # indented last line [1,2,3], "3") def test_multiline_initializer(self): @@ -198,6 +198,21 @@ class SimpleStatementTest(CoverageTest): """, [1,2,3], "") + def test_more_assignments(self): + self.check_coverage("""\ + x = [] + d = {} + d[ + 4 + len(x) + + 5 + ] = \\ + d[ + 8 ** 2 + ] = \\ + 9 + """, + [1, 2, 3], "") + def test_attribute_assignment(self): # Attribute assignment self.check_coverage("""\ diff --git a/tests/test_summary.py b/tests/test_summary.py index 56c0b831..9d7a6fe7 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -607,7 +607,7 @@ class SummaryTest2(CoverageTest): def test_empty_files(self): # Shows that empty files like __init__.py are listed as having zero # statements, not one statement. - cov = coverage.Coverage() + cov = coverage.Coverage(branch=True) cov.start() import usepkgs # pragma: nested # pylint: disable=import-error,unused-variable cov.stop() # pragma: nested @@ -617,8 +617,8 @@ class SummaryTest2(CoverageTest): report = repout.getvalue().replace('\\', '/') report = re.sub(r"\s+", " ", report) - self.assertIn("tests/modules/pkg1/__init__.py 2 0 100%", report) - self.assertIn("tests/modules/pkg2/__init__.py 0 0 100%", report) + self.assertIn("tests/modules/pkg1/__init__.py 2 0 0 0 100%", report) + self.assertIn("tests/modules/pkg2/__init__.py 0 0 0 0 100%", report) class ReportingReturnValueTest(CoverageTest): -- cgit v1.2.1 From cb33e6c3a41d48a37bebee3e8b3421ab35ab0ba5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2016 07:35:11 -0500 Subject: Test continue/finally --HG-- branch : ast-branch --- tests/test_arcs.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 60fdea37..bb811c01 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -617,6 +617,26 @@ class ExceptionArcTest(CoverageTest): arcz_missing="3D BC CD", ) + def test_continue_through_finally(self): + self.check_coverage("""\ + a, b, c, d, i = 1, 1, 1, 1, 99 + try: + for i in range(5): + try: + a = 5 + if i > 0: + continue + b = 8 + finally: + c = 10 + except: + 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_missing="BC CD", + ) + def test_finally_in_loop_bug_92(self): self.check_coverage("""\ for i in range(5): -- cgit v1.2.1 From 909e0afcc474d7ede12e2f967bbc34097e132915 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2016 07:56:05 -0500 Subject: Clean up some TODO's and code paths --HG-- branch : ast-branch --- coverage/parser.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index a6a8ad65..348eb7c5 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -431,7 +431,6 @@ class AstArcAnalyzer(object): 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: @@ -441,7 +440,6 @@ class AstArcAnalyzer(object): def process_continue_exits(self, exits): for block in self.blocks(): if isinstance(block, LoopBlock): - # TODO: what if there is no loop? for xit in exits: self.arcs.add((xit, block.start)) break @@ -470,7 +468,6 @@ class AstArcAnalyzer(object): block.return_from.update(exits) break elif isinstance(block, FunctionBlock): - # TODO: what if there is no enclosing function? for xit in exits: self.arcs.add((xit, -block.start)) break @@ -628,13 +625,12 @@ class AstArcAnalyzer(object): node.handlers = [] node.orelse = [] - if node.body: - first = node.body[0] - if first.__class__.__name__ == "TryExcept" and node.lineno == first.lineno: - assert len(node.body) == 1 - node.body = first.body - node.handlers = first.handlers - node.orelse = first.orelse + first = node.body[0] + if first.__class__.__name__ == "TryExcept" and node.lineno == first.lineno: + assert len(node.body) == 1 + node.body = first.body + node.handlers = first.handlers + node.orelse = first.orelse return self.handle_Try(node) @@ -672,10 +668,10 @@ class AstArcAnalyzer(object): ]) def handle_default(self, node): - node_name = node.__class__.__name__ - if node_name not in self.OK_TO_DEFAULT: - # TODO: put 1/0 here to find unhandled nodes. - print("*** Unhandled: {0}".format(node)) + if 0: + node_name = node.__class__.__name__ + if node_name not in self.OK_TO_DEFAULT: + print("*** Unhandled: {0}".format(node)) return set([self.line_for_node(node)]) CODE_COMPREHENSIONS = set(["GeneratorExp", "DictComp", "SetComp"]) -- cgit v1.2.1 From 426961555ee866da2addb74b25c6dfca3d2c5f33 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2016 08:11:58 -0500 Subject: More uniform dispatch: use methods for everything, and handle defaults in the dispatch instead of calling another method. --HG-- branch : ast-branch --- coverage/parser.py | 120 +++++++++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 54 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 348eb7c5..c5d7c618 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -355,8 +355,11 @@ class AstArcAnalyzer(object): def line_for_node(self, node): """What is the right line number to use for this node?""" node_name = node.__class__.__name__ - handler = getattr(self, "line_" + node_name, self.line_default) - return handler(node) + handler = getattr(self, "line_" + node_name, None) + if handler is not None: + return handler(node) + else: + return node.lineno def line_Assign(self, node): return self.line_for_node(node.value) @@ -381,8 +384,10 @@ class AstArcAnalyzer(object): # Modules have no line number, they always start at 1. return 1 - def line_default(self, node): - return node.lineno + OK_TO_DEFAULT = set([ + "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global", + "Import", "ImportFrom", "Pass", "Print", + ]) def add_arcs(self, node): """Add the arcs for `node`. @@ -397,9 +402,17 @@ class AstArcAnalyzer(object): if isinstance(value, ast.expr) and self.contains_return_expression(value): self.process_return_exits([self.line_for_node(node)]) break + node_name = node.__class__.__name__ - handler = getattr(self, "handle_" + node_name, self.handle_default) - return handler(node) + handler = getattr(self, "handle_" + node_name, None) + if handler is not None: + return handler(node) + + if 0: + node_name = node.__class__.__name__ + if node_name not in self.OK_TO_DEFAULT: + print("*** Unhandled: {0}".format(node)) + return set([self.line_for_node(node)]) def add_body_arcs(self, body, from_line=None, prev_lines=None): if prev_lines is None: @@ -662,58 +675,57 @@ class AstArcAnalyzer(object): handle_AsyncWith = handle_With - OK_TO_DEFAULT = set([ - "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global", - "Import", "ImportFrom", "Pass", "Print", - ]) - - def handle_default(self, node): - if 0: + def add_arcs_for_code_objects(self, root_node): + for node in ast.walk(root_node): node_name = node.__class__.__name__ - if node_name not in self.OK_TO_DEFAULT: - print("*** Unhandled: {0}".format(node)) - return set([self.line_for_node(node)]) + code_object_handler = getattr(self, "code_object_" + node_name, None) + if code_object_handler is not None: + code_object_handler(node) + + def code_object_Module(self, node): + start = self.line_for_node(node) + if node.body: + exits = self.add_body_arcs(node.body, from_line=-1) + for xit in exits: + self.arcs.add((xit, -start)) + else: + # Empty module. + self.arcs.add((-1, start)) + self.arcs.add((start, -1)) + + def code_object_FunctionDef(self, node): + start = self.line_for_node(node) + self.block_stack.append(FunctionBlock(start=start)) + exits = self.add_body_arcs(node.body, from_line=-1) + self.block_stack.pop() + for xit in exits: + self.arcs.add((xit, -start)) + + code_object_AsyncFunctionDef = code_object_FunctionDef - CODE_COMPREHENSIONS = set(["GeneratorExp", "DictComp", "SetComp"]) + def code_object_ClassDef(self, node): + start = self.line_for_node(node) + self.arcs.add((-1, start)) + exits = self.add_body_arcs(node.body, from_line=start) + for xit in exits: + self.arcs.add((xit, -start)) + + def do_code_object_comprehension(self, node): + start = self.line_for_node(node) + self.arcs.add((-1, start)) + self.arcs.add((start, -start)) + + code_object_GeneratorExp = do_code_object_comprehension + code_object_DictComp = do_code_object_comprehension + code_object_SetComp = do_code_object_comprehension if env.PY3: - CODE_COMPREHENSIONS.add("ListComp") + code_object_ListComp = do_code_object_comprehension - def add_arcs_for_code_objects(self, root_node): - for node in ast.walk(root_node): - node_name = node.__class__.__name__ - # TODO: should this be broken into separate methods? - if node_name == "Module": - start = self.line_for_node(node) - if node.body: - exits = self.add_body_arcs(node.body, from_line=-1) - for xit in exits: - self.arcs.add((xit, -start)) - else: - # Empty module. - self.arcs.add((-1, start)) - self.arcs.add((start, -1)) - elif node_name in ["FunctionDef", "AsyncFunctionDef"]: - start = self.line_for_node(node) - self.block_stack.append(FunctionBlock(start=start)) - exits = self.add_body_arcs(node.body, from_line=-1) - self.block_stack.pop() - for xit in exits: - self.arcs.add((xit, -start)) - elif node_name == "ClassDef": - start = self.line_for_node(node) - self.arcs.add((-1, start)) - exits = self.add_body_arcs(node.body, from_line=start) - for xit in exits: - self.arcs.add((xit, -start)) - elif node_name in self.CODE_COMPREHENSIONS: - start = self.line_for_node(node) - self.arcs.add((-1, start)) - self.arcs.add((start, -start)) - elif node_name == "Lambda": - start = self.line_for_node(node) - self.arcs.add((-1, start)) - self.arcs.add((start, -start)) - # TODO: test multi-line lambdas + def code_object_Lambda(self, node): + start = self.line_for_node(node) + self.arcs.add((-1, start)) + self.arcs.add((start, -start)) + # TODO: test multi-line lambdas def contains_return_expression(self, node): """Is there a yield-from or await in `node` someplace?""" -- cgit v1.2.1 From badf53c6cb26118cfdf3388a6eb09fd21e6c7428 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2016 08:46:59 -0500 Subject: Name the dispatched-to methods more unusually --HG-- branch : ast-branch --- coverage/parser.py | 68 +++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index c5d7c618..647dbd05 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -355,29 +355,29 @@ class AstArcAnalyzer(object): def line_for_node(self, node): """What is the right line number to use for this node?""" node_name = node.__class__.__name__ - handler = getattr(self, "line_" + node_name, None) + handler = getattr(self, "_line__" + node_name, None) if handler is not None: return handler(node) else: return node.lineno - def line_Assign(self, node): + def _line__Assign(self, node): return self.line_for_node(node.value) - def line_Dict(self, node): + def _line__Dict(self, node): # Python 3.5 changed how dict literals are made. if env.PYVERSION >= (3, 5) and node.keys: return node.keys[0].lineno else: return node.lineno - def line_List(self, node): + def _line__List(self, node): if node.elts: return self.line_for_node(node.elts[0]) else: return node.lineno - def line_Module(self, node): + def _line__Module(self, node): if node.body: return self.line_for_node(node.body[0]) else: @@ -404,7 +404,7 @@ class AstArcAnalyzer(object): break node_name = node.__class__.__name__ - handler = getattr(self, "handle_" + node_name, None) + handler = getattr(self, "_handle__" + node_name, None) if handler is not None: return handler(node) @@ -487,12 +487,12 @@ class AstArcAnalyzer(object): ## Handlers - def handle_Break(self, node): + def _handle__Break(self, node): here = self.line_for_node(node) self.process_break_exits([here]) return set() - def handle_ClassDef(self, node): + def _handle__ClassDef(self, node): return self.process_decorated(node, self.classdefs) def process_decorated(self, node, defs): @@ -513,12 +513,12 @@ class AstArcAnalyzer(object): # the body is handled in add_arcs_for_code_objects. return set([last]) - def handle_Continue(self, node): + def _handle__Continue(self, node): here = self.line_for_node(node) self.process_continue_exits([here]) return set() - def handle_For(self, node): + def _handle__For(self, node): start = self.line_for_node(node.iter) self.block_stack.append(LoopBlock(start=start)) exits = self.add_body_arcs(node.body, from_line=start) @@ -534,31 +534,31 @@ class AstArcAnalyzer(object): exits.add(start) return exits - handle_AsyncFor = handle_For + _handle__AsyncFor = _handle__For - def handle_FunctionDef(self, node): + def _handle__FunctionDef(self, node): return self.process_decorated(node, self.funcdefs) - handle_AsyncFunctionDef = handle_FunctionDef + _handle__AsyncFunctionDef = _handle__FunctionDef - def handle_If(self, node): + def _handle__If(self, node): start = self.line_for_node(node.test) exits = self.add_body_arcs(node.body, from_line=start) exits |= self.add_body_arcs(node.orelse, from_line=start) return exits - def handle_Raise(self, node): + 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): + def _handle__Return(self, node): here = self.line_for_node(node) self.process_return_exits([here]) return set() - def handle_Try(self, node): + 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. @@ -624,14 +624,14 @@ class AstArcAnalyzer(object): self.process_return_exits(exits) return exits - def handle_TryExcept(self, node): + def _handle__TryExcept(self, node): # Python 2.7 uses separate TryExcept and TryFinally nodes. If we get # TryExcept, it means there was no finally, so fake it, and treat as # a general Try node. node.finalbody = [] - return self.handle_Try(node) + return self._handle__Try(node) - def handle_TryFinally(self, node): + def _handle__TryFinally(self, node): # Python 2.7 uses separate TryExcept and TryFinally nodes. If we get # TryFinally, see if there's a TryExcept nested inside. If so, merge # them. Otherwise, fake fields to complete a Try node. @@ -645,9 +645,9 @@ class AstArcAnalyzer(object): node.handlers = first.handlers node.orelse = first.orelse - return self.handle_Try(node) + return self._handle__Try(node) - def handle_While(self, node): + def _handle__While(self, node): constant_test = self.is_constant_expr(node.test) start = to_top = self.line_for_node(node.test) if constant_test: @@ -668,21 +668,21 @@ class AstArcAnalyzer(object): exits.add(start) return exits - def handle_With(self, node): + def _handle__With(self, node): start = self.line_for_node(node) exits = self.add_body_arcs(node.body, from_line=start) return exits - handle_AsyncWith = handle_With + _handle__AsyncWith = _handle__With def add_arcs_for_code_objects(self, root_node): for node in ast.walk(root_node): node_name = node.__class__.__name__ - code_object_handler = getattr(self, "code_object_" + node_name, None) + code_object_handler = getattr(self, "_code_object__" + node_name, None) if code_object_handler is not None: code_object_handler(node) - def code_object_Module(self, node): + def _code_object__Module(self, node): start = self.line_for_node(node) if node.body: exits = self.add_body_arcs(node.body, from_line=-1) @@ -693,7 +693,7 @@ class AstArcAnalyzer(object): self.arcs.add((-1, start)) self.arcs.add((start, -1)) - def code_object_FunctionDef(self, node): + def _code_object__FunctionDef(self, node): start = self.line_for_node(node) self.block_stack.append(FunctionBlock(start=start)) exits = self.add_body_arcs(node.body, from_line=-1) @@ -701,9 +701,9 @@ class AstArcAnalyzer(object): for xit in exits: self.arcs.add((xit, -start)) - code_object_AsyncFunctionDef = code_object_FunctionDef + _code_object__AsyncFunctionDef = _code_object__FunctionDef - def code_object_ClassDef(self, node): + def _code_object__ClassDef(self, node): start = self.line_for_node(node) self.arcs.add((-1, start)) exits = self.add_body_arcs(node.body, from_line=start) @@ -715,13 +715,13 @@ class AstArcAnalyzer(object): self.arcs.add((-1, start)) self.arcs.add((start, -start)) - code_object_GeneratorExp = do_code_object_comprehension - code_object_DictComp = do_code_object_comprehension - code_object_SetComp = do_code_object_comprehension + _code_object__GeneratorExp = do_code_object_comprehension + _code_object__DictComp = do_code_object_comprehension + _code_object__SetComp = do_code_object_comprehension if env.PY3: - code_object_ListComp = do_code_object_comprehension + _code_object__ListComp = do_code_object_comprehension - def code_object_Lambda(self, node): + def _code_object__Lambda(self, node): start = self.line_for_node(node) self.arcs.add((-1, start)) self.arcs.add((start, -start)) -- cgit v1.2.1 From 8b7c4c1bf2bd0ea40c6da1c9d09f4f978835fa3b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2016 16:27:24 -0500 Subject: Remove the old bytecode-based branch analyzer --HG-- branch : ast-branch --- coverage/backward.py | 8 -- coverage/bytecode.py | 65 --------- coverage/parser.py | 361 +------------------------------------------------ tests/test_backward.py | 3 +- 4 files changed, 3 insertions(+), 434 deletions(-) diff --git a/coverage/backward.py b/coverage/backward.py index 4fc72215..50d49a0f 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -93,10 +93,6 @@ if env.PY3: """Produce a byte string with the ints from `byte_values`.""" return bytes(byte_values) - def byte_to_int(byte_value): - """Turn an element of a bytes object into an int.""" - return byte_value - def bytes_to_ints(bytes_value): """Turn a bytes object into a sequence of ints.""" # In Python 3, iterating bytes gives ints. @@ -111,10 +107,6 @@ else: """Produce a byte string with the ints from `byte_values`.""" return "".join(chr(b) for b in byte_values) - def byte_to_int(byte_value): - """Turn an element of a bytes object into an int.""" - return ord(byte_value) - def bytes_to_ints(bytes_value): """Turn a bytes object into a sequence of ints.""" for byte in bytes_value: diff --git a/coverage/bytecode.py b/coverage/bytecode.py index 82929cef..d823c67c 100644 --- a/coverage/bytecode.py +++ b/coverage/bytecode.py @@ -3,73 +3,8 @@ """Bytecode manipulation for coverage.py""" -import opcode import types -from coverage.backward import byte_to_int - - -class ByteCode(object): - """A single bytecode.""" - def __init__(self): - # The offset of this bytecode in the code object. - self.offset = -1 - - # The opcode, defined in the `opcode` module. - self.op = -1 - - # The argument, a small integer, whose meaning depends on the opcode. - self.arg = -1 - - # The offset in the code object of the next bytecode. - self.next_offset = -1 - - # The offset to jump to. - self.jump_to = -1 - - -class ByteCodes(object): - """Iterator over byte codes in `code`. - - This handles the logic of EXTENDED_ARG byte codes internally. Those byte - codes are not returned by this iterator. - - Returns `ByteCode` objects. - - """ - def __init__(self, code): - self.code = code - - def __getitem__(self, i): - return byte_to_int(self.code[i]) - - def __iter__(self): - offset = 0 - ext_arg = 0 - while offset < len(self.code): - bc = ByteCode() - bc.op = self[offset] - bc.offset = offset - - next_offset = offset+1 - if bc.op >= opcode.HAVE_ARGUMENT: - bc.arg = ext_arg + self[offset+1] + 256*self[offset+2] - next_offset += 2 - - label = -1 - if bc.op in opcode.hasjrel: - label = next_offset + bc.arg - elif bc.op in opcode.hasjabs: - label = bc.arg - bc.jump_to = label - - bc.next_offset = offset = next_offset - if bc.op == opcode.EXTENDED_ARG: - ext_arg = bc.arg * 256*256 - else: - ext_arg = 0 - yield bc - class CodeObjects(object): """Iterate over all the code objects in `code`.""" diff --git a/coverage/parser.py b/coverage/parser.py index 647dbd05..32a75900 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -5,7 +5,6 @@ import ast import collections -import dis import os import re import token @@ -14,7 +13,7 @@ import tokenize from coverage import env from coverage.backward import range # pylint: disable=redefined-builtin from coverage.backward import bytes_to_ints, string_class -from coverage.bytecode import ByteCodes, CodeObjects +from coverage.bytecode import CodeObjects from coverage.misc import contract, nice_pair, join_regex from coverage.misc import CoverageException, NoSource, NotPython from coverage.phystokens import compile_unicode, generate_tokens, neuter_encoding_declaration @@ -253,22 +252,6 @@ class PythonParser(object): starts = self.raw_statements - ignore self.statements = self.first_lines(starts) - ignore - 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 - to the first line of multi-line statements. - - """ - if self._all_arcs is None: - self._all_arcs = set() - for l1, l2 in self.byte_parser._all_arcs(): - fl1 = self.first_line(l1) - fl2 = self.first_line(l2) - if fl1 != fl2: - self._all_arcs.add((fl1, fl2)) - return self._all_arcs - def arcs(self): if self._all_arcs is None: aaa = AstArcAnalyzer(self.text, self.raw_funcdefs, self.raw_classdefs) @@ -736,62 +719,6 @@ class AstArcAnalyzer(object): return False -## Opcodes that guide the ByteParser. - -def _opcode(name): - """Return the opcode by name from the dis module.""" - return dis.opmap[name] - - -def _opcode_set(*names): - """Return a set of opcodes by the names in `names`.""" - s = set() - for name in names: - try: - s.add(_opcode(name)) - except KeyError: - pass - return s - -# Opcodes that leave the code object. -OPS_CODE_END = _opcode_set('RETURN_VALUE') - -# Opcodes that unconditionally end the code chunk. -OPS_CHUNK_END = _opcode_set( - 'JUMP_ABSOLUTE', 'JUMP_FORWARD', 'RETURN_VALUE', 'RAISE_VARARGS', - 'BREAK_LOOP', 'CONTINUE_LOOP', -) - -# Opcodes that unconditionally begin a new code chunk. By starting new chunks -# with unconditional jump instructions, we neatly deal with jumps to jumps -# properly. -OPS_CHUNK_BEGIN = _opcode_set('JUMP_ABSOLUTE', 'JUMP_FORWARD') - -# Opcodes that push a block on the block stack. -OPS_PUSH_BLOCK = _opcode_set( - 'SETUP_LOOP', 'SETUP_EXCEPT', 'SETUP_FINALLY', 'SETUP_WITH', 'SETUP_ASYNC_WITH', -) - -# Block types for exception handling. -OPS_EXCEPT_BLOCKS = _opcode_set('SETUP_EXCEPT', 'SETUP_FINALLY') - -# Opcodes that pop a block from the block stack. -OPS_POP_BLOCK = _opcode_set('POP_BLOCK') - -OPS_GET_AITER = _opcode_set('GET_AITER') - -# Opcodes that have a jump destination, but aren't really a jump. -OPS_NO_JUMP = OPS_PUSH_BLOCK - -# Individual opcodes we need below. -OP_BREAK_LOOP = _opcode('BREAK_LOOP') -OP_END_FINALLY = _opcode('END_FINALLY') -OP_COMPARE_OP = _opcode('COMPARE_OP') -COMPARE_EXCEPTION = 10 # just have to get this constant from the code. -OP_LOAD_CONST = _opcode('LOAD_CONST') -OP_RETURN_VALUE = _opcode('RETURN_VALUE') - - class ByteParser(object): """Parse byte codes to understand the structure of code.""" @@ -812,7 +739,7 @@ class ByteParser(object): # Alternative Python implementations don't always provide all the # attributes on code objects that we need to do the analysis. - for attr in ['co_lnotab', 'co_firstlineno', 'co_consts', 'co_code']: + for attr in ['co_lnotab', 'co_firstlineno', 'co_consts']: if not hasattr(self.code, attr): raise CoverageException( "This implementation of Python doesn't support code analysis.\n" @@ -867,290 +794,6 @@ class ByteParser(object): for _, l in bp._bytes_lines(): yield l - def _block_stack_repr(self, block_stack): # pragma: debugging - """Get a string version of `block_stack`, for debugging.""" - blocks = ", ".join( - "(%s, %r)" % (dis.opname[b[0]], b[1]) for b in block_stack - ) - return "[" + blocks + "]" - - def _split_into_chunks(self): - """Split the code object into a list of `Chunk` objects. - - Each chunk is only entered at its first instruction, though there can - be many exits from a chunk. - - Returns a list of `Chunk` objects. - - """ - # The list of chunks so far, and the one we're working on. - chunks = [] - chunk = None - - # A dict mapping byte offsets of line starts to the line numbers. - bytes_lines_map = dict(self._bytes_lines()) - - # The block stack: loops and try blocks get pushed here for the - # implicit jumps that can occur. - # Each entry is a tuple: (block type, destination) - block_stack = [] - - # Some op codes are followed by branches that should be ignored. This - # is a count of how many ignores are left. - ignore_branch = 0 - - ignore_pop_block = 0 - - # We have to handle the last two bytecodes specially. - ult = penult = None - - # Get a set of all of the jump-to points. - jump_to = set() - bytecodes = list(ByteCodes(self.code.co_code)) - for bc in bytecodes: - if bc.jump_to >= 0: - jump_to.add(bc.jump_to) - - chunk_lineno = 0 - - # Walk the byte codes building chunks. - for bc in bytecodes: - # Maybe have to start a new chunk. - start_new_chunk = False - first_chunk = False - if bc.offset in bytes_lines_map: - # Start a new chunk for each source line number. - start_new_chunk = True - chunk_lineno = bytes_lines_map[bc.offset] - first_chunk = True - elif bc.offset in jump_to: - # To make chunks have a single entrance, we have to make a new - # chunk when we get to a place some bytecode jumps to. - start_new_chunk = True - elif bc.op in OPS_CHUNK_BEGIN: - # Jumps deserve their own unnumbered chunk. This fixes - # problems with jumps to jumps getting confused. - start_new_chunk = True - - if not chunk or start_new_chunk: - if chunk: - chunk.exits.add(bc.offset) - chunk = Chunk(bc.offset, chunk_lineno, first_chunk) - if not chunks: - # The very first chunk of a code object is always an - # entrance. - chunk.entrance = True - chunks.append(chunk) - - # Look at the opcode. - if bc.jump_to >= 0 and bc.op not in OPS_NO_JUMP: - if ignore_branch: - # Someone earlier wanted us to ignore this branch. - ignore_branch -= 1 - else: - # The opcode has a jump, it's an exit for this chunk. - chunk.exits.add(bc.jump_to) - - if bc.op in OPS_CODE_END: - # The opcode can exit the code object. - chunk.exits.add(-self.code.co_firstlineno) - if bc.op in OPS_PUSH_BLOCK: - # The opcode adds a block to the block_stack. - block_stack.append((bc.op, bc.jump_to)) - if bc.op in OPS_POP_BLOCK: - # The opcode pops a block from the block stack. - if ignore_pop_block: - ignore_pop_block -= 1 - else: - block_stack.pop() - if bc.op in OPS_CHUNK_END: - # This opcode forces the end of the chunk. - if bc.op == OP_BREAK_LOOP: - # A break is implicit: jump where the top of the - # block_stack points. - chunk.exits.add(block_stack[-1][1]) - chunk = None - if bc.op == OP_END_FINALLY: - # For the finally clause we need to find the closest exception - # block, and use its jump target as an exit. - for block in reversed(block_stack): - if block[0] in OPS_EXCEPT_BLOCKS: - chunk.exits.add(block[1]) - break - if bc.op == OP_COMPARE_OP and bc.arg == COMPARE_EXCEPTION: - # This is an except clause. We want to overlook the next - # branch, so that except's don't count as branches. - ignore_branch += 1 - - if bc.op in OPS_GET_AITER: - # GET_AITER is weird: First, it seems to generate one more - # POP_BLOCK than SETUP_*, so we have to prepare to ignore one - # of the POP_BLOCKS. Second, we don't have a clear branch to - # the exit of the loop, so we peek into the block stack to find - # it. - ignore_pop_block += 1 - chunk.exits.add(block_stack[-1][1]) - - penult = ult - ult = bc - - if chunks: - # The last two bytecodes could be a dummy "return None" that - # shouldn't be counted as real code. Every Python code object seems - # to end with a return, and a "return None" is inserted if there - # isn't an explicit return in the source. - if ult and penult: - if penult.op == OP_LOAD_CONST and ult.op == OP_RETURN_VALUE: - if self.code.co_consts[penult.arg] is None: - # This is "return None", but is it dummy? A real line - # would be a last chunk all by itself. - if chunks[-1].byte != penult.offset: - ex = -self.code.co_firstlineno - # Split the last chunk - last_chunk = chunks[-1] - last_chunk.exits.remove(ex) - last_chunk.exits.add(penult.offset) - chunk = Chunk( - penult.offset, last_chunk.line, False - ) - chunk.exits.add(ex) - chunks.append(chunk) - - # Give all the chunks a length. - chunks[-1].length = bc.next_offset - chunks[-1].byte - for i in range(len(chunks)-1): - chunks[i].length = chunks[i+1].byte - chunks[i].byte - - #self.validate_chunks(chunks) - return chunks - - def validate_chunks(self, chunks): # pragma: debugging - """Validate the rule that chunks have a single entrance.""" - # starts is the entrances to the chunks - starts = set(ch.byte for ch in chunks) - for ch in chunks: - assert all((ex in starts or ex < 0) for ex in ch.exits) - - def _arcs(self): - """Find the executable arcs in the code. - - Yields pairs: (from,to). From and to are integer line numbers. If - from is < 0, then the arc is an entrance into the code object. If to - is < 0, the arc is an exit from the code object. - - """ - chunks = self._split_into_chunks() - - # A map from byte offsets to the chunk starting at that offset. - byte_chunks = dict((c.byte, c) for c in chunks) - - # Traverse from the first chunk in each line, and yield arcs where - # the trace function will be invoked. - for chunk in chunks: - if chunk.entrance: - yield (-1, chunk.line) - - if not chunk.first: - continue - - chunks_considered = set() - chunks_to_consider = [chunk] - while chunks_to_consider: - # Get the chunk we're considering, and make sure we don't - # consider it again. - this_chunk = chunks_to_consider.pop() - chunks_considered.add(this_chunk) - - # For each exit, add the line number if the trace function - # would be triggered, or add the chunk to those being - # considered if not. - for ex in this_chunk.exits: - if ex < 0: - yield (chunk.line, ex) - else: - next_chunk = byte_chunks[ex] - if next_chunk in chunks_considered: - continue - - # The trace function is invoked if visiting the first - # bytecode in a line, or if the transition is a - # backward jump. - backward_jump = next_chunk.byte < this_chunk.byte - if next_chunk.first or backward_jump: - if next_chunk.line != chunk.line: - yield (chunk.line, next_chunk.line) - else: - chunks_to_consider.append(next_chunk) - - def _all_chunks(self): - """Returns a list of `Chunk` objects for this code and its children. - - See `_split_into_chunks` for details. - - """ - chunks = [] - for bp in self.child_parsers(): - chunks.extend(bp._split_into_chunks()) - - return chunks - - def _all_arcs(self): - """Get the set of all arcs in this code object and its children. - - See `_arcs` for details. - - """ - arcs = set() - for bp in self.child_parsers(): - arcs.update(bp._arcs()) - - return arcs - - -class Chunk(object): - """A sequence of byte codes with a single entrance. - - To analyze byte code, we have to divide it into chunks, sequences of byte - codes such that each chunk has only one entrance, the first instruction in - the block. - - This is almost the CS concept of `basic block`_, except that we're willing - to have many exits from a chunk, and "basic block" is a more cumbersome - term. - - .. _basic block: http://en.wikipedia.org/wiki/Basic_block - - `byte` is the offset to the bytecode starting this chunk. - - `line` is the source line number containing this chunk. - - `first` is true if this is the first chunk in the source line. - - An exit < 0 means the chunk can leave the code (return). The exit is - the negative of the starting line number of the code block. - - The `entrance` attribute is a boolean indicating whether the code object - can be entered at this chunk. - - """ - def __init__(self, byte, line, first): - self.byte = byte - self.line = line - self.first = first - self.length = 0 - self.entrance = False - self.exits = set() - - def __repr__(self): - return "<%d+%d @%d%s%s %r>" % ( - self.byte, - self.length, - self.line, - "!" if self.first else "", - "v" if self.entrance else "", - list(self.exits), - ) - SKIP_DUMP_FIELDS = ["ctx"] diff --git a/tests/test_backward.py b/tests/test_backward.py index fbb9ad8b..bbecb780 100644 --- a/tests/test_backward.py +++ b/tests/test_backward.py @@ -4,7 +4,7 @@ """Tests that our version shims in backward.py are working.""" from coverage.backunittest import TestCase -from coverage.backward import iitems, binary_bytes, byte_to_int, bytes_to_ints +from coverage.backward import iitems, binary_bytes, bytes_to_ints class BackwardTest(TestCase): @@ -20,4 +20,3 @@ class BackwardTest(TestCase): bb = binary_bytes(byte_values) self.assertEqual(len(bb), len(byte_values)) self.assertEqual(byte_values, list(bytes_to_ints(bb))) - self.assertEqual(byte_values, [byte_to_int(b) for b in bb]) -- cgit v1.2.1 From cefd14cafc49a244c865885c87f019217d6d3a2f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 7 Jan 2016 08:46:35 -0500 Subject: Bytecode not byte code --HG-- branch : ast-branch --- coverage/parser.py | 4 ++-- lab/branches.py | 2 +- lab/disgen.py | 6 +++--- tests/test_arcs.py | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 32a75900..16419ca4 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -142,7 +142,7 @@ class PythonParser(object): indent -= 1 elif toktype == token.NAME: if ttext == 'class': - # Class definitions look like branches in the byte code, so + # Class definitions look like branches in the bytecode, so # we need to exclude them. The simplest way is to note the # lines with the 'class' keyword. self.raw_classdefs.add(slineno) @@ -720,7 +720,7 @@ class AstArcAnalyzer(object): class ByteParser(object): - """Parse byte codes to understand the structure of code.""" + """Parse bytecode to understand the structure of code.""" @contract(text='unicode') def __init__(self, text, code=None, filename=None): diff --git a/lab/branches.py b/lab/branches.py index 275eef4a..d1908d0f 100644 --- a/lab/branches.py +++ b/lab/branches.py @@ -21,7 +21,7 @@ def my_function(x): # Notice that "while 1" also has this problem. Even though the compiler # knows there's no computation at the top of the loop, it's still expressed - # in byte code as a branch with two possibilities. + # in bytecode as a branch with two possibilities. i = 0 while 1: diff --git a/lab/disgen.py b/lab/disgen.py index 4e4c6fa6..26bc56bc 100644 --- a/lab/disgen.py +++ b/lab/disgen.py @@ -1,4 +1,4 @@ -"""Disassembler of Python byte code into mnemonics.""" +"""Disassembler of Python bytecode into mnemonics.""" # Adapted from stdlib dis.py, but returns structured information # instead of printing to stdout. @@ -133,7 +133,7 @@ def byte_from_code(code, i): return byte def findlabels(code): - """Detect all offsets in a byte code which are jump targets. + """Detect all offsets in a bytecode which are jump targets. Return the list of offsets. @@ -158,7 +158,7 @@ def findlabels(code): return labels def findlinestarts(code): - """Find the offsets in a byte code which are start of lines in the source. + """Find the offsets in a bytecode which are start of lines in the source. Generate pairs (offset, lineno) as described in Python/compile.c. diff --git a/tests/test_arcs.py b/tests/test_arcs.py index bb811c01..bcc6c024 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -942,9 +942,11 @@ class MiscArcTest(CoverageTest): def test_pathologically_long_code_object(self): # https://bitbucket.org/ned/coveragepy/issue/359 - # The structure of this file is such that an EXTENDED_ARG byte code is + # The structure of this file is such that an EXTENDED_ARG bytecode is # needed to encode the jump at the end. We weren't interpreting those # opcodes. + # Note that we no longer interpret bytecode at all, but it couldn't + # hurt to keep the test... code = """\ data = [ """ + "".join("""\ -- cgit v1.2.1 From 1a57255a7fe11f6a4318b728dfa90131c97b7eee Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 7 Jan 2016 08:56:40 -0500 Subject: A test that I'll fix soon --HG-- branch : ast-branch --- tests/test_arcs.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index bcc6c024..b7976889 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -964,6 +964,25 @@ class MiscArcTest(CoverageTest): arcs_missing=[], arcs_unpredicted=[], ) + def test_optimized_away_lines(self): + self.skip("TODO: fix this test") + self.check_coverage("""\ + a = 1 + if len([2]): + c = 3 + if 0: # this line isn't in the compiled code. + if len([5]): + d = 6 + e = 7 + """, + lines=[1, 2, 3, 7], + arcz=".1 12 23 27 37 7.", + ) + + +class DecoractorArcTest(CoverageTest): + """Tests of arcs with decorators.""" + def test_function_decorator(self): self.check_coverage("""\ def decorator(arg): -- cgit v1.2.1 From 152dd7d6e4b9a53e89cb7ec0cacf0f01be4abc73 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 7 Jan 2016 12:06:11 -0500 Subject: Clean up small stuff --HG-- branch : ast-branch --- coverage/parser.py | 9 +++++++++ coverage/python.py | 4 ---- pylintrc | 6 +++++- tests/test_arcs.py | 2 ++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 16419ca4..c03a3083 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -253,6 +253,12 @@ class PythonParser(object): self.statements = self.first_lines(starts) - ignore def arcs(self): + """Get information about the arcs available in the code. + + Returns a set of line number pairs. Line numbers have been normalized + to the first line of multi-line statements. + + """ if self._all_arcs is None: aaa = AstArcAnalyzer(self.text, self.raw_funcdefs, self.raw_classdefs) arcs = aaa.collect_arcs() @@ -298,10 +304,12 @@ class LoopBlock(object): 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? @@ -803,6 +811,7 @@ def is_simple_value(value): isinstance(value, (string_class, int, float)) ) +# TODO: a test of ast_dump? def ast_dump(node, depth=0): indent = " " * depth if not isinstance(node, ast.AST): diff --git a/coverage/python.py b/coverage/python.py index bf19cb22..5e563828 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -159,10 +159,6 @@ class PythonFileReporter(FileReporter): def arcs(self): return self.parser.arcs() - @expensive - def ast_arcs(self): - return self.parser.ast_arcs() - @expensive def exit_counts(self): return self.parser.exit_counts() diff --git a/pylintrc b/pylintrc index 09ac1416..4dc9c8e1 100644 --- a/pylintrc +++ b/pylintrc @@ -134,7 +134,11 @@ required-attributes= # Regular expression which should only match functions or classes name which do # not require a docstring -no-docstring-rgx=__.*__|test[A-Z_].*|setUp|tearDown +# Special methods don't: __foo__ +# Test methods don't: testXXXX +# TestCase overrides don't: setUp, tearDown +# Dispatched methods don't: _xxx__Xxxx +no-docstring-rgx=__.*__|test[A-Z_].*|setUp|tearDown|_.*__.* # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ diff --git a/tests/test_arcs.py b/tests/test_arcs.py index b7976889..28c1df72 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -1032,6 +1032,8 @@ class DecoractorArcTest(CoverageTest): class AsyncTest(CoverageTest): + """Tests of the new async and await keywords in Python 3.5""" + def setUp(self): if env.PYVERSION < (3, 5): self.skip("Async features are new in Python 3.5") -- cgit v1.2.1 From fcc18ffcb11bca315c6c0690414e817d5c22b4dd Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 7 Jan 2016 19:42:09 -0500 Subject: Make lab/parser.py usable on snippets within larger Python files. --HG-- branch : ast-branch --- lab/parser.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lab/parser.py b/lab/parser.py index 717fbbf9..08b50921 100644 --- a/lab/parser.py +++ b/lab/parser.py @@ -5,9 +5,13 @@ from __future__ import division -import glob, os, sys import collections -from optparse import OptionParser +import glob +import optparse +import os +import re +import sys +import textwrap import disgen @@ -24,7 +28,7 @@ class ParserMain(object): def main(self, args): """A main function for trying the code from the command line.""" - parser = OptionParser() + parser = optparse.OptionParser() parser.add_option( "-c", action="store_true", dest="chunks", help="Show basic block chunks" @@ -72,8 +76,20 @@ class ParserMain(object): def one_file(self, options, filename): """Process just one file.""" + # `filename` can have a line number suffix. In that case, extract those + # lines, dedent them, and use that. + match = re.search(r"^(.*):(\d+)-(\d+)$", filename) + if match: + filename, start, end = match.groups() + start, end = int(start), int(end) + else: + start = end = None + try: text = get_python_source(filename) + if start is not None: + lines = text.splitlines(True) + text = textwrap.dedent("".join(lines[start-1:end])) bp = ByteParser(text, filename=filename) except Exception as err: print("%s" % (err,)) -- cgit v1.2.1 From 2e48dedf1ea439988fba0c9693cea7a818ab3213 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 7 Jan 2016 19:42:42 -0500 Subject: Add tests of multiline lambdas, though i don't quite understand the line numbers involved --HG-- branch : ast-branch --- coverage/parser.py | 5 ++--- tests/test_arcs.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index c03a3083..9f7400e5 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -312,8 +312,8 @@ class FunctionBlock(object): 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.handler_start = handler_start + self.final_start = final_start self.break_from = set() self.continue_from = set() self.return_from = set() @@ -716,7 +716,6 @@ class AstArcAnalyzer(object): start = self.line_for_node(node) self.arcs.add((-1, start)) self.arcs.add((start, -start)) - # TODO: test multi-line lambdas def contains_return_expression(self, node): """Is there a yield-from or await in `node` someplace?""" diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 28c1df72..c52bc8aa 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -1031,6 +1031,34 @@ class DecoractorArcTest(CoverageTest): ) +class LambdaArcTest(CoverageTest): + """Tests of lambdas""" + + def test_lambda(self): + self.check_coverage("""\ + fn = (lambda x: + x + 2 + ) + assert fn(4) == 6 + """, + arcz=".1 14 4-1 1-1", + ) + self.check_coverage("""\ + + fn = \\ + ( + lambda + x: + x + + + 8 + ) + assert fn(10) == 18 + """, + arcz=".2 2A A-4 2-4", + ) + + class AsyncTest(CoverageTest): """Tests of the new async and await keywords in Python 3.5""" @@ -1108,7 +1136,6 @@ class AsyncTest(CoverageTest): async with x: pass """, - # TODO: we don't run any code, so many arcs are missing. arcz=".1 1. .2 23 3.", arcz_missing=".2 23 3.", ) -- cgit v1.2.1 From d93ddb9524a3e3535541812bbeade8e8ff822409 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 7 Jan 2016 19:46:57 -0500 Subject: When extracting snippets, also need to undo backslashing --HG-- branch : ast-branch --- lab/parser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lab/parser.py b/lab/parser.py index 08b50921..5e5b4b36 100644 --- a/lab/parser.py +++ b/lab/parser.py @@ -77,7 +77,8 @@ class ParserMain(object): def one_file(self, options, filename): """Process just one file.""" # `filename` can have a line number suffix. In that case, extract those - # lines, dedent them, and use that. + # lines, dedent them, and use that. This is for trying test cases + # embedded in the test files. match = re.search(r"^(.*):(\d+)-(\d+)$", filename) if match: filename, start, end = match.groups() @@ -89,7 +90,7 @@ class ParserMain(object): text = get_python_source(filename) if start is not None: lines = text.splitlines(True) - text = textwrap.dedent("".join(lines[start-1:end])) + text = textwrap.dedent("".join(lines[start-1:end]).replace("\\\\", "\\")) bp = ByteParser(text, filename=filename) except Exception as err: print("%s" % (err,)) -- cgit v1.2.1