1010
1111from __future__ import annotations
1212
13+ import dataclasses
1314import time
1415import typing as t
1516from collections import defaultdict
2425
2526pytest_plugins = ["pytester" ]
2627
28+
29+ @dataclasses .dataclass
30+ class FixtureMetrics :
31+ """Metrics collected during fixture execution."""
32+
33+ fixture_name : str
34+ duration : float
35+ cache_hit : bool | None = None # None if not applicable (non-repo fixture)
36+
37+
2738# Fixture profiling storage
2839_fixture_timings : dict [str , list [float ]] = defaultdict (list )
2940_fixture_call_counts : dict [str , int ] = defaultdict (int )
41+ _fixture_cache_hits : dict [str , int ] = defaultdict (int )
42+ _fixture_cache_misses : dict [str , int ] = defaultdict (int )
3043
3144
3245def pytest_addoption (parser : pytest .Parser ) -> None :
@@ -76,10 +89,17 @@ def pytest_fixture_setup(
7689 fixturedef : FixtureDef [t .Any ],
7790 request : SubRequest ,
7891) -> t .Generator [None , t .Any , t .Any ]:
79- """Wrap fixture setup to measure timing."""
92+ """Wrap fixture setup to measure timing and track cache hits ."""
8093 start = time .perf_counter ()
8194 try :
8295 result = yield
96+ # Track cache hits for fixtures that support it (RepoFixtureResult)
97+ if hasattr (result , "from_cache" ):
98+ fixture_name = fixturedef .argname
99+ if result .from_cache :
100+ _fixture_cache_hits [fixture_name ] += 1
101+ else :
102+ _fixture_cache_misses [fixture_name ] += 1
83103 return result
84104 finally :
85105 duration = time .perf_counter () - start
@@ -93,7 +113,7 @@ def pytest_terminal_summary(
93113 exitstatus : int ,
94114 config : pytest .Config ,
95115) -> None :
96- """Display fixture timing summary."""
116+ """Display fixture timing and cache statistics summary."""
97117 durations_count = config .option .fixture_durations
98118 durations_min = config .option .fixture_durations_min
99119
@@ -134,6 +154,29 @@ def pytest_terminal_summary(
134154 f"{ name :<40} { total :>9.3f} s { calls :>8} { avg :>9.3f} s" ,
135155 )
136156
157+ # Display cache statistics if any repo fixtures were used
158+ if _fixture_cache_hits or _fixture_cache_misses :
159+ terminalreporter .write_line ("" )
160+ terminalreporter .write_sep ("=" , "fixture cache statistics" )
161+ terminalreporter .write_line ("" )
162+ terminalreporter .write_line (
163+ f"{ 'Fixture' :<40} { 'Hits' :>8} { 'Misses' :>8} { 'Hit Rate' :>10} " ,
164+ )
165+ terminalreporter .write_line ("-" * 70 )
166+
167+ # Combine hits and misses for all fixtures that have cache tracking
168+ all_cache_fixtures = set (_fixture_cache_hits .keys ()) | set (
169+ _fixture_cache_misses .keys ()
170+ )
171+ for name in sorted (all_cache_fixtures ):
172+ hits = _fixture_cache_hits .get (name , 0 )
173+ misses = _fixture_cache_misses .get (name , 0 )
174+ total = hits + misses
175+ hit_rate = (hits / total * 100 ) if total > 0 else 0
176+ terminalreporter .write_line (
177+ f"{ name :<40} { hits :>8} { misses :>8} { hit_rate :>9.1f} %" ,
178+ )
179+
137180
138181@pytest .fixture (autouse = True )
139182def add_doctest_fixtures (
0 commit comments