11"""Custom lint tests."""
22
3+ import subprocess
4+ import sys
35from pathlib import Path
46from typing import TYPE_CHECKING
57
@@ -24,37 +26,33 @@ def _ci_patterns(*, repository_root: Path) -> set[str]:
2426
2527
2628@beartype
27- def _tests_from_pattern (
28- * ,
29- ci_pattern : str ,
30- capsys : pytest .CaptureFixture [str ],
31- ) -> set [str ]:
32- """From a CI pattern, get all tests ``pytest`` would collect."""
33- # Clear the captured output.
34- capsys .readouterr ()
35- tests : Iterable [str ] = set ()
36- pytest .main (
29+ def _collect (
30+ * , ci_pattern : str , repository_root : Path
31+ ) -> subprocess .CompletedProcess [str ]:
32+ """Run ``pytest --collect-only`` for ``ci_pattern`` in a fresh
33+ subprocess.
34+
35+ A real subprocess (not ``pytest.main``) is used so that plugin state
36+ (notably ``pytest-beartype-tests`` re-wrapping the same test
37+ functions) does not accumulate across iterations and trigger
38+ ``Cannot stringify annotation containing string formatting`` under
39+ Python 3.14 deferred annotations.
40+ """
41+ return subprocess .run (
3742 args = [
43+ sys .executable ,
44+ "-m" ,
45+ "pytest" ,
3846 "-q" ,
3947 "--collect-only" ,
40- # If there are any warnings, these obscure the output.
4148 "--disable-warnings" ,
42- # Disable ``pytest-beartype-tests`` to avoid repeated wrapping
43- # of the same test functions across many ``pytest.main`` calls,
44- # which can trigger ``Cannot stringify annotation containing
45- # string formatting`` under Python 3.14 deferred annotations.
46- "-p" ,
47- "no:pytest_beartype_tests" ,
4849 ci_pattern ,
4950 ],
51+ check = False ,
52+ cwd = repository_root ,
53+ capture_output = True ,
54+ text = True ,
5055 )
51- data = capsys .readouterr ().out
52- for line in data .splitlines ():
53- # We filter empty lines and lines which look like
54- # "9 tests collected in 0.01s".
55- if line and "collected in" not in line :
56- tests = {* tests , line }
57- return set (tests )
5856
5957
6058def test_ci_patterns_valid (request : pytest .FixtureRequest ) -> None :
@@ -63,52 +61,38 @@ def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
6361 test in
6462 the test suite.
6563 """
66- ci_patterns = _ci_patterns (repository_root = request .config .rootpath )
64+ repository_root = request .config .rootpath
65+ ci_patterns = _ci_patterns (repository_root = repository_root )
6766
6867 for ci_pattern in ci_patterns :
69- collect_only_result = pytest .main (
70- args = [
71- "--collect-only" ,
72- ci_pattern ,
73- # Disable pytest-retry to avoid:
74- # ```
75- # ValueError: no option named 'filtered_exceptions'
76- # ````
77- "-p" ,
78- "no:pytest-retry" ,
79- # Disable ``pytest-beartype-tests`` to avoid repeated
80- # wrapping of the same test functions across many
81- # ``pytest.main`` calls, which can trigger ``Cannot
82- # stringify annotation containing string formatting``
83- # under Python 3.14 deferred annotations.
84- "-p" ,
85- "no:pytest_beartype_tests" ,
86- # Disable warnings to avoid many instances of:
87- # ```
88- # Unknown config option: retry_delay
89- # ```
90- "--disable-warnings" ,
91- ],
68+ result = _collect (
69+ ci_pattern = ci_pattern ,
70+ repository_root = repository_root ,
9271 )
93-
94- message = f'"{ ci_pattern } " does not match any tests.'
95- assert collect_only_result == 0 , message
72+ message = (
73+ f'"{ ci_pattern } " does not match any tests.\n '
74+ f"stdout:\n { result .stdout } \n stderr:\n { result .stderr } "
75+ )
76+ assert result .returncode == 0 , message
9677
9778
9879def test_tests_collected_once (
9980 * ,
100- capsys : pytest .CaptureFixture [str ],
10181 request : pytest .FixtureRequest ,
10282) -> None :
10383 """Each test in the test suite is collected exactly once.
10484
10585 This does not necessarily mean that they are run - they may be skipped.
10686 """
107- ci_patterns = _ci_patterns (repository_root = request .config .rootpath )
87+ repository_root = request .config .rootpath
88+ ci_patterns = _ci_patterns (repository_root = repository_root )
10889 tests_to_patterns : dict [str , set [str ]] = {}
10990
11091 for pattern in ci_patterns :
111- tests = _tests_from_pattern (ci_pattern = pattern , capsys = capsys )
92+ tests = _tests_from_pattern (
93+ ci_pattern = pattern ,
94+ repository_root = repository_root ,
95+ )
11296 for test in tests :
11397 if test in tests_to_patterns :
11498 tests_to_patterns [test ].add (pattern )
@@ -123,6 +107,29 @@ def test_tests_collected_once(
123107 )
124108 assert len (patterns ) == 1 , message
125109
126- all_tests = _tests_from_pattern (ci_pattern = "." , capsys = capsys )
110+ all_tests = _tests_from_pattern (
111+ ci_pattern = "." ,
112+ repository_root = repository_root ,
113+ )
127114 assert tests_to_patterns .keys () - all_tests == set ()
128115 assert all_tests - tests_to_patterns .keys () == set ()
116+
117+
118+ @beartype
119+ def _tests_from_pattern (
120+ * ,
121+ ci_pattern : str ,
122+ repository_root : Path ,
123+ ) -> set [str ]:
124+ """From a CI pattern, get all tests ``pytest`` would collect."""
125+ result = _collect (
126+ ci_pattern = ci_pattern ,
127+ repository_root = repository_root ,
128+ )
129+ tests : Iterable [str ] = set ()
130+ for line in result .stdout .splitlines ():
131+ # We filter empty lines and lines which look like
132+ # "9 tests collected in 0.01s".
133+ if line and "collected in" not in line :
134+ tests = {* tests , line }
135+ return set (tests )
0 commit comments