Skip to content

Commit 41b05c2

Browse files
committed
Use real subprocess in test_custom_linters to avoid beartype state accumulation
1 parent dfadbc6 commit 41b05c2

1 file changed

Lines changed: 59 additions & 43 deletions

File tree

ci/test_custom_linters.py

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Custom lint tests."""
22

3+
import subprocess
4+
import sys
35
from pathlib import Path
46

57
import pytest
@@ -19,32 +21,31 @@ def _ci_patterns(*, repository_root: Path) -> set[str]:
1921
return ci_patterns
2022

2123

22-
class _CollectPlugin:
23-
"""Pytest plugin that records the node IDs of collected items."""
24-
25-
def __init__(self) -> None:
26-
"""Start with an empty set of collected node IDs."""
27-
self.collected: set[str] = set()
28-
29-
def pytest_itemcollected(self, item: pytest.Item) -> None:
30-
"""Record each collected item's node ID."""
31-
self.collected.add(item.nodeid)
32-
33-
3424
@beartype
35-
def _tests_from_pattern(*, ci_pattern: str) -> set[str]:
36-
"""From a CI pattern, get all tests ``pytest`` would collect."""
37-
plugin = _CollectPlugin()
38-
pytest.main(
25+
def _collect(
26+
*, ci_pattern: str, repository_root: Path
27+
) -> subprocess.CompletedProcess[str]:
28+
"""Run ``pytest --collect-only`` for ``ci_pattern`` in a fresh
29+
subprocess.
30+
31+
A real subprocess (not ``pytest.main``) is used so that plugin state
32+
-- notably ``pytest-beartype-tests`` re-wrapping the same test
33+
functions -- does not accumulate across iterations and trigger
34+
``Cannot stringify annotation containing string formatting`` under
35+
Python 3.14 deferred annotations.
36+
See https://github.com/adamtheturtle/pytest-beartype-tests/issues/30.
37+
"""
38+
return subprocess.run(
3939
args=[
40+
sys.executable,
41+
"-m",
42+
"pytest",
4043
"-q",
4144
"--collect-only",
4245
# Disable pytest-retry to avoid:
4346
# ```
4447
# ValueError: no option named 'filtered_exceptions'
4548
# ```
46-
# which causes the nested run to exit with INTERNAL_ERROR
47-
# before any items are collected.
4849
"-p",
4950
"no:pytest-retry",
5051
# Disable warnings to avoid many instances of:
@@ -54,9 +55,27 @@ def _tests_from_pattern(*, ci_pattern: str) -> set[str]:
5455
"--disable-warnings",
5556
ci_pattern,
5657
],
57-
plugins=[plugin],
58+
check=False,
59+
cwd=repository_root,
60+
capture_output=True,
61+
text=True,
62+
)
63+
64+
65+
@beartype
66+
def _tests_from_pattern(*, ci_pattern: str, repository_root: Path) -> set[str]:
67+
"""From a CI pattern, get all tests ``pytest`` would collect."""
68+
result = _collect(
69+
ci_pattern=ci_pattern,
70+
repository_root=repository_root,
5871
)
59-
return plugin.collected
72+
tests: set[str] = set()
73+
for line in result.stdout.splitlines():
74+
# We filter empty lines and lines which look like
75+
# "9 tests collected in 0.01s".
76+
if line and "collected in" not in line:
77+
tests.add(line)
78+
return tests
6079

6180

6281
def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
@@ -65,43 +84,40 @@ def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
6584
test in
6685
the test suite.
6786
"""
68-
ci_patterns = _ci_patterns(repository_root=request.config.rootpath)
87+
repository_root = request.config.rootpath
88+
ci_patterns = _ci_patterns(repository_root=repository_root)
6989

7090
for ci_pattern in ci_patterns:
71-
collect_only_result = pytest.main(
72-
args=[
73-
"--collect-only",
74-
ci_pattern,
75-
# Disable pytest-retry to avoid:
76-
# ```
77-
# ValueError: no option named 'filtered_exceptions'
78-
# ````
79-
"-p",
80-
"no:pytest-retry",
81-
# Disable warnings to avoid many instances of:
82-
# ```
83-
# Unknown config option: retry_delay
84-
# ```
85-
"--disable-warnings",
86-
],
91+
result = _collect(
92+
ci_pattern=ci_pattern,
93+
repository_root=repository_root,
8794
)
88-
89-
message = f'"{ci_pattern}" does not match any tests.'
90-
assert collect_only_result == 0, message
95+
message = (
96+
f'"{ci_pattern}" does not match any tests.\n'
97+
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
98+
)
99+
assert result.returncode == 0, message
91100

92101

93102
def test_tests_collected_once(request: pytest.FixtureRequest) -> None:
94103
"""Each test in the test suite is collected exactly once.
95104
96105
This does not necessarily mean that they are run - they may be skipped.
97106
"""
98-
ci_patterns = _ci_patterns(repository_root=request.config.rootpath)
99-
all_tests = _tests_from_pattern(ci_pattern=".")
107+
repository_root = request.config.rootpath
108+
ci_patterns = _ci_patterns(repository_root=repository_root)
109+
all_tests = _tests_from_pattern(
110+
ci_pattern=".",
111+
repository_root=repository_root,
112+
)
100113
assert all_tests
101114
tests_to_patterns: dict[str, set[str]] = {}
102115

103116
for pattern in ci_patterns:
104-
tests = _tests_from_pattern(ci_pattern=pattern)
117+
tests = _tests_from_pattern(
118+
ci_pattern=pattern,
119+
repository_root=repository_root,
120+
)
105121
for test in tests:
106122
if test in tests_to_patterns:
107123
tests_to_patterns[test].add(pattern)

0 commit comments

Comments
 (0)