11"""pytest plugin for API coverage tracking."""
22
33import logging
4- from typing import Any , Optional
4+ from typing import Any , Optional , Tuple
55
66import pytest
77
@@ -27,6 +27,7 @@ def is_supported_framework(app: Any) -> bool:
2727 or (module_name == "fastapi" and app_type == "FastAPI" )
2828 )
2929
30+
3031def extract_app_from_client (client : Any ) -> Optional [Any ]:
3132 """Extract app from various client types."""
3233 # Typical attributes used by popular clients
@@ -46,10 +47,11 @@ def extract_app_from_client(client: Any) -> Optional[Any]:
4647
4748 # Flask's test client may expose the application via "application" or "app"
4849 if hasattr (client , "_app" ):
49- return getattr ( client , " _app" )
50+ return client . _app
5051
5152 return None
5253
54+
5355def pytest_addoption (parser : pytest .Parser ) -> None :
5456 """Add API coverage flags to the pytest parser."""
5557 add_pytest_api_cov_flags (parser )
@@ -137,7 +139,9 @@ def fixture_func(request: pytest.FixtureRequest) -> Any:
137139 try :
138140 app = request .getfixturevalue ("app" )
139141 except pytest .FixtureLookupError :
140- logger .warning (f"> Coverage not enabled and no existing fixture available for '{ fixture_name } ', returning None" )
142+ logger .warning (
143+ f"> Coverage not enabled and no existing fixture available for '{ fixture_name } ', returning None"
144+ )
141145 yield None
142146 return
143147 # if we have an app, attempt to create a tracked client using adapter without recorder
@@ -146,11 +150,12 @@ def fixture_func(request: pytest.FixtureRequest) -> Any:
146150
147151 adapter = get_framework_adapter (app )
148152 client = adapter .get_tracked_client (None , request .node .name )
149- yield client
150- return
151- except Exception :
153+ except Exception : # noqa: BLE001
152154 yield existing_client
153155 return
156+ else :
157+ yield client
158+ return
154159
155160 # At this point coverage is enabled and coverage_data exists
156161 if existing_client is None :
@@ -187,7 +192,9 @@ def fixture_func(request: pytest.FixtureRequest) -> Any:
187192 for endpoint_method in endpoints :
188193 method , path = endpoint_method .split (" " , 1 )
189194 coverage_data .add_discovered_endpoint (path , method , f"{ framework_name .lower ()} _adapter" )
190- logger .info (f"> pytest-api-coverage: Discovered { len (endpoints )} endpoints when creating '{ fixture_name } '." )
195+ logger .info (
196+ f"> pytest-api-coverage: Discovered { len (endpoints )} endpoints when creating '{ fixture_name } '."
197+ )
191198 except Exception as e : # noqa: BLE001
192199 logger .warning (f"> pytest-api-coverage: Could not discover endpoints from app. Error: { e } " )
193200
@@ -203,13 +210,17 @@ def fixture_func(request: pytest.FixtureRequest) -> Any:
203210
204211 adapter = get_framework_adapter (app )
205212 client = adapter .get_tracked_client (coverage_data .recorder , request .node .name )
206- yield client
207- return
208213 except Exception as e : # noqa: BLE001
209214 logger .warning (f"> Failed to create tracked client for '{ fixture_name } ': { e } " )
215+ else :
216+ yield client
217+ return
210218
211219 # Last resort: yield None but do not skip
212- logger .warning (f"> create_coverage_fixture('{ fixture_name } ') could not provide a client; tests will run without API coverage for this fixture." )
220+ logger .warning (
221+ f"> create_coverage_fixture('{ fixture_name } ') could not provide a client; "
222+ "tests will run without API coverage for this fixture."
223+ )
213224 yield None
214225
215226 fixture_func .__name__ = fixture_name
@@ -225,7 +236,7 @@ class CoverageWrapper:
225236 def __init__ (self , wrapped_client : Any ) -> None :
226237 self ._wrapped = wrapped_client
227238
228- def _extract_path_and_method (self , name : str , args : Any , kwargs : Any ) -> Optional [tuple ]:
239+ def _extract_path_and_method (self , name : str , args : Any , kwargs : Any ) -> Optional [Tuple [ str , str ] ]:
229240 # Try several strategies to obtain a path and method
230241 path = None
231242 method = None
@@ -243,9 +254,10 @@ def _extract_path_and_method(self, name: str, args: Any, kwargs: Any) -> Optiona
243254 try :
244255 path = first .url .path
245256 method = getattr (first , "method" , name ).upper ()
246- return path , method
247- except Exception :
257+ except Exception : # noqa: BLE001
248258 pass
259+ else :
260+ return path , method
249261
250262 # Try kwargs-based FlaskClient open signature
251263 if kwargs :
@@ -272,7 +284,8 @@ def tracked_method(*args: Any, **kwargs: Any) -> Any:
272284
273285 return tracked_method
274286
275- elif name == "open" :
287+ if name == "open" :
288+
276289 def tracked_open (* args : Any , ** kwargs : Any ) -> Any :
277290 response = attr (* args , ** kwargs )
278291 if recorder is not None :
@@ -285,6 +298,7 @@ def tracked_open(*args: Any, **kwargs: Any) -> Any:
285298 return tracked_open
286299
287300 return attr
301+
288302 return CoverageWrapper (client )
289303
290304
@@ -300,30 +314,48 @@ def coverage_client(request: pytest.FixtureRequest) -> Any:
300314 coverage_data = getattr (session , "api_coverage_data" , None )
301315 if coverage_data is None :
302316 pytest .skip ("API coverage data not initialized. This should not happen." )
303-
317+
304318 client = None
305319 for fixture_name in config .client_fixture_names :
306320 try :
307321 client = request .getfixturevalue (fixture_name )
308322 logger .info (f"> Found custom fixture '{ fixture_name } ', wrapping with coverage tracking" )
309323 break
310324 except pytest .FixtureLookupError :
311- logger .warning (f"> Custom fixture '{ fixture_name } ' not found, trying next one" )
325+ logger .debug (f"> Custom fixture '{ fixture_name } ' not found, trying next one" )
312326 continue
313-
327+
328+ if client is None :
329+ # Try to fallback to an 'app' fixture and create a tracked client
330+ try :
331+ app = request .getfixturevalue ("app" )
332+ logger .info ("> Found 'app' fixture, creating tracked client from app" )
333+ from .frameworks import get_framework_adapter
334+
335+ adapter = get_framework_adapter (app )
336+ client = adapter .get_tracked_client (coverage_data .recorder , request .node .name )
337+ except pytest .FixtureLookupError :
338+ logger .warning ("> No test client fixture found and no 'app' fixture available. Falling back to None" )
339+ client = None
340+ except Exception as e : # noqa: BLE001
341+ logger .warning (f"> Failed to create tracked client from 'app' fixture: { e } " )
342+ client = None
343+
314344 if client is None :
315- logger .warning ("> No test client fixture found, skipping coverage tracking " )
345+ logger .warning ("> Coverage client could not be created; tests will run without API coverage for this session. " )
316346 return None
317347
318348 app = extract_app_from_client (client )
319349 logger .debug (f"> Extracted app from client: { app } , app type: { type (app ).__name__ if app else None } " )
320350
321351 if app is None :
322- logger .warning ("> No app found, skipping coverage tracking" )
352+ logger .warning ("> No app found, returning client without coverage tracking" )
323353 return client
324354
325355 if not is_supported_framework (app ):
326- logger .warning (f"> Unsupported framework: { type (app ).__name__ } . pytest-api-coverage supports Flask and FastAPI." )
356+ logger .warning (
357+ f"> Unsupported framework: { type (app ).__name__ } . pytest-api-coverage supports Flask and FastAPI."
358+ )
327359 return client
328360
329361 try :
@@ -348,9 +380,6 @@ def coverage_client(request: pytest.FixtureRequest) -> Any:
348380 except Exception as e : # noqa: BLE001
349381 logger .warning (f"> pytest-api-coverage: Could not discover endpoints. Error: { e } " )
350382 return client
351-
352- else :
353- logger .debug (f"> Endpoints already discovered: { len (coverage_data .discovered_endpoints .endpoints )} " )
354383
355384 return wrap_client_with_coverage (client , coverage_data .recorder , request .node .name )
356385
0 commit comments