# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.py's code parsing.""" import textwrap import pytest from coverage import env from coverage.misc import NotPython from coverage.parser import PythonParser from tests.coveragetest import CoverageTest, xfail from tests.helpers import arcz_to_arcs class PythonParserTest(CoverageTest): """Tests for coverage.py's Python code parsing.""" run_in_temp_dir = False def parse_source(self, text): """Parse `text` as source, and return the `PythonParser` used.""" if env.PY2: text = text.decode("ascii") text = textwrap.dedent(text) parser = PythonParser(text=text, exclude="nocover") parser.parse_source() return parser def test_exit_counts(self): parser = self.parse_source("""\ # check some basic branch counting class Foo: def foo(self, a): if a: return 5 else: return 7 class Bar: pass """) assert parser.exit_counts() == { 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1 } def test_generator_exit_counts(self): # https://github.com/nedbat/coveragepy/issues/324 parser = self.parse_source("""\ def gen(input): for n in inp: yield (i * 2 for i in range(n)) list(gen([1,2,3])) """) assert parser.exit_counts() == { 1:1, # def -> list 2:2, # for -> yield; for -> exit 3:2, # yield -> for; genexp exit 5:1, # list -> exit } def test_try_except(self): parser = self.parse_source("""\ try: a = 2 except ValueError: a = 4 except ZeroDivideError: a = 6 except: a = 8 b = 9 """) assert parser.exit_counts() == { 1: 1, 2:1, 3:2, 4:1, 5:2, 6:1, 7:1, 8:1, 9:1 } def test_excluded_classes(self): parser = self.parse_source("""\ class Foo: def __init__(self): pass if len([]): # nocover class Bar: pass """) assert parser.exit_counts() == { 1:0, 2:1, 3:1 } def test_missing_branch_to_excluded_code(self): parser = self.parse_source("""\ if fooey: a = 2 else: # nocover a = 4 b = 5 """) assert parser.exit_counts() == { 1:1, 2:1, 5:1 } parser = self.parse_source("""\ def foo(): if fooey: a = 3 else: a = 5 b = 6 """) assert parser.exit_counts() == { 1:1, 2:2, 3:1, 5:1, 6:1 } parser = self.parse_source("""\ def foo(): if fooey: a = 3 else: # nocover a = 5 b = 6 """) assert parser.exit_counts() == { 1:1, 2:1, 3:1, 6:1 } def test_indentation_error(self): msg = ( "Couldn't parse '' as Python source: " "'unindent does not match any outer indentation level' at line 3" ) with pytest.raises(NotPython, match=msg): _ = self.parse_source("""\ 0 spaces 2 1 """) def test_token_error(self): msg = "Couldn't parse '' as Python source: 'EOF in multi-line string' at line 1" with pytest.raises(NotPython, match=msg): _ = self.parse_source("""\ ''' """) @xfail( env.PYPY3 and env.PYPYVERSION == (7, 3, 0), "https://bitbucket.org/pypy/pypy/issues/3139", ) def test_decorator_pragmas(self): parser = self.parse_source("""\ # 1 @foo(3) # nocover @bar def func(x, y=5): return 6 class Foo: # this is the only statement. '''9''' @foo # nocover def __init__(self): '''12''' return 13 @foo( # nocover 16, 17, ) def meth(self): return 20 @foo( # nocover 23 ) def func(x=25): return 26 """) raw_statements = {3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26} if env.PYBEHAVIOR.trace_decorated_def: raw_statements.update([11, 19]) assert parser.raw_statements == raw_statements assert parser.statements == {8} def test_class_decorator_pragmas(self): parser = self.parse_source("""\ class Foo(object): def __init__(self): self.x = 3 @foo # nocover class Bar(object): def __init__(self): self.x = 8 """) assert parser.raw_statements == {1, 2, 3, 5, 6, 7, 8} assert parser.statements == {1, 2, 3} def test_empty_decorated_function(self): parser = self.parse_source("""\ def decorator(func): return func @decorator def foo(self): '''Docstring''' @decorator def bar(self): pass """) if env.PYBEHAVIOR.trace_decorated_def: expected_statements = {1, 2, 4, 5, 8, 9, 10} expected_arcs = set(arcz_to_arcs(".1 14 45 58 89 9. .2 2. -8A A-8")) expected_exits = {1: 1, 2: 1, 4: 1, 5: 1, 8: 1, 9: 1, 10: 1} else: expected_statements = {1, 2, 4, 8, 10} expected_arcs = set(arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8")) expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1} if env.PYBEHAVIOR.docstring_only_function: # 3.7 changed how functions with only docstrings are numbered. expected_arcs.update(set(arcz_to_arcs("-46 6-4"))) expected_exits.update({6: 1}) assert expected_statements == parser.statements assert expected_arcs == parser.arcs() assert expected_exits == parser.exit_counts() class ParserMissingArcDescriptionTest(CoverageTest): """Tests for PythonParser.missing_arc_description.""" run_in_temp_dir = False def parse_text(self, source): """Parse Python source, and return the parser object.""" parser = PythonParser(text=textwrap.dedent(source)) parser.parse_source() return parser def test_missing_arc_description(self): # This code is never run, so the actual values don't matter. parser = self.parse_text(u"""\ if x: print(2) print(3) def func5(): for x in range(6): if x == 7: break def func10(): while something(11): thing(12) more_stuff(13) """) expected = "line 1 didn't jump to line 2, because the condition on line 1 was never true" assert expected == parser.missing_arc_description(1, 2) expected = "line 1 didn't jump to line 3, because the condition on line 1 was never false" assert expected == parser.missing_arc_description(1, 3) expected = ( "line 6 didn't return from function 'func5', " + "because the loop on line 6 didn't complete" ) assert expected == parser.missing_arc_description(6, -5) expected = "line 6 didn't jump to line 7, because the loop on line 6 never started" assert expected == parser.missing_arc_description(6, 7) expected = "line 11 didn't jump to line 12, because the condition on line 11 was never true" assert expected == parser.missing_arc_description(11, 12) expected = ( "line 11 didn't jump to line 13, " + "because the condition on line 11 was never false" ) assert expected == parser.missing_arc_description(11, 13) def test_missing_arc_descriptions_for_small_callables(self): parser = self.parse_text(u"""\ callables = [ lambda: 2, (x for x in range(3)), {x:1 for x in range(4)}, {x for x in range(5)}, ] x = 7 """) expected = "line 2 didn't finish the lambda on line 2" assert expected == parser.missing_arc_description(2, -2) expected = "line 3 didn't finish the generator expression on line 3" assert expected == parser.missing_arc_description(3, -3) expected = "line 4 didn't finish the dictionary comprehension on line 4" assert expected == parser.missing_arc_description(4, -4) expected = "line 5 didn't finish the set comprehension on line 5" assert expected == parser.missing_arc_description(5, -5) def test_missing_arc_descriptions_for_exceptions(self): parser = self.parse_text(u"""\ try: pass except ZeroDivideError: print("whoops") except ValueError: print("yikes") """) expected = ( "line 3 didn't jump to line 4, " + "because the exception caught by line 3 didn't happen" ) assert expected == parser.missing_arc_description(3, 4) expected = ( "line 5 didn't jump to line 6, " + "because the exception caught by line 5 didn't happen" ) assert expected == parser.missing_arc_description(5, 6) def test_missing_arc_descriptions_for_finally(self): parser = self.parse_text(u"""\ def function(): for i in range(2): try: if something(4): break elif something(6): x = 7 else: if something(9): continue else: continue if also_this(13): return 14 else: raise Exception(16) finally: this_thing(18) that_thing(19) """) if env.PYBEHAVIOR.finally_jumps_back: expected = "line 18 didn't jump to line 5, because the break on line 5 wasn't executed" assert expected == parser.missing_arc_description(18, 5) expected = "line 5 didn't jump to line 19, because the break on line 5 wasn't executed" assert expected == parser.missing_arc_description(5, 19) expected = ( "line 18 didn't jump to line 10, " + "because the continue on line 10 wasn't executed" ) assert expected == parser.missing_arc_description(18, 10) expected = ( "line 10 didn't jump to line 2, " + "because the continue on line 10 wasn't executed" ) assert expected == parser.missing_arc_description(10, 2) expected = ( "line 18 didn't jump to line 14, " + "because the return on line 14 wasn't executed" ) assert expected == parser.missing_arc_description(18, 14) expected = ( "line 14 didn't return from function 'function', " + "because the return on line 14 wasn't executed" ) assert expected == parser.missing_arc_description(14, -1) expected = ( "line 18 didn't except from function 'function', " + "because the raise on line 16 wasn't executed" ) assert expected == parser.missing_arc_description(18, -1) else: expected = ( "line 18 didn't jump to line 19, " + "because the break on line 5 wasn't executed" ) assert expected == parser.missing_arc_description(18, 19) expected = ( "line 18 didn't jump to line 2, " + "because the continue on line 10 wasn't executed" + " or " + "the continue on line 12 wasn't executed" ) assert expected == parser.missing_arc_description(18, 2) expected = ( "line 18 didn't except from function 'function', " + "because the raise on line 16 wasn't executed" + " or " + "line 18 didn't return from function 'function', " + "because the return on line 14 wasn't executed" ) assert expected == parser.missing_arc_description(18, -1) def test_missing_arc_descriptions_bug460(self): parser = self.parse_text(u"""\ x = 1 d = { 3: lambda: [], 4: lambda: [], } x = 6 """) assert parser.missing_arc_description(2, -3) == "line 3 didn't finish the lambda on line 3" class ParserFileTest(CoverageTest): """Tests for coverage.py's code parsing from files.""" def parse_file(self, filename): """Parse `text` as source, and return the `PythonParser` used.""" parser = PythonParser(filename=filename, exclude="nocover") parser.parse_source() return parser def test_line_endings(self): text = """\ # check some basic branch counting class Foo: def foo(self, a): if a: return 5 else: return 7 class Bar: pass """ counts = { 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1 } name_endings = (("unix", "\n"), ("dos", "\r\n"), ("mac", "\r")) for fname, newline in name_endings: fname = fname + ".py" self.make_file(fname, text, newline=newline) parser = self.parse_file(fname) assert parser.exit_counts() == counts, "Wrong for %r" % fname def test_encoding(self): self.make_file("encoded.py", """\ coverage = "\xe7\xf6v\xear\xe3g\xe9" """) parser = self.parse_file("encoded.py") assert parser.exit_counts() == {1: 1} def test_missing_line_ending(self): # Test that the set of statements is the same even if a final # multi-line statement has no final newline. # https://github.com/nedbat/coveragepy/issues/293 self.make_file("normal.py", """\ out, err = subprocess.Popen( [sys.executable, '-c', 'pass'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() """) parser = self.parse_file("normal.py") assert parser.statements == {1} self.make_file("abrupt.py", """\ out, err = subprocess.Popen( [sys.executable, '-c', 'pass'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()""") # no final newline. # Double-check that some test helper wasn't being helpful. with open("abrupt.py") as f: assert f.read()[-1] == ")" parser = self.parse_file("abrupt.py") assert parser.statements == {1}