Skip to content

Commit f6fc33e

Browse files
committed
test: added script which finds new formatting bugs with pysource-codegen
1 parent dbb14ea commit f6fc33e

File tree

3 files changed

+203
-0
lines changed

3 files changed

+203
-0
lines changed

.pre-commit-config.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ repos:
5858
- types-commonmark
5959
- urllib3
6060
- hypothesmith
61+
- pysource-codegen
62+
- pysource-minimize
6163
- id: mypy
6264
name: mypy (Python 3.10)
6365
files: scripts/generate_schema.py

scripts/find_issue.py

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import random
2+
import re
3+
import subprocess
4+
import sys
5+
import tempfile
6+
import time
7+
from dataclasses import dataclass
8+
from pathlib import Path
9+
from typing import Optional, cast
10+
11+
from pysource_codegen import generate
12+
from pysource_minimize import minimize
13+
14+
import black
15+
16+
base_path = Path(__file__).parent
17+
18+
19+
@dataclass()
20+
class Issue:
21+
src: str
22+
mode: black.FileMode
23+
24+
25+
def bug_in_code(issue: Issue) -> bool:
26+
try:
27+
dst_contents = black.format_str(issue.src, mode=issue.mode)
28+
29+
black.assert_equivalent(issue.src, dst_contents)
30+
black.assert_stable(issue.src, dst_contents, mode=issue.mode)
31+
except Exception:
32+
return True
33+
return False
34+
35+
36+
def current_target_version() -> black.TargetVersion:
37+
v = sys.version_info
38+
return cast(black.TargetVersion, getattr(black.TargetVersion, f"PY{v[0]}{v[1]}"))
39+
40+
41+
def find_issue() -> Optional[Issue]:
42+
t = time.time()
43+
print("search for new issue ", end="", flush=True)
44+
45+
while time.time() - t < 60 * 10:
46+
for line_length in (100, 1):
47+
for magic_trailing_comma in (True, False):
48+
print(".", end="", flush=True)
49+
mode = black.FileMode(
50+
line_length=line_length,
51+
string_normalization=True,
52+
is_pyi=False,
53+
magic_trailing_comma=magic_trailing_comma,
54+
target_versions={current_target_version()},
55+
)
56+
seed = random.randint(0, 100000000)
57+
58+
src_code = generate(seed)
59+
60+
issue = Issue(src_code, mode)
61+
62+
if bug_in_code(issue):
63+
print(f"found bug (seed={seed})")
64+
return issue
65+
print("no new issue found in 10 minutes")
66+
return None
67+
68+
69+
def minimize_code(issue: Issue) -> Issue:
70+
minimized = Issue(
71+
minimize(issue.src, lambda code: bug_in_code(Issue(code, issue.mode))),
72+
issue.mode,
73+
)
74+
assert bug_in_code(minimized)
75+
76+
print("minimized code:")
77+
print(minimized.src)
78+
79+
return minimized
80+
81+
82+
def mode_to_options(mode: black.FileMode) -> list[str]:
83+
result = ["-l", str(mode.line_length)]
84+
if not mode.magic_trailing_comma:
85+
result.append("-C")
86+
(v,) = list(mode.target_versions)
87+
result += ["-t", v.name.lower()]
88+
return result
89+
90+
91+
def create_issue(issue: Issue) -> str:
92+
93+
dir = tempfile.TemporaryDirectory()
94+
95+
cwd = Path(dir.name)
96+
(cwd / "bug.py").write_text(issue.src)
97+
98+
multiline_code = "\n".join([" " + repr(s + "\n") for s in issue.src.split("\n")])
99+
100+
parse_code = f"""\
101+
from ast import parse
102+
parse(
103+
{multiline_code}
104+
)
105+
"""
106+
cwd = Path(dir.name)
107+
(cwd / "parse_code.py").write_text(parse_code)
108+
command = ["black", *mode_to_options(issue.mode), "bug.py"]
109+
110+
format_result = subprocess.run(
111+
[sys.executable, "-m", *command], capture_output=True, cwd=cwd
112+
)
113+
114+
error_output = format_result.stderr.decode()
115+
116+
m = re.search("This diff might be helpful: (/.*)", error_output)
117+
reported_diff = ""
118+
if m:
119+
path = Path(m[1])
120+
reported_diff = f"""
121+
the reported diff in {path} is:
122+
``` diff
123+
{path.read_text()}
124+
```
125+
"""
126+
127+
run_result = subprocess.run(
128+
[sys.executable, "parse_code.py"], capture_output=True, cwd=cwd
129+
)
130+
131+
git_ref = subprocess.run(
132+
["git", "rev-parse", "origin/main"], capture_output=True
133+
).stdout
134+
135+
return f"""
136+
**Describe the bug**
137+
138+
The following code can not be parsed/formatted by black:
139+
140+
``` python
141+
{issue.src}
142+
```
143+
144+
black reported the following error:
145+
```
146+
> {" ".join(command)}
147+
{format_result.stderr.decode()}
148+
```
149+
{reported_diff}
150+
151+
but it can be parsed by cpython:
152+
``` python
153+
{parse_code}
154+
```
155+
result:
156+
```
157+
{run_result.stderr.decode()}
158+
returncode: {run_result.returncode}
159+
```
160+
161+
162+
**Environment**
163+
164+
<!-- Please complete the following information: -->
165+
166+
- Black's version: current main ({git_ref.decode().strip()})
167+
- OS and Python version: Linux/Python {sys.version}
168+
169+
**Additional context**
170+
171+
The bug was found by pysource-codegen (see #3908)
172+
173+
"""
174+
175+
176+
def main() -> None:
177+
issue = find_issue()
178+
if issue is None:
179+
return
180+
181+
issue = minimize_code(issue)
182+
183+
while issue.mode.line_length > 1 and bug_in_code(issue):
184+
issue.mode.line_length -= 1
185+
issue.mode.line_length += 1
186+
187+
issue = minimize_code(issue)
188+
189+
while issue.mode.line_length <= 100 and bug_in_code(issue):
190+
issue.mode.line_length += 1
191+
issue.mode.line_length -= 1
192+
193+
issue = minimize_code(issue)
194+
195+
print(create_issue(issue))
196+
197+
198+
if __name__ == "__main__":
199+
main()

test_requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ pytest >= 6.1.1
44
pytest-xdist >= 3.0.2
55
pytest-cov >= 4.1.0
66
tox
7+
pysource_codegen >= 0.4.1
8+
pysource_minimize >= 0.4.0

0 commit comments

Comments
 (0)