11"""Custom lint tests."""
22
3+ import subprocess
4+ import sys
35from pathlib import Path
46
57import pytest
@@ -19,52 +21,61 @@ 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" ,
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" ,
5851 # Disable warnings to avoid many instances of:
5952 # ```
6053 # Unknown config option: retry_delay
6154 # ```
6255 "--disable-warnings" ,
6356 ci_pattern ,
6457 ],
65- plugins = [plugin ],
58+ check = False ,
59+ cwd = repository_root ,
60+ capture_output = True ,
61+ text = True ,
6662 )
67- return plugin .collected
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
6879
6980
7081def test_ci_patterns_valid (request : pytest .FixtureRequest ) -> None :
@@ -73,53 +84,45 @@ def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
7384 test in
7485 the test suite.
7586 """
76- ci_patterns = _ci_patterns (repository_root = request .config .rootpath )
87+ repository_root = request .config .rootpath
88+ ci_patterns = _ci_patterns (repository_root = repository_root )
7789
7890 for ci_pattern in ci_patterns :
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- ],
91+ result = _collect (
92+ ci_pattern = ci_pattern ,
93+ repository_root = repository_root ,
10394 )
104-
105- message = f'"{ ci_pattern } " does not match any tests.'
106- assert collect_only_result == 0 , message
95+ message = (
96+ f'"{ ci_pattern } " does not match any tests.\n '
97+ f"stdout:\n { result .stdout } \n stderr:\n { result .stderr } "
98+ )
99+ assert result .returncode == 0 , message
107100
108101
109102def test_tests_collected_once (request : pytest .FixtureRequest ) -> None :
110103 """Each test in the test suite is collected exactly once.
111104
112105 This does not necessarily mean that they are run - they may be skipped.
113106 """
114- ci_patterns = _ci_patterns (repository_root = request .config .rootpath )
115- 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+ )
116113 assert all_tests
117114 tests_to_patterns : dict [str , set [str ]] = {}
118115
119116 for pattern in ci_patterns :
120- tests = _tests_from_pattern (ci_pattern = pattern )
117+ tests = _tests_from_pattern (
118+ ci_pattern = pattern ,
119+ repository_root = repository_root ,
120+ )
121121 for test in tests :
122- tests_to_patterns .setdefault (test , set ()).add (pattern )
122+ if test in tests_to_patterns :
123+ tests_to_patterns [test ].add (pattern )
124+ else :
125+ tests_to_patterns [test ] = {pattern }
123126
124127 for test_name , patterns in tests_to_patterns .items ():
125128 message = (
0 commit comments