Skip to content

Commit fdee6c2

Browse files
committed
test: added script which finds new formatting bugs with pysource-codegen
1 parent 950ec38 commit fdee6c2

File tree

3 files changed

+241
-0
lines changed

3 files changed

+241
-0
lines changed

Diff for: .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

Diff for: scripts/find_issue.py

+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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"\nfound bug (seed={seed})")
64+
return issue
65+
print("\nno 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_link(issue: Issue) -> str:
92+
import base64
93+
import json
94+
import lzma
95+
96+
data = {
97+
"sc": issue.src,
98+
"ll": issue.mode.line_length,
99+
"ssfl": issue.mode.skip_source_first_line,
100+
"ssn": not issue.mode.string_normalization,
101+
"smtc": not issue.mode.magic_trailing_comma,
102+
"pyi": issue.mode.is_pyi,
103+
"fast": False,
104+
"prv": issue.mode.preview,
105+
"usb": issue.mode.unstable,
106+
"tv": [v.name.lower() for v in issue.mode.target_versions],
107+
}
108+
109+
compressed = lzma.compress(json.dumps(data).encode("utf-8"))
110+
state = base64.urlsafe_b64encode(compressed).decode("utf-8")
111+
112+
return f"https://black.vercel.app/?version=main&state={state}"
113+
114+
115+
def create_issue(issue: Issue) -> str:
116+
117+
dir = tempfile.TemporaryDirectory()
118+
119+
cwd = Path(dir.name)
120+
bug_file = cwd / "bug.py"
121+
bug_file.write_text(issue.src)
122+
123+
multiline_code = "\n".join([" " + repr(s + "\n") for s in issue.src.split("\n")])
124+
125+
parse_code = f"""\
126+
from ast import parse
127+
parse(
128+
{multiline_code}
129+
)
130+
"""
131+
cwd = Path(dir.name)
132+
(cwd / "parse_code.py").write_text(parse_code)
133+
command = ["black", *mode_to_options(issue.mode), "bug.py"]
134+
135+
format_result = subprocess.run(
136+
[sys.executable, "-m", *command], capture_output=True, cwd=cwd
137+
)
138+
139+
fast_command = [*command, "--fast"]
140+
format_fast_result = subprocess.run(
141+
[sys.executable, "-m", *fast_command], capture_output=True, cwd=cwd
142+
)
143+
144+
fast_formatted = ""
145+
if format_fast_result.returncode == 0:
146+
fast_formatted = f"""
147+
The code can be formatted with `{" ".join(fast_command)}`:
148+
``` python
149+
{bug_file.read_text().rstrip()}
150+
```"""
151+
152+
error_output = format_result.stderr.decode()
153+
154+
m = re.search("This diff might be helpful: (/.*)", error_output)
155+
reported_diff = ""
156+
if m:
157+
path = Path(m[1])
158+
reported_diff = f"""
159+
the reported diff in {path} is:
160+
``` diff
161+
{path.read_text().rstrip()}
162+
```"""
163+
164+
run_result = subprocess.run(
165+
[sys.executable, "parse_code.py"], capture_output=True, cwd=cwd
166+
)
167+
168+
assert (
169+
run_result.returncode == 0
170+
), "pysource-codegen should only generate code which can be parsed"
171+
172+
git_ref = subprocess.run(
173+
["git", "rev-parse", "origin/main"], capture_output=True
174+
).stdout
175+
176+
return f"""
177+
**Describe the bug**
178+
179+
The following code can not be parsed/formatted by black:
180+
181+
``` python
182+
{issue.src}
183+
```
184+
([playground]({create_link(issue)}))
185+
186+
black reported the following error:
187+
```
188+
> {" ".join(command)}
189+
{format_result.stderr.decode().rstrip()}
190+
```
191+
{reported_diff}
192+
193+
but it can be parsed by cpython:
194+
``` python
195+
{parse_code.rstrip()}
196+
```
197+
{fast_formatted}
198+
199+
**Environment**
200+
201+
<!-- Please complete the following information: -->
202+
203+
- Black's version: current main ({git_ref.decode().strip()})
204+
- OS and Python version: Linux/Python {sys.version}
205+
206+
**Additional context**
207+
208+
The bug was found by pysource-codegen (see #3908)
209+
The above problem description was created from a script,
210+
let me know if you think it can be improved.
211+
"""
212+
213+
214+
def main() -> None:
215+
issue = find_issue()
216+
if issue is None:
217+
return
218+
219+
issue = minimize_code(issue)
220+
221+
while issue.mode.line_length > 1 and bug_in_code(issue):
222+
issue.mode.line_length -= 1
223+
issue.mode.line_length += 1
224+
225+
issue = minimize_code(issue)
226+
227+
while issue.mode.line_length <= 100 and bug_in_code(issue):
228+
issue.mode.line_length += 1
229+
issue.mode.line_length -= 1
230+
231+
issue = minimize_code(issue)
232+
233+
print(create_issue(issue))
234+
235+
236+
if __name__ == "__main__":
237+
main()

Diff for: 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)