@@ -34,9 +34,9 @@ def pytest_collection_modifyitems(
3434 self .nodeids .update (item .nodeid for item in items )
3535
3636
37- @beartype
38- def _tests_from_pattern ( * , ci_pattern : str ) -> set [str ]:
39- """From a CI pattern, get all tests ``pytest`` would collect .
37+ @pytest . fixture ( scope = "module" )
38+ def all_tests ( ) -> frozenset [str ]:
39+ """Collect every test node ID in the suite, exactly once .
4040
4141 Uses a collection-hook plugin instead of parsing stdout: an in-process
4242 ``pytest.main()`` installs its own output capture, so reading from
@@ -57,14 +57,36 @@ def _tests_from_pattern(*, ci_pattern: str) -> set[str]:
5757 # Unknown config option: retry_delay
5858 # ```
5959 "--disable-warnings" ,
60- ci_pattern ,
60+ "." ,
6161 ],
6262 plugins = [plugin ],
6363 )
64- return plugin .nodeids
64+ return frozenset (plugin .nodeids )
65+
6566
67+ @beartype
68+ def _matches (* , nodeid : str , ci_pattern : str ) -> bool :
69+ """Whether ``pytest <ci_pattern>`` would have collected ``nodeid``.
70+
71+ The patterns in the CI matrix are all of the form ``path[/]`` or
72+ ``path::Class[::method]``. A node ID matches if it equals the pattern,
73+ is a directory child of a pattern ending with ``/``, or extends the
74+ pattern at a ``::`` (sub-item), ``/`` (path), or ``[`` (parametrize)
75+ boundary.
76+ """
77+ if nodeid == ci_pattern :
78+ return True
79+ if not nodeid .startswith (ci_pattern ):
80+ return False
81+ if ci_pattern .endswith ("/" ):
82+ return True
83+ return nodeid [len (ci_pattern )] in {":" , "/" , "[" }
6684
67- def test_ci_patterns_valid (request : pytest .FixtureRequest ) -> None :
85+
86+ def test_ci_patterns_valid (
87+ request : pytest .FixtureRequest ,
88+ all_tests : frozenset [str ],
89+ ) -> None :
6890 """
6991 All of the CI patterns in the CI configuration match at least one
7092 test in
@@ -73,14 +95,17 @@ def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
7395 ci_patterns = _ci_patterns (repository_root = request .config .rootpath )
7496
7597 for ci_pattern in ci_patterns :
76- tests = _tests_from_pattern (ci_pattern = ci_pattern )
98+ matched = {
99+ n for n in all_tests if _matches (nodeid = n , ci_pattern = ci_pattern )
100+ }
77101 message = f'"{ ci_pattern } " does not match any tests.'
78- assert tests , message
102+ assert matched , message
79103
80104
81105def test_tests_collected_once (
82106 * ,
83107 request : pytest .FixtureRequest ,
108+ all_tests : frozenset [str ],
84109) -> None :
85110 """Each test in the test suite is collected exactly once.
86111
@@ -90,12 +115,9 @@ def test_tests_collected_once(
90115 tests_to_patterns : dict [str , set [str ]] = {}
91116
92117 for pattern in ci_patterns :
93- tests = _tests_from_pattern (ci_pattern = pattern )
94- for test in tests :
95- if test in tests_to_patterns :
96- tests_to_patterns [test ].add (pattern )
97- else :
98- tests_to_patterns [test ] = {pattern }
118+ for test in all_tests :
119+ if _matches (nodeid = test , ci_pattern = pattern ):
120+ tests_to_patterns .setdefault (test , set ()).add (pattern )
99121
100122 for test_name , patterns in tests_to_patterns .items ():
101123 message = (
@@ -105,6 +127,5 @@ def test_tests_collected_once(
105127 )
106128 assert len (patterns ) == 1 , message
107129
108- all_tests = _tests_from_pattern (ci_pattern = "." )
109130 assert tests_to_patterns .keys () - all_tests == set ()
110131 assert all_tests - tests_to_patterns .keys () == set ()
0 commit comments