11"""Custom lint tests."""
22
3- import subprocess
4- import sys
53from pathlib import Path
64
75import pytest
@@ -21,61 +19,52 @@ def _ci_patterns(*, repository_root: Path) -> set[str]:
2119 return ci_patterns
2220
2321
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+
2434@beartype
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 (
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 (
3939 args = [
40- sys .executable ,
41- "-m" ,
42- "pytest" ,
4340 "-q" ,
4441 "--collect-only" ,
4542 # Disable pytest-retry to avoid:
4643 # ```
4744 # ValueError: no option named 'filtered_exceptions'
4845 # ```
46+ # which causes the nested run to exit with INTERNAL_ERROR
47+ # before any items are collected.
4948 "-p" ,
5049 "no:pytest-retry" ,
50+ # Disable pytest-beartype-tests to avoid
51+ # https://github.com/beartype/beartype/issues/637 — wrapping
52+ # collected items with @beartype installs a buggy
53+ # __annotate_beartype__ closure on the underlying test
54+ # function, which crashes a subsequent nested collection on
55+ # Python 3.14.
56+ "-p" ,
57+ "no:pytest_beartype_tests" ,
5158 # Disable warnings to avoid many instances of:
5259 # ```
5360 # Unknown config option: retry_delay
5461 # ```
5562 "--disable-warnings" ,
5663 ci_pattern ,
5764 ],
58- check = False ,
59- cwd = repository_root ,
60- capture_output = True ,
61- text = True ,
65+ plugins = [plugin ],
6266 )
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 ,
71- )
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
67+ return plugin .collected
7968
8069
8170def test_ci_patterns_valid (request : pytest .FixtureRequest ) -> None :
@@ -84,45 +73,53 @@ def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
8473 test in
8574 the test suite.
8675 """
87- repository_root = request .config .rootpath
88- ci_patterns = _ci_patterns (repository_root = repository_root )
76+ ci_patterns = _ci_patterns (repository_root = request .config .rootpath )
8977
9078 for ci_pattern in ci_patterns :
91- result = _collect (
92- ci_pattern = ci_pattern ,
93- repository_root = repository_root ,
94- )
95- message = (
96- f'"{ ci_pattern } " does not match any tests.\n '
97- f"stdout:\n { result .stdout } \n stderr:\n { result .stderr } "
79+ collect_only_result = pytest .main (
80+ args = [
81+ "--collect-only" ,
82+ ci_pattern ,
83+ # Disable pytest-retry to avoid:
84+ # ```
85+ # ValueError: no option named 'filtered_exceptions'
86+ # ````
87+ "-p" ,
88+ "no:pytest-retry" ,
89+ # Disable pytest-beartype-tests to avoid
90+ # https://github.com/beartype/beartype/issues/637 —
91+ # wrapping collected items with @beartype installs a
92+ # buggy __annotate_beartype__ closure on the underlying
93+ # test function, which crashes a subsequent nested
94+ # collection on Python 3.14.
95+ "-p" ,
96+ "no:pytest_beartype_tests" ,
97+ # Disable warnings to avoid many instances of:
98+ # ```
99+ # Unknown config option: retry_delay
100+ # ```
101+ "--disable-warnings" ,
102+ ],
98103 )
99- assert result .returncode == 0 , message
104+
105+ message = f'"{ ci_pattern } " does not match any tests.'
106+ assert collect_only_result == 0 , message
100107
101108
102109def test_tests_collected_once (request : pytest .FixtureRequest ) -> None :
103110 """Each test in the test suite is collected exactly once.
104111
105112 This does not necessarily mean that they are run - they may be skipped.
106113 """
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- )
114+ ci_patterns = _ci_patterns (repository_root = request .config .rootpath )
115+ all_tests = _tests_from_pattern (ci_pattern = "." )
113116 assert all_tests
114117 tests_to_patterns : dict [str , set [str ]] = {}
115118
116119 for pattern in ci_patterns :
117- tests = _tests_from_pattern (
118- ci_pattern = pattern ,
119- repository_root = repository_root ,
120- )
120+ tests = _tests_from_pattern (ci_pattern = pattern )
121121 for test in tests :
122- if test in tests_to_patterns :
123- tests_to_patterns [test ].add (pattern )
124- else :
125- tests_to_patterns [test ] = {pattern }
122+ tests_to_patterns .setdefault (test , set ()).add (pattern )
126123
127124 for test_name , patterns in tests_to_patterns .items ():
128125 message = (
0 commit comments