blob: 0e6a0859c7785a76abd85de823d31aa89c1d3b95 [file] [log] [blame]
# 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
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
""")
self.assertEqual(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]))
""")
self.assertEqual(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
""")
self.assertEqual(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
""")
self.assertEqual(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
""")
self.assertEqual(parser.exit_counts(), { 1:1, 2:1, 5:1 })
parser = self.parse_source("""\
def foo():
if fooey:
a = 3
else:
a = 5
b = 6
""")
self.assertEqual(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
""")
self.assertEqual(parser.exit_counts(), { 1:1, 2:1, 3:1, 6:1 })
def test_indentation_error(self):
msg = (
"Couldn't parse '<code>' as Python source: "
"'unindent does not match any outer indentation level' at line 3"
)
with self.assertRaisesRegex(NotPython, msg):
_ = self.parse_source("""\
0 spaces
2
1
""")
def test_token_error(self):
msg = "Couldn't parse '<code>' as Python source: 'EOF in multi-line string' at line 1"
with self.assertRaisesRegex(NotPython, 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 = set([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])
self.assertEqual(parser.raw_statements, raw_statements)
self.assertEqual(parser.statements, set([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
""")
self.assertEqual(parser.raw_statements, set([1, 2, 3, 5, 6, 7, 8]))
self.assertEqual(parser.statements, set([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 (not env.PYPY) and (env.PYVERSION >= (3, 7, 0, 'beta', 5)):
# 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})
self.assertEqual(expected_statements, parser.statements)
self.assertEqual(expected_arcs, parser.arcs())
self.assertEqual(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)
""")
self.assertEqual(
parser.missing_arc_description(1, 2),
"line 1 didn't jump to line 2, because the condition on line 1 was never true"
)
self.assertEqual(
parser.missing_arc_description(1, 3),
"line 1 didn't jump to line 3, because the condition on line 1 was never false"
)
self.assertEqual(
parser.missing_arc_description(6, -5),
"line 6 didn't return from function 'func5', "
"because the loop on line 6 didn't complete"
)
self.assertEqual(
parser.missing_arc_description(6, 7),
"line 6 didn't jump to line 7, because the loop on line 6 never started"
)
self.assertEqual(
parser.missing_arc_description(11, 12),
"line 11 didn't jump to line 12, because the condition on line 11 was never true"
)
self.assertEqual(
parser.missing_arc_description(11, 13),
"line 11 didn't jump to line 13, because the condition on line 11 was never false"
)
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
""")
self.assertEqual(
parser.missing_arc_description(2, -2),
"line 2 didn't finish the lambda on line 2"
)
self.assertEqual(
parser.missing_arc_description(3, -3),
"line 3 didn't finish the generator expression on line 3"
)
self.assertEqual(
parser.missing_arc_description(4, -4),
"line 4 didn't finish the dictionary comprehension on line 4"
)
self.assertEqual(
parser.missing_arc_description(5, -5),
"line 5 didn't finish the set comprehension on line 5"
)
def test_missing_arc_descriptions_for_exceptions(self):
parser = self.parse_text(u"""\
try:
pass
except ZeroDivideError:
print("whoops")
except ValueError:
print("yikes")
""")
self.assertEqual(
parser.missing_arc_description(3, 4),
"line 3 didn't jump to line 4, because the exception caught by line 3 didn't happen"
)
self.assertEqual(
parser.missing_arc_description(5, 6),
"line 5 didn't jump to line 6, because the exception caught by line 5 didn't happen"
)
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
else:
if something(7):
continue
else:
continue
if also_this(11):
return 12
else:
raise Exception(14)
finally:
this_thing(16)
that_thing(17)
""")
if env.PYBEHAVIOR.finally_jumps_back:
self.assertEqual(
parser.missing_arc_description(16, 5),
"line 16 didn't jump to line 5, because the break on line 5 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(5, 17),
"line 5 didn't jump to line 17, because the break on line 5 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, 8),
"line 16 didn't jump to line 8, because the continue on line 8 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(8, 2),
"line 8 didn't jump to line 2, because the continue on line 8 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, 12),
"line 16 didn't jump to line 12, because the return on line 12 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(12, -1),
"line 12 didn't return from function 'function', "
"because the return on line 12 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, -1),
"line 16 didn't except from function 'function', "
"because the raise on line 14 wasn't executed"
)
else:
self.assertEqual(
parser.missing_arc_description(16, 17),
"line 16 didn't jump to line 17, because the break on line 5 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, 2),
"line 16 didn't jump to line 2, "
"because the continue on line 8 wasn't executed"
" or "
"the continue on line 10 wasn't executed"
)
self.assertEqual(
parser.missing_arc_description(16, -1),
"line 16 didn't except from function 'function', "
"because the raise on line 14 wasn't executed"
" or "
"line 16 didn't return from function 'function', "
"because the return on line 12 wasn't executed"
)
def test_missing_arc_descriptions_bug460(self):
parser = self.parse_text(u"""\
x = 1
d = {
3: lambda: [],
4: lambda: [],
}
x = 6
""")
self.assertEqual(
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)
self.assertEqual(
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")
self.assertEqual(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")
self.assertEqual(parser.statements, set([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:
self.assertEqual(f.read()[-1], ")")
parser = self.parse_file("abrupt.py")
self.assertEqual(parser.statements, set([1]))