Coverage for tests/test_valid_source.py: 25%
103 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
3from pathlib import Path
4from unittest.mock import patch
6import pytest
7from pysource_minimize import minimize
9from .test_invalid_ast import does_compile
10from pysource_codegen._codegen import generate_ast
11from pysource_codegen._codegen import is_valid_ast
12from pysource_codegen._codegen import unparse
14sample_dir = Path(__file__).parent / "valid_source_samples"
15sample_dir.mkdir(exist_ok=True)
18@pytest.mark.parametrize(
19 "file", [pytest.param(f, id=f.stem[:12]) for f in sample_dir.glob("*.py")]
20)
21def test_valid_source(file):
22 code = file.read_text()
24 try:
25 tree = ast.parse(code)
26 except:
27 return
29 if not does_compile(tree): 29 ↛ 30line 29 didn't jump to line 30, because the condition on line 29 was never true
30 return
32 print("the following code is valid but can not be generated by codegen:\n")
33 print(code)
34 print()
36 assert is_valid_ast(tree)
39def minimize_if_valid(code):
40 def bug_found(code):
41 try:
42 tree = ast.parse(code)
43 except:
44 return False
46 if not does_compile(tree):
47 return False
49 return not is_valid_ast(tree)
51 if bug_found(code):
52 code = minimize(code, bug_found)
53 print()
54 print("minimized:")
55 print(code)
56 name = sample_dir / f"{hashlib.sha256(code.encode('utf-8')).hexdigest()}.py"
57 name.write_text(code)
58 return True
59 else:
60 return False
63def generate_valid_source(seed):
64 """
65 try to find source code which is valid but can not be generated by codegen()
67 The use() hook controls if a specific restriction in the code generation should be applied
69 """
70 print("seed=", seed)
72 ignore_index = 0
73 ignored_something = True
74 max_index = 0
76 while ignored_something:
77 ignored_something = False
79 current_index = 0
81 def use():
82 nonlocal current_index
83 nonlocal ignored_something
85 ignore = current_index == ignore_index
86 current_index += 1
87 if ignore:
88 ignored_something = True
89 return not ignore
91 with patch("pysource_codegen._codegen.use", use):
92 tree = generate_ast(seed, node_limit=200, depth_limit=5)
94 max_index = max(max_index, current_index)
96 try:
97 code = unparse(tree)
98 except Exception as e:
99 print(repr(e))
100 continue
102 print(f"{seed} {ignore_index} of {max_index} ignored? {ignored_something}")
104 if minimize_if_valid(code):
105 return True
107 ignore_index += 1
109 return False
112if __name__ == "__main__": 112 ↛ 113line 112 didn't jump to line 113, because the condition on line 112 was never true
113 import json
114 import argparse
116 parser = argparse.ArgumentParser(
117 description="Find valid code which can not be generated by pysource-codegen"
118 )
119 parser.add_argument("folder", help="Folder to process")
120 parser.add_argument("--skip-first", action="store_true", help="Skip the first file")
121 args = parser.parse_args()
123 db = Path("checked_valid_source_files")
125 if db.exists():
126 checked = set(json.loads(db.read_text()))
127 else:
128 checked = set()
130 globbed_files = set(map(str, Path(args.folder).rglob("*.py")))
132 all_files = globbed_files - checked
134 if not all_files:
135 all_files = globbed_files
136 checked = set()
138 is_first = True
140 for file in map(Path, sorted(all_files)):
141 print(file)
142 try:
143 code = file.read_text("utf-8")
144 compile(code, str(file), "exec")
145 except:
146 print("skip")
147 continue
148 if is_first and args.skip_first:
149 checked.add(str(file))
150 is_first = False
151 continue
152 if minimize_if_valid(code):
153 break
155 checked.add(str(file))
157 db.write_text(json.dumps(sorted(checked)))