Coverage for tests/test_invalid_ast.py: 56%
92 statements
« prev ^ index » next coverage.py v7.5.3, created at 2024-06-13 21:17 +0200
« prev ^ index » next coverage.py v7.5.3, created at 2024-06-13 21:17 +0200
1import ast
2import hashlib
3import sys
4import textwrap
5import warnings
6from pathlib import Path
8import pytest
9from pysource_minimize._minimize import minimize_ast
11from pysource_codegen._codegen import generate_ast
12from pysource_codegen._codegen import is_valid_ast
13from pysource_codegen._codegen import unparse
14from pysource_codegen._utils import ast_dump
16sample_dir = Path(__file__).parent / "invalid_ast_samples"
17sample_dir.mkdir(exist_ok=True)
20def does_compile(tree: ast.Module):
21 for node in ast.walk(tree):
22 if isinstance(node, ast.BoolOp) and len(node.values) < 2:
23 return False
24 if not isinstance(node, ast.JoinedStr) and any(
25 isinstance(n, ast.FormattedValue) for n in ast.iter_child_nodes(node)
26 ):
27 return False
28 try:
29 with warnings.catch_warnings():
30 warnings.simplefilter("ignore", SyntaxWarning)
31 source = unparse(tree)
32 compile(source, "<file>", "exec")
33 compile(ast.fix_missing_locations(tree), "<file>", "exec")
34 except Exception as e:
35 print(e)
36 return False
37 return True
40@pytest.mark.parametrize(
41 "file", [pytest.param(f, id=f.stem[:12]) for f in sample_dir.glob("*.py")]
42)
43def test_invalid_ast(file):
44 code = file.read_text()
45 print(code)
46 globals = {}
47 try:
48 exec(code, globals)
49 except (NameError, ImportError) as e:
50 pytest.skip(f"wrong python version {e}")
52 tree = globals["tree"]
54 for node in ast.walk(tree):
55 for field in node._fields:
56 if not hasattr(node, field):
57 pytest.skip(
58 f"wrong python version {node.__class__.__name__} is missing .{field}"
59 )
60 if sys.version_info < (3, 8) and isinstance(node, ast.Constant): 60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true
61 pytest.skip(f"ast.Constant can not be unparsed on python3.7")
63 assert is_valid_ast(tree) == does_compile(tree)
66def x_test_example():
67 seed = 2273381
68 tree = generate_ast(seed)
69 tree = generate_ast(seed, depth_limit=9)
70 # print(ast.dump(tree, indent=2))
71 assert is_valid_ast(tree)
74def generate_invalid_ast(seed):
75 print("seed =", seed)
77 tree = generate_ast(seed, depth_limit=9)
78 try:
79 assert is_valid_ast(tree)
80 except:
81 print(f"error for is_valid_ast seed={seed}")
82 raise
84 if not does_compile(tree):
85 last_checked_tree = tree
87 def checker(tree):
88 nonlocal last_checked_tree
90 bug_found = not does_compile(tree) and is_valid_ast(tree)
91 if bug_found:
92 last_checked_tree = tree
94 return bug_found
96 try:
97 new_tree = minimize_ast(tree, checker)
98 except:
99 print(f"error happend while minimize_ast seed={seed}")
100 print(ast_dump(last_checked_tree))
101 raise
103 print(
104 "pysource-codegen thinks that the current ast produces valid python code, but this is not the case:"
105 )
106 info = "from ast import *\n"
107 info += f"tree = {ast_dump(new_tree)}\n"
108 source = ""
109 try:
110 source = unparse(new_tree)
111 compile(source, "<file>", "exec")
112 compile(ast.fix_missing_locations(tree), "<file>", "exec")
113 except Exception as e:
114 comment = f"version: {sys.version.split()[0]}\nseed = {seed}\n\n"
115 if source:
116 comment += f"Source:\n{source}\n\n"
117 comment += f"\nError:\n {e!r}"
119 info += "\n" + textwrap.indent(comment, "# ", lambda l: True)
121 print(info)
122 name = sample_dir / f"{hashlib.sha256(info.encode('utf-8')).hexdigest()}.py"
123 name.write_text(info)
124 return True
125 else:
126 assert False
127 return False