Skip to content

Commit 632c88b

Browse files
committed
meta tests: refactor run_pytest
1 parent e3210d0 commit 632c88b

File tree

3 files changed

+104
-72
lines changed

3 files changed

+104
-72
lines changed

mypy/test/meta/_pytest.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import shlex
2+
import subprocess
3+
import sys
4+
import textwrap
5+
import uuid
6+
from dataclasses import dataclass
7+
from pathlib import Path
8+
from typing import Iterable
9+
10+
from mypy.test.config import test_data_prefix
11+
12+
13+
@dataclass
14+
class PytestResult:
15+
source: str
16+
source_updated: str
17+
stdout: str
18+
stderr: str
19+
20+
21+
def strip_source(s: str) -> str:
22+
return textwrap.dedent(s).lstrip()
23+
24+
25+
def run_pytest(
26+
data_suite: str,
27+
*,
28+
data_file_prefix: str,
29+
node_prefix: str,
30+
extra_args: Iterable[str],
31+
max_attempts: int,
32+
) -> PytestResult:
33+
"""
34+
Runs a suite of data test cases through pytest until either tests pass
35+
or until a maximum number of attempts (needed for incremental tests).
36+
"""
37+
p_test_data = Path(test_data_prefix)
38+
p_root = p_test_data.parent.parent
39+
p = p_test_data / f"{data_file_prefix}-meta-{uuid.uuid4()}.test"
40+
assert not p.exists()
41+
data_suite = strip_source(data_suite)
42+
try:
43+
p.write_text(data_suite)
44+
45+
test_nodeid = f"{node_prefix}::{p.name}"
46+
extra_args = [sys.executable, "-m", "pytest", "-n", "0", "-s", *extra_args, test_nodeid]
47+
if sys.version_info >= (3, 8):
48+
cmd = shlex.join(extra_args)
49+
else:
50+
cmd = " ".join(extra_args)
51+
for i in range(max_attempts - 1, -1, -1):
52+
print(f">> {cmd}")
53+
proc = subprocess.run(extra_args, capture_output=True, check=False, cwd=p_root)
54+
if proc.returncode == 0:
55+
break
56+
prefix = "NESTED PYTEST STDOUT"
57+
for line in proc.stdout.decode().splitlines():
58+
print(f"{prefix}: {line}")
59+
prefix = " " * len(prefix)
60+
prefix = "NESTED PYTEST STDERR"
61+
for line in proc.stderr.decode().splitlines():
62+
print(f"{prefix}: {line}")
63+
prefix = " " * len(prefix)
64+
print(f"Exit code {proc.returncode} ({i} attempts remaining)")
65+
66+
return PytestResult(
67+
source=data_suite,
68+
source_updated=p.read_text(),
69+
stdout=proc.stdout.decode(),
70+
stderr=proc.stderr.decode(),
71+
)
72+
finally:
73+
p.unlink()
74+
75+
76+
def run_type_check_suite(
77+
data_suite: str, *, extra_args: Iterable[str], max_attempts: int
78+
) -> PytestResult:
79+
run_pytest(
80+
data_suite,
81+
data_file_prefix="check",
82+
node_prefix="mypy/test/testcheck.py::TypeCheckSuite",
83+
extra_args=extra_args,
84+
max_attempts=max_attempts,
85+
)

