11"""Custom lint tests."""
22
3+ import subprocess
4+ import sys
35from pathlib import Path
46
57import 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
6281def 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 } \n stderr:\n { result .stderr } "
98+ )
99+ assert result .returncode == 0 , message
91100
92101
93102def 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