summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2021-10-12 07:21:45 -0400
committerNed Batchelder <ned@nedbatchelder.com>2021-10-12 07:24:37 -0400
commit5b6b6ecb87f4aa1145977b1a4c8359b202da0d7a (patch)
tree0838a3f5686a94122d952ca7fcdcb87c00741427
parentce0e38e131e987c1bba1dd51abb2d4a002ac3c4e (diff)
downloadpython-coveragepy-git-5b6b6ecb87f4aa1145977b1a4c8359b202da0d7a.tar.gz
test: lightly test the ast_dump function
-rw-r--r--coverage/parser.py133
-rw-r--r--tests/test_parser.py26
2 files changed, 90 insertions, 69 deletions
diff --git a/coverage/parser.py b/coverage/parser.py
index 3be822d5..80665b5a 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -604,10 +604,6 @@ class ArcStart(collections.namedtuple("Arc", "lineno, cause")):
new_contract('ArcStarts', lambda seq: all(isinstance(x, ArcStart) for x in seq))
-# Turn on AST dumps with an environment variable.
-# $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code.
-AST_DUMP = bool(int(os.environ.get("COVERAGE_AST_DUMP", 0)))
-
class NodeList:
"""A synthetic fictitious node, containing a sequence of nodes.
@@ -619,22 +615,30 @@ class NodeList:
self.body = body
self.lineno = body[0].lineno
-
# TODO: some add_arcs methods here don't add arcs, they return them. Rename them.
# TODO: the cause messages have too many commas.
# TODO: Shouldn't the cause messages join with "and" instead of "or"?
+def ast_parse(text):
+ """How we create an AST parse."""
+ return ast.parse(neuter_encoding_declaration(text))
+
+
class AstArcAnalyzer:
"""Analyze source text with an AST to find executable code paths."""
@contract(text='unicode', statements=set)
def __init__(self, text, statements, multiline):
- self.root_node = ast.parse(neuter_encoding_declaration(text))
+ self.root_node = ast_parse(text)
# TODO: I think this is happening in too many places.
self.statements = {multiline.get(l, l) for l in statements}
self.multiline = multiline
- if AST_DUMP: # pragma: debugging
+ # Turn on AST dumps with an environment variable.
+ # $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code.
+ dump_ast = bool(int(os.environ.get("COVERAGE_AST_DUMP", 0)))
+
+ if dump_ast: # pragma: debugging
# Dump the AST so that failing tests have helpful output.
print(f"Statements: {self.statements}")
print(f"Multiline map: {self.multiline}")
@@ -1294,69 +1298,64 @@ class AstArcAnalyzer:
_code_object__ListComp = _make_expression_code_method("list comprehension")
-if AST_DUMP: # pragma: debugging
- # Code only used when dumping the AST for debugging.
+# Code only used when dumping the AST for debugging.
- SKIP_DUMP_FIELDS = ["ctx"]
+SKIP_DUMP_FIELDS = ["ctx"]
- def _is_simple_value(value):
- """Is `value` simple enough to be displayed on a single line?"""
- return (
- value in [None, [], (), {}, set()] or
- isinstance(value, (str, int, float))
- )
+def _is_simple_value(value):
+ """Is `value` simple enough to be displayed on a single line?"""
+ return (
+ value in [None, [], (), {}, set()] or
+ isinstance(value, (str, int, float))
+ )
- def ast_dump(node, depth=0):
- """Dump the AST for `node`.
+def ast_dump(node, depth=0, print=print): # pylint: disable=redefined-builtin
+ """Dump the AST for `node`.
- This recursively walks the AST, printing a readable version.
+ This recursively walks the AST, printing a readable version.
- """
- indent = " " * depth
- if not isinstance(node, ast.AST):
- print(f"{indent}<{node.__class__.__name__} {node!r}>")
- return
-
- lineno = getattr(node, "lineno", None)
- if lineno is not None:
- linemark = f" @ {node.lineno},{node.col_offset}"
- if hasattr(node, "end_lineno"):
- linemark += ":"
- if node.end_lineno != node.lineno:
- linemark += f"{node.end_lineno},"
- linemark += f"{node.end_col_offset}"
- else:
- linemark = ""
- head = f"{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(f"{head}>")
- elif len(named_fields) == 1 and _is_simple_value(named_fields[0][1]):
- field_name, value = named_fields[0]
- print(f"{head} {field_name}: {value!r}>")
- else:
- print(head)
- if 0:
- print("{}# mro: {}".format(
- indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]),
- ))
- next_indent = indent + " "
- for field_name, value in named_fields:
- prefix = f"{next_indent}{field_name}:"
- if _is_simple_value(value):
- print(f"{prefix} {value!r}")
- elif isinstance(value, list):
- print(f"{prefix} [")
- for n in value:
- ast_dump(n, depth + 8)
- print(f"{next_indent}]")
- else:
- print(prefix)
- ast_dump(value, depth + 8)
+ """
+ indent = " " * depth
+ lineno = getattr(node, "lineno", None)
+ if lineno is not None:
+ linemark = f" @ {node.lineno},{node.col_offset}"
+ if hasattr(node, "end_lineno"):
+ linemark += ":"
+ if node.end_lineno != node.lineno:
+ linemark += f"{node.end_lineno},"
+ linemark += f"{node.end_col_offset}"
+ else:
+ linemark = ""
+ head = f"{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(f"{head}>")
+ elif len(named_fields) == 1 and _is_simple_value(named_fields[0][1]):
+ field_name, value = named_fields[0]
+ print(f"{head} {field_name}: {value!r}>")
+ else:
+ print(head)
+ if 0:
+ print("{}# mro: {}".format(
+ indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]),
+ ))
+ next_indent = indent + " "
+ for field_name, value in named_fields:
+ prefix = f"{next_indent}{field_name}:"
+ if _is_simple_value(value):
+ print(f"{prefix} {value!r}")
+ elif isinstance(value, list):
+ print(f"{prefix} [")
+ for n in value:
+ ast_dump(n, depth + 8, print=print)
+ print(f"{next_indent}]")
+ else:
+ print(prefix)
+ ast_dump(value, depth + 8, print=print)
- print(f"{indent}>")
+ print(f"{indent}>")
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 82bf7616..ff7d9ef9 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -3,15 +3,17 @@
"""Tests for coverage.py's code parsing."""
+import os.path
+import re
import textwrap
import pytest
from coverage import env
from coverage.exceptions import NotPython
-from coverage.parser import PythonParser
+from coverage.parser import ast_dump, ast_parse, PythonParser
-from tests.coveragetest import CoverageTest
+from tests.coveragetest import CoverageTest, TESTS_DIR
from tests.helpers import arcz_to_arcs
@@ -477,3 +479,23 @@ class ParserFileTest(CoverageTest):
parser = self.parse_file("abrupt.py")
assert parser.statements == {1}
+
+
+def test_ast_dump():
+ # Run the AST_DUMP code to make sure it doesn't fail, with some light
+ # assertions. Use parser.py as the test code since it is the longest file,
+ # and fitting, since it's the AST_DUMP code.
+ parser_py = os.path.join(TESTS_DIR, "../coverage/parser.py")
+ with open(parser_py) as f:
+ ast_root = ast_parse(f.read())
+ result = []
+ ast_dump(ast_root, print=result.append)
+ assert len(result) > 10000
+ assert result[0] == "<Module"
+ assert result[-1] == ">"
+
+ def count(pat):
+ return sum(1 for line in result if re.search(pat, line))
+
+ assert count(r"^\s+>") > 2000
+ assert count(r"<Name @ \d+,\d+(:\d+)? id: '\w+'>") > 1000