mypy/test/meta/test_parse_data.py

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,17 @@
22
A "meta test" which tests the parsing of .test files. This is not meant to become exhaustive
33
but to ensure we maintain a basic level of ergonomics for mypy contributors.
44
"""
5-
import subprocess
6-
import sys
7-
import textwrap
8-
import uuid
9-
from pathlib import Path
10-
11-
from mypy.test.config import test_data_prefix
125
from mypy.test.helpers import Suite
6+
from mypy.test.meta._pytest import PytestResult, run_type_check_suite
137

148

159
class ParseTestDataSuite(Suite):
16-
def _dedent(self, s: str) -> str:
17-
return textwrap.dedent(s).lstrip()
18-
19-
def _run_pytest(self, data_suite: str) -> str:
20-
p_test_data = Path(test_data_prefix)
21-
p_root = p_test_data.parent.parent
22-
p = p_test_data / f"check-meta-{uuid.uuid4()}.test"
23-
assert not p.exists()
24-
try:
25-
p.write_text(data_suite)
26-
test_nodeid = f"mypy/test/testcheck.py::TypeCheckSuite::{p.name}"
27-
args = [sys.executable, "-m", "pytest", "-n", "0", "-s", test_nodeid]
28-
proc = subprocess.run(args, cwd=p_root, capture_output=True, check=False)
29-
return proc.stdout.decode()
30-
finally:
31-
p.unlink()
10+
def _run_pytest(self, data_suite: str) -> PytestResult:
11+
return run_type_check_suite(data_suite, extra_args=[], max_attempts=1)
3212

3313
def test_parse_invalid_case(self) -> None:
34-
# Arrange
35-
data = self._dedent(
14+
# Act
15+
result = self._run_pytest(
3616
"""
3717
[case abc]
3818
s: str
@@ -41,15 +21,12 @@ def test_parse_invalid_case(self) -> None:
4121
"""
4222
)
4323

44-
# Act
45-
actual = self._run_pytest(data)
46-
4724
# Assert
48-
assert "Invalid testcase id 'foo-XFAIL'" in actual
25+
assert "Invalid testcase id 'foo-XFAIL'" in result.stdout
4926

5027
def test_parse_invalid_section(self) -> None:
51-
# Arrange
52-
data = self._dedent(
28+
# Act
29+
result = self._run_pytest(
5330
"""
5431
[case abc]
5532
s: str
@@ -58,12 +35,9 @@ def test_parse_invalid_section(self) -> None:
5835
"""
5936
)
6037

61-
# Act
62-
actual = self._run_pytest(data)
63-
6438
# Assert
65-
expected_lineno = data.splitlines().index("[unknownsection]") + 1
39+
expected_lineno = result.source.splitlines().index("[unknownsection]") + 1
6640
expected = (
6741
f".test:{expected_lineno}: Invalid section header [unknownsection] in case 'abc'"
6842
)
69-
assert expected in actual
43+
assert expected in result.stdout

mypy/test/meta/test_update_data.py

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,22 @@
33
Updating the expected output, especially when it's in the form of inline (comment) assertions,
44
can be brittle, which is why we're "meta-testing" here.
55
"""
6-
import shlex
7-
import subprocess
8-
import sys
9-
import textwrap
10-
import uuid
11-
from pathlib import Path
12-
13-
from mypy.test.config import test_data_prefix
146
from mypy.test.helpers import Suite
7+
from mypy.test.meta._pytest import PytestResult, run_type_check_suite, strip_source
158

169

1710
class UpdateDataSuite(Suite):
18-
def _run_pytest_update_data(self, data_suite: str, *, max_attempts: int) -> str:
11+
def _run_pytest_update_data(self, data_suite: str) -> PytestResult:
1912
"""
2013
Runs a suite of data test cases through 'pytest --update-data' until either tests pass
2114
or until a maximum number of attempts (needed for incremental tests).
2215
"""
23-
p_test_data = Path(test_data_prefix)
24-
p_root = p_test_data.parent.parent
25-
p = p_test_data / f"check-meta-{uuid.uuid4()}.test"
26-
assert not p.exists()
27-
try:
28-
p.write_text(textwrap.dedent(data_suite).lstrip())
29-
30-
test_nodeid = f"mypy/test/testcheck.py::TypeCheckSuite::{p.name}"
31-
args = [sys.executable, "-m", "pytest", "-n", "0", "-s", "--update-data", test_nodeid]
32-
if sys.version_info >= (3, 8):
33-
cmd = shlex.join(args)
34-
else:
35-
cmd = " ".join(args)
36-
for i in range(max_attempts - 1, -1, -1):
37-
res = subprocess.run(args, cwd=p_root)
38-
if res.returncode == 0:
39-
break
40-
print(f"`{cmd}` returned {res.returncode}: {i} attempts remaining")
41-
42-
return p.read_text()
43-
finally:
44-
p.unlink()
16+
return run_type_check_suite(data_suite, extra_args=["--update-data"], max_attempts=3)
4517

4618
def test_update_data(self) -> None:
4719
# Note: We test multiple testcases rather than 'test case per test case'
4820
# so we could also exercise rewriting multiple testcases at once.
49-
actual = self._run_pytest_update_data(
21+
result = self._run_pytest_update_data(
5022
"""
5123
[case testCorrect]
5224
s: str = 42 # E: Incompatible types in assignment (expression has type "int", variable has type "str")
@@ -100,12 +72,12 @@ def test_update_data(self) -> None:
10072
[file b.py]
10173
s2: str = 43 # E: baz
10274
[builtins fixtures/list.pyi]
103-
""",
104-
max_attempts=3,
75+
"""
10576
)
10677

10778
# Assert
108-
expected = """
79+
expected = strip_source(
80+
"""
10981
[case testCorrect]
11082
s: str = 42 # E: Incompatible types in assignment (expression has type "int", variable has type "str")
11183
@@ -157,4 +129,5 @@ def test_update_data(self) -> None:
157129
s2: str = 43 # E: Incompatible types in assignment (expression has type "int", variable has type "str")
158130
[builtins fixtures/list.pyi]
159131
"""
160-
assert actual == textwrap.dedent(expected).lstrip()
132+
)
133+
assert result.source_updated == expected

0 commit comments

Comments
 (0)