Skip to content

Commit 65a6322

Browse files
committed
Speed up custom linter tests by collecting once
Previously ran pytest.main() per pattern (~120 invocations, 89s). Now do a single in-process collection and check each pattern against the cached node IDs in pure Python via a small boundary-aware prefix matcher. Runtime drops from 89s to under 1s.
1 parent c516aee commit 65a6322

1 file changed

Lines changed: 36 additions & 15 deletions

File tree

ci/test_custom_linters.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

81105
def 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

Comments
 (0)