| # 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.""" |
| |
| from __future__ import annotations |
| |
| import ast |
| import re |
| import textwrap |
| from unittest import mock |
| |
| import pytest |
| |
| from coverage import env |
| from coverage.exceptions import NoSource, NotPython |
| from coverage.parser import PythonParser, is_constant_test_expr |
| |
| from tests.coveragetest import CoverageTest |
| from tests.helpers import arcz_to_arcs |
| |
| |
| class PythonParserTestBase(CoverageTest): |
| """Tests for coverage.py's Python code parsing.""" |
| |
| run_in_temp_dir = False |
| |
| def parse_text(self, text: str, exclude: str = "nocover") -> PythonParser: |
| """Parse `text` as source, and return the `PythonParser` used.""" |
| text = textwrap.dedent(text) |
| parser = PythonParser(text=text, exclude=exclude) |
| parser.parse_source() |
| return parser |
| |
| |
| class PythonParserTest(PythonParserTestBase): |
| """Tests of coverage.parser.""" |
| |
| def test_exit_counts(self) -> None: |
| parser = self.parse_text("""\ |
| # 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_try_except(self) -> None: |
| parser = self.parse_text("""\ |
| 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: 1, |
| 4: 1, |
| 5: 1, |
| 6: 1, |
| 7: 1, |
| 8: 1, |
| 9: 1, |
| } |
| |
| def test_excluded_classes(self) -> None: |
| parser = self.parse_text("""\ |
| class Foo: |
| def __init__(self): |
| pass |
| |
| if len([]): # nocover |
| class Bar: |
| pass |
| """) |
| assert parser.exit_counts() == {2: 1, 3: 1} |
| |
| def test_missing_branch_to_excluded_code(self) -> None: |
| parser = self.parse_text("""\ |
| if fooey: |
| a = 2 |
| else: # nocover |
| a = 4 |
| b = 5 |
| """) |
| assert parser.exit_counts() == {1: 1, 2: 1, 5: 1} |
| parser = self.parse_text("""\ |
| 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_text("""\ |
| def foo(): |
| if fooey: |
| a = 3 |
| else: # nocover |
| a = 5 |
| b = 6 |
| """) |
| assert parser.exit_counts() == {1: 1, 2: 1, 3: 1, 6: 1} |
| |
| @pytest.mark.parametrize( |
| "text", |
| [ |
| pytest.param("0 spaces\n 2\n 1", id="bad_indent"), |
| pytest.param("'''", id="string_eof"), |
| pytest.param("$hello", id="dollar"), |
| # on 3.10 this passes ast.parse but fails on tokenize.generate_tokens |
| pytest.param( |
| "\r'\\\n'''", |
| id="leading_newline_eof", |
| marks=[ |
| pytest.mark.skipif(env.PYVERSION >= (3, 12), reason="parses fine in 3.12"), |
| ], |
| ), |
| ], |
| ) |
| def test_not_python(self, text: str) -> None: |
| msg = r"Couldn't parse '<code>' as Python source: ['\"].*['\"] at line \d+" |
| with pytest.raises(NotPython, match=msg): |
| _ = self.parse_text(text) |
| |
| def test_empty_decorated_function(self) -> None: |
| parser = self.parse_text("""\ |
| def decorator(func): |
| return func |
| |
| @decorator |
| def foo(self): |
| '''Docstring''' |
| |
| @decorator |
| def bar(self): |
| pass |
| """) |
| |
| assert parser.statements == {1, 2, 4, 5, 8, 9, 10} |
| assert parser.arcs() == set(arcz_to_arcs("14 45 58 89 9. 2. A-8")) |
| assert parser.exit_counts() == {1: 1, 2: 1, 4: 1, 5: 1, 8: 1, 9: 1, 10: 1} |
| |
| def test_nested_context_managers(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/1876 |
| parser = self.parse_text("""\ |
| a = 1 |
| with suppress(ValueError): |
| with suppress(ValueError): |
| x = 4 |
| with suppress(ValueError): |
| x = 6 |
| with suppress(ValueError): |
| x = 8 |
| a = 9 |
| """) |
| |
| one_nine = set(range(1, 10)) |
| assert parser.statements == one_nine |
| assert parser.exit_counts() == dict.fromkeys(one_nine, 1) |
| |
| def test_module_docstrings(self) -> None: |
| parser = self.parse_text("""\ |
| '''The docstring on line 1''' |
| a = 2 |
| """) |
| assert {2} == parser.statements |
| |
| parser = self.parse_text("""\ |
| # Docstring is not line 1 |
| '''The docstring on line 2''' |
| a = 3 |
| """) |
| assert {3} == parser.statements |
| |
| def test_fuzzed_double_parse(self) -> None: |
| # https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50381 |
| # The second parse used to raise `TypeError: 'NoneType' object is not iterable` |
| msg = ( |
| r"(EOF in multi-line statement)" # before 3.12.0b1 |
| + r"|(unmatched ']')" # after 3.12.0b1 |
| ) |
| with pytest.raises(NotPython, match=msg): |
| self.parse_text("]") |
| with pytest.raises(NotPython, match=msg): |
| self.parse_text("]") |
| |
| def test_bug_1891(self) -> None: |
| # These examples exercise code paths I thought were impossible. |
| parser = self.parse_text("""\ |
| res = siblings( |
| 'configure', |
| **ca, |
| ) |
| """) |
| assert parser.exit_counts() == {1: 1} |
| # In conftest.py, we silence the SyntaxWarning this code causes. If |
| # we remove this code, we can probably remove that warning. |
| parser = self.parse_text("""\ |
| def g2(): |
| try: |
| return 2 |
| finally: |
| return 3 |
| """) |
| assert parser.exit_counts() == {1: 1, 2: 1, 3: 1, 5: 1} |
| |
| def test_fuzzed_weirdness(self) -> None: |
| # This used to cause a `min of empty sequence` error: |
| self.parse_text("\r\\\n\n\t\t\\\n\n") |
| |
| |
| class ExclusionParserTest(PythonParserTestBase): |
| """Tests for the exclusion code in PythonParser.""" |
| |
| def test_simple(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 1; b = 2 |
| |
| if len([]): |
| a = 4 # nocover |
| """, |
| ) |
| assert parser.statements == {1, 3} |
| |
| def test_excluding_if_suite(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 1; b = 2 |
| |
| if len([]): # nocover |
| a = 4 |
| b = 5 |
| c = 6 |
| assert a == 1 and b == 2 |
| """, |
| ) |
| assert parser.statements == {1, 7} |
| |
| def test_excluding_if_but_not_else_suite(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 1; b = 2 |
| |
| if len([]): # nocover |
| a = 4 |
| b = 5 |
| c = 6 |
| else: |
| a = 8 |
| b = 9 |
| assert a == 8 and b == 9 |
| """, |
| ) |
| assert parser.statements == {1, 8, 9, 10} |
| |
| def test_excluding_else_suite(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 1; b = 2 |
| |
| if 1==1: |
| a = 4 |
| b = 5 |
| c = 6 |
| else: # nocover |
| a = 8 |
| b = 9 |
| assert a == 4 and b == 5 and c == 6 |
| """, |
| ) |
| assert parser.statements == {1, 3, 4, 5, 6, 10} |
| parser = self.parse_text( |
| """\ |
| a = 1; b = 2 |
| |
| if 1==1: |
| a = 4 |
| b = 5 |
| c = 6 |
| |
| # Lots of comments to confuse the else handler. |
| # more. |
| |
| else: # nocover |
| |
| # Comments here too. |
| |
| a = 8 |
| b = 9 |
| assert a == 4 and b == 5 and c == 6 |
| """, |
| ) |
| assert parser.statements == {1, 3, 4, 5, 6, 17} |
| |
| def test_excluding_oneline_if(self) -> None: |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| a = 2 |
| if len([]): x = 3 # nocover |
| b = 4 |
| |
| foo() |
| """, |
| ) |
| assert parser.statements == {1, 2, 4, 6} |
| |
| def test_excluding_a_colon_not_a_suite(self) -> None: |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| l = list(range(10)) |
| a = l[:3] # nocover |
| b = 4 |
| |
| foo() |
| """, |
| ) |
| assert parser.statements == {1, 2, 4, 6} |
| |
| def test_excluding_for_suite(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| for i in [1,2,3,4,5]: # nocover |
| a += i |
| assert a == 15 |
| """, |
| ) |
| assert parser.statements == {1, 4} |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| for i in [1, |
| 2,3,4, |
| 5]: # nocover |
| a += i |
| assert a == 15 |
| """, |
| ) |
| assert parser.statements == {1, 6} |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| for i in [1,2,3,4,5 |
| ]: # nocover |
| a += i |
| break |
| a = 99 |
| assert a == 1 |
| """, |
| ) |
| assert parser.statements == {1, 7} |
| |
| def test_excluding_for_else(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| for i in range(5): |
| a += i+1 |
| break |
| else: # nocover |
| a = 123 |
| assert a == 1 |
| """, |
| ) |
| assert parser.statements == {1, 2, 3, 4, 7} |
| |
| def test_excluding_while(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 3; b = 0 |
| while a*b: # nocover |
| b += 1 |
| break |
| assert a == 3 and b == 0 |
| """, |
| ) |
| assert parser.statements == {1, 5} |
| parser = self.parse_text( |
| """\ |
| a = 3; b = 0 |
| while ( |
| a*b |
| ): # nocover |
| b += 1 |
| break |
| assert a == 3 and b == 0 |
| """, |
| ) |
| assert parser.statements == {1, 7} |
| |
| def test_excluding_while_else(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 3; b = 0 |
| while a: |
| b += 1 |
| break |
| else: # nocover |
| b = 123 |
| assert a == 3 and b == 1 |
| """, |
| ) |
| assert parser.statements == {1, 2, 3, 4, 7} |
| |
| def test_excluding_try_except(self) -> None: |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| try: |
| a = 1 |
| except: # nocover |
| a = 99 |
| assert a == 1 |
| """, |
| ) |
| assert parser.statements == {1, 2, 3, 6} |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| try: |
| a = 1 |
| raise Exception("foo") |
| except: |
| a = 99 |
| assert a == 99 |
| """, |
| ) |
| assert parser.statements == {1, 2, 3, 4, 5, 6, 7} |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| try: |
| a = 1 |
| raise Exception("foo") |
| except ImportError: # nocover |
| a = 99 |
| except: |
| a = 123 |
| assert a == 123 |
| """, |
| ) |
| assert parser.statements == {1, 2, 3, 4, 7, 8, 9} |
| |
| def test_excluding_if_pass(self) -> None: |
| # From a comment on the coverage.py page by Michael McNeil Forbes: |
| parser = self.parse_text( |
| """\ |
| def f(): |
| if False: # pragma: nocover |
| pass # This line still reported as missing |
| if False: # pragma: nocover |
| x = 1 # Now it is skipped. |
| |
| f() |
| """, |
| ) |
| assert parser.statements == {1, 7} |
| |
| def test_multiline_if_no_branch(self) -> None: |
| # From https://github.com/nedbat/coveragepy/issues/754 |
| parser = self.parse_text( |
| """\ |
| if (this_is_a_verylong_boolean_expression == True # pragma: no branch |
| and another_long_expression and here_another_expression): |
| do_something() |
| """, |
| ) |
| parser2 = self.parse_text( |
| """\ |
| if this_is_a_verylong_boolean_expression == True and another_long_expression \\ |
| and here_another_expression: # pragma: no branch |
| do_something() |
| """, |
| ) |
| assert parser.statements == parser2.statements == {1, 3} |
| pragma_re = ".*pragma: no branch.*" |
| assert parser.lines_matching(pragma_re) == parser2.lines_matching(pragma_re) |
| |
| def test_excluding_function(self) -> None: |
| parser = self.parse_text( |
| """\ |
| def fn(foo): # nocover |
| a = 1 |
| b = 2 |
| c = 3 |
| |
| x = 1 |
| assert x == 1 |
| """, |
| ) |
| assert parser.statements == {6, 7} |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| def very_long_function_to_exclude_name(very_long_argument1, |
| very_long_argument2): |
| pass |
| assert a == 0 |
| """, |
| exclude="function_to_exclude", |
| ) |
| assert parser.statements == {1, 5} |
| parser = self.parse_text( |
| """\ |
| a = 0 |
| def very_long_function_to_exclude_name( |
| very_long_argument1, |
| very_long_argument2 |
| ): |
| pass |
| assert a == 0 |
| """, |
| exclude="function_to_exclude", |
| ) |
| assert parser.statements == {1, 7} |
| # linters and formatters can't agree about whether strings are too |
| # long, so format in the long variable names that make them too long. |
| long_arg = "super_long_input_argument" |
| parser = self.parse_text( |
| f"""\ |
| def my_func( |
| {long_arg}_0=0, |
| {long_arg}_1=1, |
| {long_arg}_2=2): |
| pass |
| |
| def my_func_2({long_arg}_0=0, {long_arg}_1=1, {long_arg}_2=2): |
| pass |
| """, |
| exclude="my_func", |
| ) |
| assert parser.statements == set() |
| parser = self.parse_text( |
| f"""\ |
| def my_func( |
| {long_arg}_0=0, |
| {long_arg}_1=1, |
| {long_arg}_2=2): |
| pass |
| |
| def my_func_2({long_arg}_0=0, {long_arg}_1=1, {long_arg}_2=2): |
| pass |
| """, |
| exclude="my_func_2", |
| ) |
| assert parser.statements == {1, 5} |
| parser = self.parse_text( |
| f"""\ |
| def my_func ( |
| {long_arg}_0=0, |
| {long_arg}_1=1, |
| {long_arg}_2=2): |
| pass |
| |
| def my_func_2 ({long_arg}_0=0, {long_arg}_1=1, {long_arg}_2=2): |
| pass |
| """, |
| exclude="my_func_2", |
| ) |
| assert parser.statements == {1, 5} |
| parser = self.parse_text( |
| f"""\ |
| def my_func ( |
| {long_arg}_0=0, |
| {long_arg}_1=1, |
| {long_arg}_2=2): |
| pass |
| |
| def my_func_2 ({long_arg}_0=0, {long_arg}_1=1, {long_arg}_2=2): |
| pass |
| """, |
| exclude="my_func", |
| ) |
| assert parser.statements == set() |
| parser = self.parse_text( |
| f"""\ |
| def my_func \ |
| ( |
| {long_arg}_0=0, |
| {long_arg}_1=1 |
| ): |
| pass |
| |
| def my_func_2({long_arg}_0=0, {long_arg}_1=1, {long_arg}_2=2): |
| pass |
| """, |
| exclude="my_func_2", |
| ) |
| assert parser.statements == {1, 5} |
| parser = self.parse_text( |
| f"""\ |
| def my_func \ |
| ( |
| {long_arg}_0=0, |
| {long_arg}_1=1 |
| ): |
| pass |
| |
| def my_func_2({long_arg}_0=0, {long_arg}_1=1, {long_arg}_2=2): |
| pass |
| """, |
| exclude="my_func", |
| ) |
| assert parser.statements == set() |
| |
| def test_excluding_bug1713(self) -> None: |
| parser = self.parse_text( |
| """\ |
| print("1") |
| |
| def hello_3(a): # pragma: nocover |
| match a: |
| case ("5" |
| | "6"): |
| print("7") |
| case "8": |
| print("9") |
| |
| print("11") |
| """, |
| ) |
| assert parser.statements == {1, 11} |
| parser = self.parse_text( |
| """\ |
| print("1") |
| |
| def hello_3(a): # nocover |
| if ("4" or |
| "5"): |
| print("6") |
| else: |
| print("8") |
| |
| print("10") |
| """, |
| ) |
| assert parser.statements == {1, 10} |
| parser = self.parse_text( |
| """\ |
| print(1) |
| |
| def func(a, b): |
| if a == 4: # nocover |
| func5() |
| if b: |
| print(7) |
| func8() |
| |
| print(10) |
| """, |
| ) |
| assert parser.statements == {1, 3, 10} |
| parser = self.parse_text( |
| """\ |
| class Foo: # pragma: nocover |
| def greet(self): |
| print("hello world") |
| """, |
| ) |
| assert parser.statements == set() |
| |
| def test_excluding_method(self) -> None: |
| parser = self.parse_text( |
| """\ |
| class Fooey: |
| def __init__(self): |
| self.a = 1 |
| |
| def foo(self): # nocover |
| return self.a |
| |
| x = Fooey() |
| assert x.a == 1 |
| """, |
| ) |
| assert parser.statements == {1, 2, 3, 8, 9} |
| parser = self.parse_text( |
| """\ |
| class Fooey: |
| def __init__(self): |
| self.a = 1 |
| |
| def very_long_method_to_exclude_name( |
| very_long_argument1, |
| very_long_argument2 |
| ): |
| pass |
| |
| x = Fooey() |
| assert x.a == 1 |
| """, |
| exclude="method_to_exclude", |
| ) |
| assert parser.statements == {1, 2, 3, 11, 12} |
| |
| def test_excluding_class(self) -> None: |
| parser = self.parse_text( |
| """\ |
| class Fooey: # nocover |
| def __init__(self): |
| self.a = 1 |
| |
| def foo(self): |
| return self.a |
| |
| x = 1 |
| assert x == 1 |
| """, |
| ) |
| assert parser.statements == {8, 9} |
| |
| def test_excludes_non_ascii(self) -> None: |
| parser = self.parse_text( |
| """\ |
| # coding: utf-8 |
| a = 1; b = 2 |
| |
| if len([]): |
| a = 5 # ✘cover |
| """, |
| exclude="✘cover", |
| ) |
| assert parser.statements == {2, 4} |
| |
| def test_no_exclude_at_all(self) -> None: |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| if fooey: |
| a = 3 |
| else: |
| a = 5 |
| b = 6 |
| """, |
| exclude="", |
| ) |
| assert parser.exit_counts() == {1: 1, 2: 2, 3: 1, 5: 1, 6: 1} |
| |
| def test_formfeed(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/461 |
| parser = self.parse_text( |
| """\ |
| x = 1 |
| assert len([]) == 0, ( |
| "This won't happen %s" % ("hello",) |
| ) |
| \f |
| x = 6 |
| assert len([]) == 0, ( |
| "This won't happen %s" % ("hello",) |
| ) |
| """, |
| exclude="assert", |
| ) |
| assert parser.statements == {1, 6} |
| |
| def test_decorator_pragmas(self) -> None: |
| parser = self.parse_text("""\ |
| # 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, 11, 13, 15, 16, 17, 19, 20, 22, 23, 25, 26} |
| assert parser.raw_statements == raw_statements |
| assert parser.statements == {8} |
| |
| def test_decorator_pragmas_with_colons(self) -> None: |
| # A colon in a decorator expression would confuse the parser, |
| # ending the exclusion of the decorated function. |
| parser = self.parse_text("""\ |
| @decorate(X) # nocover |
| @decorate("Hello"[2]) |
| def f(): |
| x = 4 |
| |
| @decorate(X) # nocover |
| @decorate("Hello"[:7]) |
| def g(): |
| x = 9 |
| """) |
| raw_statements = {1, 2, 3, 4, 6, 7, 8, 9} |
| assert parser.raw_statements == raw_statements |
| assert parser.statements == set() |
| |
| def test_class_decorator_pragmas(self) -> None: |
| parser = self.parse_text("""\ |
| 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_over_exclusion_bug1779(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/1779 |
| parser = self.parse_text("""\ |
| import abc |
| |
| class MyProtocol: # nocover 3 |
| @abc.abstractmethod # nocover 4 |
| def my_method(self) -> int: |
| ... # 6 |
| |
| def function() -> int: |
| return 9 |
| """) |
| assert parser.raw_statements == {1, 3, 4, 5, 6, 8, 9} |
| assert parser.statements == {1, 8, 9} |
| |
| def test_multiline_exclusion_single_line(self) -> None: |
| regex = r"print\('.*'\)" |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| print('Hello, world!') |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {2} |
| assert parser.raw_statements == {1, 2} |
| assert parser.statements == {1} |
| |
| def test_multiline_exclusion_suite(self) -> None: |
| # A multi-line exclusion that matches a colon line still excludes the entire block. |
| regex = r"if T:\n\s+print\('Hello, world!'\)" |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| if T: |
| print('Hello, world!') |
| print('This is a multiline regex test.') |
| a = 5 |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {2, 3} |
| assert parser.raw_statements == {1, 2, 3, 4, 5} |
| assert parser.statements == {1, 5} |
| |
| def test_multiline_exclusion_no_match(self) -> None: |
| regex = r"nonexistent" |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| print('Hello, world!') |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == set() |
| assert parser.raw_statements == {1, 2} |
| assert parser.statements == {1, 2} |
| |
| def test_multiline_exclusion_no_source(self) -> None: |
| regex = r"anything" |
| parser = PythonParser(text="", filename="dummy.py", exclude=regex) |
| assert parser.lines_matching(regex) == set() |
| assert parser.raw_statements == set() |
| assert parser.statements == set() |
| |
| def test_multiline_exclusion_all_lines_must_match(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/996 |
| regex = r"except ValueError:\n\s*print\('false'\)" |
| parser = self.parse_text( |
| """\ |
| try: |
| a = 2 |
| print('false') |
| except ValueError: |
| print('false') |
| except ValueError: |
| print('something else') |
| except IndexError: |
| print('false') |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {4, 5} |
| assert parser.raw_statements == {1, 2, 3, 4, 5, 6, 7, 8, 9} |
| assert parser.statements == {1, 2, 3, 6, 7, 8, 9} |
| |
| def test_multiline_exclusion_multiple_matches(self) -> None: |
| regex = r"print\('.*'\)\n\s+. = \d" |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| print('Hello, world!') |
| a = 5 |
| def bar(): |
| print('Hello again!') |
| b = 6 |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {2, 3, 5, 6} |
| assert parser.raw_statements == {1, 2, 3, 4, 5, 6} |
| assert parser.statements == {1, 4} |
| |
| def test_multiline_exclusion_suite2(self) -> None: |
| regex = r"print\('Hello, world!'\)\n\s+if T:" |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| print('Hello, world!') |
| if T: |
| print('This is a test.') |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {2, 3} |
| assert parser.raw_statements == {1, 2, 3, 4} |
| assert parser.statements == {1} |
| |
| def test_multiline_exclusion_match_all(self) -> None: |
| regex = ( |
| r"def foo\(\):\n\s+print\('Hello, world!'\)\n" |
| + r"\s+if T:\n\s+print\('This is a test\.'\)" |
| ) |
| parser = self.parse_text( |
| """\ |
| def foo(): |
| print('Hello, world!') |
| if T: |
| print('This is a test.') |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {1, 2, 3, 4} |
| assert parser.raw_statements == {1, 2, 3, 4} |
| assert parser.statements == set() |
| |
| def test_multiline_exclusion_block(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/1803 |
| regex = "# no cover: start(?s:.)*?# no cover: stop" |
| parser = self.parse_text( |
| """\ |
| a = my_function1() |
| if debug: |
| msg = "blah blah" |
| # no cover: start |
| log_message(msg, a) |
| b = my_function2() |
| # no cover: stop |
| count_this() |
| # no cover: start |
| but_not_this() |
| # no cover: stop |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {4, 5, 6, 7, 9, 10, 11} |
| assert parser.raw_statements == {1, 2, 3, 5, 6, 8, 10} |
| assert parser.statements == {1, 2, 3, 8} |
| |
| def test_multiline_exclusion_block2(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/1797 |
| regex = r"case _:\n\s+assert_never\(" |
| parser = self.parse_text( |
| """\ |
| match something: |
| case type_1(): |
| logic_1() |
| case type_2(): |
| logic_2() |
| case _: |
| assert_never(something) |
| match something: |
| case type_1(): |
| logic_1() |
| case type_2(): |
| logic_2() |
| case _: |
| print("Default case") |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {6, 7} |
| assert parser.raw_statements == {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14} |
| assert parser.statements == {1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14} |
| |
| def test_multiline_exclusion_block3(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/1741 |
| # This will only work if there's exactly one return statement in the rest of the function |
| regex = r"# no cover: to return(?s:.)*?return" |
| parser = self.parse_text( |
| """\ |
| def my_function(args, j): |
| if args.command == Commands.CMD.value: |
| return cmd_handler(j, args) |
| # no cover: to return |
| print(f"Command '{args.command}' was not handled.", file=sys.stderr) |
| parser.print_help(file=sys.stderr) |
| |
| return os.EX_USAGE |
| print("not excluded") |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {4, 5, 6, 7, 8} |
| assert parser.raw_statements == {1, 2, 3, 5, 6, 8, 9} |
| assert parser.statements == {1, 2, 3, 9} |
| |
| def test_multiline_exclusion_whole_source(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/118 |
| regex = r"\A(?s:.*# pragma: exclude file.*)\Z" |
| parser = self.parse_text( |
| """\ |
| import coverage |
| # pragma: exclude file |
| def the_void() -> None: |
| if "py" not in __file__: |
| print("Not a Python file.") |
| print("Everything here is excluded.") |
| |
| return |
| print("Excluded too") |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} |
| assert parser.raw_statements == {1, 3, 4, 5, 6, 8, 9} |
| assert parser.statements == set() |
| |
| def test_multiline_exclusion_from_marker(self) -> None: |
| # https://github.com/nedbat/coveragepy/issues/118 |
| regex = r"# pragma: rest of file(?s:.)*\Z" |
| parser = self.parse_text( |
| """\ |
| import coverage |
| # pragma: rest of file |
| def the_void() -> None: |
| if "py" not in __file__: |
| print("Not a Python file.") |
| print("Everything here is excluded.") |
| |
| return |
| print("Excluded too") |
| """, |
| regex, |
| ) |
| assert parser.lines_matching(regex) == {2, 3, 4, 5, 6, 7, 8, 9, 10} |
| assert parser.raw_statements == {1, 3, 4, 5, 6, 8, 9} |
| assert parser.statements == {1} |
| |
| |
| class ParserMissingArcDescriptionTest(PythonParserTestBase): |
| """Tests for PythonParser.missing_arc_description.""" |
| |
| def test_missing_arc_description(self) -> None: |
| # This code is never run, so the actual values don't matter. |
| parser = self.parse_text("""\ |
| 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 always true" |
| 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 always true" |
| ) |
| assert expected == parser.missing_arc_description(11, 13) |
| |
| def test_missing_arc_descriptions_for_exceptions(self) -> None: |
| parser = self.parse_text("""\ |
| 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) |
| |
| |
| class MatchCaseMissingArcDescriptionTest(PythonParserTestBase): |
| """Missing arc descriptions for match/case.""" |
| |
| def test_match_case(self) -> None: |
| parser = self.parse_text("""\ |
| match command.split(): |
| case ["go", direction] if direction in "nesw": # 2 |
| match = f"go: {direction}" |
| case ["go", _]: # 4 |
| match = "no go" |
| print(match) # 6 |
| """) |
| assert parser.missing_arc_description(2, 3) == ( |
| "line 2 didn't jump to line 3 because the pattern on line 2 never matched" |
| ) |
| assert parser.missing_arc_description(2, 4) == ( |
| "line 2 didn't jump to line 4 because the pattern on line 2 always matched" |
| ) |
| assert parser.missing_arc_description(4, 6) == ( |
| "line 4 didn't jump to line 6 because the pattern on line 4 always matched" |
| ) |
| |
| def test_final_wildcard(self) -> None: |
| parser = self.parse_text("""\ |
| match command.split(): |
| case ["go", direction] if direction in "nesw": # 2 |
| match = f"go: {direction}" |
| case _: # 4 |
| match = "no go" |
| print(match) # 6 |
| """) |
| assert parser.missing_arc_description(2, 3) == ( |
| "line 2 didn't jump to line 3 because the pattern on line 2 never matched" |
| ) |
| assert parser.missing_arc_description(2, 4) == ( |
| "line 2 didn't jump to line 4 because the pattern on line 2 always matched" |
| ) |
| # 4-6 isn't a possible arc, so the description is generic. |
| assert parser.missing_arc_description(4, 6) == "line 4 didn't jump to line 6" |
| |
| def test_missing_arc_descriptions_bug1775(self) -> None: |
| # Bug: the `if x == 2:` line was marked partial with a message of: |
| # line 6 didn't return from function 'func', because the return on line 4 wasn't executed |
| # At some point it changed to "didn't jump to the function exit," which |
| # is close enough. These situations are hard to describe precisely. |
| parser = self.parse_text("""\ |
| def func(): |
| x = 2 |
| try: |
| return 4 |
| finally: |
| if x == 2: # line 6 |
| print("x is 2") |
| |
| func() |
| """) |
| assert parser.missing_arc_description(6, -1) == "line 6 didn't jump to the function exit" |
| |
| |
| class ParserFileTest(CoverageTest): |
| """Tests for coverage.py's code parsing from files.""" |
| |
| def parse_file(self, filename: str) -> PythonParser: |
| """Parse `text` as source, and return the `PythonParser` used.""" |
| parser = PythonParser(filename=filename, exclude="nocover") |
| parser.parse_source() |
| return parser |
| |
| @pytest.mark.parametrize( |
| "slug, newline", |
| [ |
| ("unix", "\n"), |
| ("dos", "\r\n"), |
| ("mac", "\r"), |
| ], |
| ) |
| def test_line_endings(self, slug: str, newline: str) -> None: |
| 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} |
| fname = slug + ".py" |
| self.make_file(fname, text, newline=newline) |
| parser = self.parse_file(fname) |
| assert parser.exit_counts() == counts, f"Wrong for {fname!r}" |
| |
| def test_encoding(self) -> None: |
| 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) -> None: |
| # 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 = some_module.some_function( |
| ["my data", "-c", "pass"], |
| arg1=some_module.NAME, |
| arg2=some_module.OTHER_NAME).function() |
| """, |
| ) |
| |
| parser = self.parse_file("normal.py") |
| assert parser.statements == {1} |
| |
| self.make_file( |
| "abrupt.py", |
| """\ |
| out, err = some_module.some_function( |
| ["my data", "-c", "pass"], |
| arg1=some_module.NAME, |
| arg2=some_module.OTHER_NAME).function()""", |
| ) # no final newline. |
| |
| # Double-check that some test helper wasn't being helpful. |
| with open("abrupt.py", encoding="utf-8") as f: |
| assert f.read()[-1] == ")" |
| |
| parser = self.parse_file("abrupt.py") |
| assert parser.statements == {1} |
| |
| def test_os_error(self) -> None: |
| self.make_file("cant-read.py", "BOOM!") |
| msg = "No source for code: 'cant-read.py': Fake!" |
| with pytest.raises(NoSource, match=re.escape(msg)): |
| with mock.patch("coverage.python.read_python_source", side_effect=OSError("Fake!")): |
| PythonParser(filename="cant-read.py") |
| |
| |
| @pytest.mark.parametrize( |
| ["expr", "ret"], |
| [ |
| ("True", (True, True)), |
| ("False", (True, False)), |
| ("1", (True, True)), |
| ("0", (True, False)), |
| ("__debug__", (True, True)), |
| ("not __debug__", (True, False)), |
| ("not(__debug__)", (True, False)), |
| ("-__debug__", (False, False)), |
| ("__debug__ or True", (True, True)), |
| ("__debug__ + True", (False, False)), |
| ("x", (False, False)), |
| ("__debug__ or debug", (False, False)), |
| ], |
| ) |
| def test_is_constant_test_expr(expr: str, ret: tuple[bool, bool]) -> None: |
| node = ast.parse(expr, mode="eval").body |
| print(ast.dump(node, indent=4)) |
| assert is_constant_test_expr(node) == ret |