pytest-api-cov 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_api_cov/__init__.py +1 -0
- pytest_api_cov/cli.py +192 -0
- pytest_api_cov/config.py +77 -0
- pytest_api_cov/frameworks.py +80 -0
- pytest_api_cov/models.py +122 -0
- pytest_api_cov/plugin.py +270 -0
- pytest_api_cov/pytest_flags.py +68 -0
- pytest_api_cov/report.py +185 -0
- pytest_api_cov-0.1.0.dist-info/METADATA +330 -0
- pytest_api_cov-0.1.0.dist-info/RECORD +13 -0
- pytest_api_cov-0.1.0.dist-info/WHEEL +4 -0
- pytest_api_cov-0.1.0.dist-info/entry_points.txt +5 -0
- pytest_api_cov-0.1.0.dist-info/licenses/LICENSE +201 -0
pytest_api_cov/plugin.py
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
"""pytest plugin for API coverage tracking."""
|
2
|
+
|
3
|
+
import importlib
|
4
|
+
import importlib.util
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
from typing import Any, Optional
|
8
|
+
|
9
|
+
import pytest
|
10
|
+
|
11
|
+
from .config import get_pytest_api_cov_report_config
|
12
|
+
from .frameworks import get_framework_adapter
|
13
|
+
from .models import SessionData
|
14
|
+
from .pytest_flags import add_pytest_api_cov_flags
|
15
|
+
from .report import generate_pytest_api_cov_report
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
def is_supported_framework(app: Any) -> bool:
|
21
|
+
"""Check if the app is a supported framework (Flask or FastAPI)."""
|
22
|
+
if app is None:
|
23
|
+
return False
|
24
|
+
|
25
|
+
app_type = type(app).__name__
|
26
|
+
module_name = getattr(type(app), "__module__", "").split(".")[0]
|
27
|
+
|
28
|
+
return (module_name == "flask" and app_type == "Flask") or (module_name == "fastapi" and app_type == "FastAPI")
|
29
|
+
|
30
|
+
|
31
|
+
def auto_discover_app() -> Optional[Any]:
|
32
|
+
"""Automatically discover Flask/FastAPI apps in common locations."""
|
33
|
+
logger.debug("> Auto-discovering app in common locations...")
|
34
|
+
|
35
|
+
common_patterns = [
|
36
|
+
("app.py", ["app", "application", "main"]),
|
37
|
+
("main.py", ["app", "application", "main"]),
|
38
|
+
("server.py", ["app", "application", "server"]),
|
39
|
+
("wsgi.py", ["app", "application"]),
|
40
|
+
("asgi.py", ["app", "application"]),
|
41
|
+
]
|
42
|
+
|
43
|
+
for filename, attr_names in common_patterns:
|
44
|
+
if os.path.exists(filename):
|
45
|
+
logger.debug(f"> Found {filename}, checking for app variables...")
|
46
|
+
try:
|
47
|
+
module_name = filename[:-3] # .py extension
|
48
|
+
spec = importlib.util.spec_from_file_location(module_name, filename)
|
49
|
+
if spec and spec.loader:
|
50
|
+
module = importlib.util.module_from_spec(spec)
|
51
|
+
spec.loader.exec_module(module)
|
52
|
+
|
53
|
+
for attr_name in attr_names:
|
54
|
+
if hasattr(module, attr_name):
|
55
|
+
app = getattr(module, attr_name)
|
56
|
+
if is_supported_framework(app):
|
57
|
+
logger.info(
|
58
|
+
f"✅ Auto-discovered {type(app).__name__} app in {filename} as '{attr_name}'"
|
59
|
+
)
|
60
|
+
return app
|
61
|
+
else:
|
62
|
+
logger.debug(f"> Found '{attr_name}' in {filename} but it's not a supported framework")
|
63
|
+
|
64
|
+
except Exception as e:
|
65
|
+
logger.debug(f"> Could not import {filename}: {e}")
|
66
|
+
continue
|
67
|
+
|
68
|
+
logger.debug("> No app auto-discovered")
|
69
|
+
return None
|
70
|
+
|
71
|
+
|
72
|
+
def get_helpful_error_message() -> str:
|
73
|
+
"""Generate a helpful error message for setup guidance."""
|
74
|
+
return """
|
75
|
+
🚫 No API app found!
|
76
|
+
|
77
|
+
Quick Setup Options:
|
78
|
+
|
79
|
+
Option 1 - Auto-discovery (Recommended):
|
80
|
+
Place your FastAPI/Flask app in one of these files:
|
81
|
+
• app.py (with variable named 'app', 'application', or 'main')
|
82
|
+
• main.py (with variable named 'app', 'application', or 'main')
|
83
|
+
• server.py (with variable named 'app', 'application', or 'server')
|
84
|
+
|
85
|
+
Example app.py:
|
86
|
+
from fastapi import FastAPI
|
87
|
+
app = FastAPI() # <- Plugin will auto-discover this
|
88
|
+
|
89
|
+
Option 2 - Manual fixture:
|
90
|
+
Create conftest.py with:
|
91
|
+
|
92
|
+
import pytest
|
93
|
+
from your_module import your_app
|
94
|
+
|
95
|
+
@pytest.fixture
|
96
|
+
def app():
|
97
|
+
return your_app
|
98
|
+
|
99
|
+
Then run: pytest --api-cov-report
|
100
|
+
|
101
|
+
Need help? Run: pytest-api-cov init (for setup wizard)
|
102
|
+
"""
|
103
|
+
|
104
|
+
|
105
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
106
|
+
"""Add API coverage flags to the pytest parser."""
|
107
|
+
add_pytest_api_cov_flags(parser)
|
108
|
+
|
109
|
+
|
110
|
+
def pytest_configure(config: pytest.Config) -> None:
|
111
|
+
"""Configure the pytest session and logging."""
|
112
|
+
if config.getoption("--api-cov-report"):
|
113
|
+
verbosity = config.option.verbose
|
114
|
+
|
115
|
+
if verbosity >= 2: # -vv or more
|
116
|
+
log_level = logging.DEBUG
|
117
|
+
elif verbosity >= 1: # -v
|
118
|
+
log_level = logging.INFO
|
119
|
+
else:
|
120
|
+
log_level = logging.WARNING
|
121
|
+
|
122
|
+
logger.setLevel(log_level)
|
123
|
+
|
124
|
+
if not logger.handlers:
|
125
|
+
handler = logging.StreamHandler()
|
126
|
+
handler.setLevel(log_level)
|
127
|
+
formatter = logging.Formatter("%(message)s")
|
128
|
+
handler.setFormatter(formatter)
|
129
|
+
logger.addHandler(handler)
|
130
|
+
|
131
|
+
logger.info("Initializing API coverage plugin...")
|
132
|
+
|
133
|
+
if config.pluginmanager.hasplugin("xdist"):
|
134
|
+
config.pluginmanager.register(DeferXdistPlugin(), "defer_xdist_plugin")
|
135
|
+
|
136
|
+
|
137
|
+
def pytest_sessionstart(session: pytest.Session) -> None:
|
138
|
+
"""Initialize the call recorder at the start of the session."""
|
139
|
+
if session.config.getoption("--api-cov-report"):
|
140
|
+
session.api_coverage_data = SessionData() # type: ignore[attr-defined]
|
141
|
+
|
142
|
+
|
143
|
+
@pytest.fixture
|
144
|
+
def client(request: pytest.FixtureRequest) -> Any:
|
145
|
+
"""
|
146
|
+
Smart auto-discovering test client that records API calls for coverage.
|
147
|
+
|
148
|
+
Tries to find an 'app' fixture first, then auto-discovers apps in common locations.
|
149
|
+
"""
|
150
|
+
session = request.node.session
|
151
|
+
|
152
|
+
if not session.config.getoption("--api-cov-report"):
|
153
|
+
pytest.skip("API coverage not enabled. Use --api-cov-report flag.")
|
154
|
+
|
155
|
+
app = None
|
156
|
+
try:
|
157
|
+
app = request.getfixturevalue("app")
|
158
|
+
logger.debug("> Found 'app' fixture")
|
159
|
+
except pytest.FixtureLookupError:
|
160
|
+
logger.debug("> No 'app' fixture found, trying auto-discovery...")
|
161
|
+
app = auto_discover_app()
|
162
|
+
|
163
|
+
if app is None:
|
164
|
+
helpful_msg = get_helpful_error_message()
|
165
|
+
print(helpful_msg)
|
166
|
+
pytest.skip("No API app found. See error message above for setup guidance.")
|
167
|
+
|
168
|
+
if not is_supported_framework(app):
|
169
|
+
pytest.skip(f"Unsupported framework: {type(app).__name__}. pytest-api-coverage supports Flask and FastAPI.")
|
170
|
+
|
171
|
+
try:
|
172
|
+
adapter = get_framework_adapter(app)
|
173
|
+
except TypeError as e:
|
174
|
+
pytest.skip(f"Framework detection failed: {e}")
|
175
|
+
|
176
|
+
coverage_data = getattr(session, "api_coverage_data", None)
|
177
|
+
if coverage_data is None:
|
178
|
+
pytest.skip("API coverage data not initialized. This should not happen.")
|
179
|
+
|
180
|
+
if not coverage_data.discovered_endpoints.endpoints:
|
181
|
+
try:
|
182
|
+
endpoints = adapter.get_endpoints()
|
183
|
+
framework_name = type(app).__name__
|
184
|
+
for endpoint in endpoints:
|
185
|
+
coverage_data.add_discovered_endpoint(endpoint, f"{framework_name.lower()}_adapter")
|
186
|
+
logger.info(f"> pytest-api-coverage: Discovered {len(endpoints)} endpoints.")
|
187
|
+
logger.debug(f"> Discovered endpoints: {endpoints}")
|
188
|
+
except Exception as e:
|
189
|
+
logger.warning(f"> pytest-api-coverage: Could not discover endpoints. Error: {e}")
|
190
|
+
|
191
|
+
client = adapter.get_tracked_client(coverage_data.recorder, request.node.name)
|
192
|
+
yield client
|
193
|
+
|
194
|
+
|
195
|
+
def pytest_sessionfinish(session: pytest.Session) -> None:
|
196
|
+
"""Generate the API coverage report at the end of the session."""
|
197
|
+
if session.config.getoption("--api-cov-report"):
|
198
|
+
coverage_data = getattr(session, "api_coverage_data", None)
|
199
|
+
if coverage_data is None:
|
200
|
+
logger.warning("> No API coverage data found. Plugin may not have been properly initialized.")
|
201
|
+
return
|
202
|
+
|
203
|
+
logger.debug(f"> pytest-api-coverage: Generating report for {len(coverage_data.recorder)} recorded endpoints.")
|
204
|
+
if hasattr(session.config, "workeroutput"):
|
205
|
+
serializable_recorder = coverage_data.recorder.to_serializable()
|
206
|
+
session.config.workeroutput["api_call_recorder"] = serializable_recorder
|
207
|
+
session.config.workeroutput["discovered_endpoints"] = coverage_data.discovered_endpoints.endpoints
|
208
|
+
logger.debug("> Sent API call data and discovered endpoints to master process")
|
209
|
+
else:
|
210
|
+
logger.debug("> No workeroutput found, generating report for master data.")
|
211
|
+
|
212
|
+
worker_recorder_data = getattr(session.config, "worker_api_call_recorder", {})
|
213
|
+
worker_endpoints = getattr(session.config, "worker_discovered_endpoints", [])
|
214
|
+
|
215
|
+
# Merge worker data into session data
|
216
|
+
if worker_recorder_data or worker_endpoints:
|
217
|
+
coverage_data.merge_worker_data(worker_recorder_data, worker_endpoints)
|
218
|
+
logger.debug(f"> Merged worker data: {len(worker_recorder_data)} endpoints")
|
219
|
+
|
220
|
+
logger.debug(f"> Final merged data: {len(coverage_data.recorder)} recorded endpoints")
|
221
|
+
logger.debug(f"> Using discovered endpoints: {coverage_data.discovered_endpoints.endpoints}")
|
222
|
+
|
223
|
+
api_cov_config = get_pytest_api_cov_report_config(session.config)
|
224
|
+
status = generate_pytest_api_cov_report(
|
225
|
+
api_cov_config=api_cov_config,
|
226
|
+
called_data=coverage_data.recorder.calls,
|
227
|
+
discovered_endpoints=coverage_data.discovered_endpoints.endpoints,
|
228
|
+
)
|
229
|
+
if session.exitstatus == 0:
|
230
|
+
session.exitstatus = status
|
231
|
+
|
232
|
+
if hasattr(session, "api_coverage_data"):
|
233
|
+
delattr(session, "api_coverage_data")
|
234
|
+
|
235
|
+
if hasattr(session.config, "worker_api_call_recorder"):
|
236
|
+
delattr(session.config, "worker_api_call_recorder")
|
237
|
+
|
238
|
+
|
239
|
+
class DeferXdistPlugin:
|
240
|
+
"""Simple class to defer pytest-xdist hook until we know it is installed."""
|
241
|
+
|
242
|
+
def pytest_testnodedown(self, node: Any) -> None:
|
243
|
+
"""Collect API call data from each worker as they finish."""
|
244
|
+
logger.debug("> pytest-api-coverage: Worker node down.")
|
245
|
+
worker_data = node.workeroutput.get("api_call_recorder", {})
|
246
|
+
discovered_endpoints = node.workeroutput.get("discovered_endpoints", [])
|
247
|
+
logger.debug(f"> Worker data: {worker_data}")
|
248
|
+
logger.debug(f"> Worker discovered endpoints: {discovered_endpoints}")
|
249
|
+
|
250
|
+
# Merge API call data
|
251
|
+
if worker_data:
|
252
|
+
logger.debug("> Worker data found, merging with current data.")
|
253
|
+
current = getattr(node.config, "worker_api_call_recorder", {})
|
254
|
+
logger.debug(f"> Current data before merge: {current}")
|
255
|
+
|
256
|
+
# Merge the worker data into current
|
257
|
+
for endpoint, calls in worker_data.items():
|
258
|
+
if endpoint not in current:
|
259
|
+
current[endpoint] = set()
|
260
|
+
elif not isinstance(current[endpoint], set):
|
261
|
+
current[endpoint] = set(current[endpoint])
|
262
|
+
current[endpoint].update(calls)
|
263
|
+
logger.debug(f"> Updated endpoint {endpoint} with calls: {calls}")
|
264
|
+
|
265
|
+
node.config.worker_api_call_recorder = current
|
266
|
+
logger.debug(f"> Updated current data: {current}")
|
267
|
+
|
268
|
+
if discovered_endpoints and not getattr(node.config, "worker_discovered_endpoints", []):
|
269
|
+
node.config.worker_discovered_endpoints = discovered_endpoints
|
270
|
+
logger.debug(f"> Set discovered endpoints from worker: {discovered_endpoints}")
|
@@ -0,0 +1,68 @@
|
|
1
|
+
"""pytest flag configuration for API coverage."""
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
import pytest
|
9
|
+
|
10
|
+
|
11
|
+
def add_pytest_api_cov_flags(parser: pytest.Parser) -> None:
|
12
|
+
"""Add API coverage flags to the parser."""
|
13
|
+
parser.addoption(
|
14
|
+
"--api-cov-report",
|
15
|
+
action="store_true",
|
16
|
+
default=False,
|
17
|
+
help="Generate API coverage report.",
|
18
|
+
)
|
19
|
+
parser.addoption(
|
20
|
+
"--api-cov-fail-under",
|
21
|
+
action="store",
|
22
|
+
type=float,
|
23
|
+
default=None,
|
24
|
+
help="Fail if API coverage is below this percentage.",
|
25
|
+
)
|
26
|
+
parser.addoption(
|
27
|
+
"--api-cov-show-uncovered-endpoints",
|
28
|
+
action="store_true",
|
29
|
+
default=True,
|
30
|
+
help="Show uncovered endpoints in the console report.",
|
31
|
+
)
|
32
|
+
parser.addoption(
|
33
|
+
"--api-cov-show-covered-endpoints",
|
34
|
+
action="store_true",
|
35
|
+
default=False,
|
36
|
+
help="Show covered endpoints in the console report.",
|
37
|
+
)
|
38
|
+
parser.addoption(
|
39
|
+
"--api-cov-show-excluded-endpoints",
|
40
|
+
action="store_true",
|
41
|
+
default=False,
|
42
|
+
help="Show excluded endpoints in the console report.",
|
43
|
+
)
|
44
|
+
parser.addoption(
|
45
|
+
"--api-cov-exclusion-patterns",
|
46
|
+
action="append",
|
47
|
+
default=[],
|
48
|
+
help="Patterns for endpoints to exclude from coverage.",
|
49
|
+
)
|
50
|
+
parser.addoption(
|
51
|
+
"--api-cov-report-path",
|
52
|
+
action="store",
|
53
|
+
type=str,
|
54
|
+
default=None,
|
55
|
+
help="Path to save the API coverage report.",
|
56
|
+
)
|
57
|
+
parser.addoption(
|
58
|
+
"--api-cov-force-sugar",
|
59
|
+
action="store_true",
|
60
|
+
default=False,
|
61
|
+
help="Force use of API coverage sugar in console report.",
|
62
|
+
)
|
63
|
+
parser.addoption(
|
64
|
+
"--api-cov-force-sugar-disabled",
|
65
|
+
action="store_true",
|
66
|
+
default=False,
|
67
|
+
help="Disable use of API coverage sugar in console report.",
|
68
|
+
)
|
pytest_api_cov/report.py
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
"""API coverage report generation."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import re
|
5
|
+
from pathlib import Path
|
6
|
+
from re import Pattern
|
7
|
+
from typing import Any, Dict, List, Set, Tuple
|
8
|
+
|
9
|
+
from rich.console import Console
|
10
|
+
|
11
|
+
from .config import ApiCoverageReportConfig
|
12
|
+
|
13
|
+
|
14
|
+
def endpoint_to_regex(endpoint: str) -> Pattern[str]:
|
15
|
+
"""Create a regex pattern from an endpoint by replacing dynamic segments."""
|
16
|
+
placeholder = "___PLACEHOLDER___"
|
17
|
+
temp_endpoint = re.escape(re.sub(r"<[^>]+>|\{[^}]+\}", placeholder, endpoint))
|
18
|
+
return re.compile("^" + temp_endpoint.replace(placeholder, "(.+)") + "$")
|
19
|
+
|
20
|
+
|
21
|
+
def contains_escape_characters(endpoint: str) -> bool:
|
22
|
+
"""Escape special characters in the endpoint string."""
|
23
|
+
return ("<" in endpoint and ">" in endpoint) or ("{" in endpoint and "}" in endpoint)
|
24
|
+
|
25
|
+
|
26
|
+
def categorise_endpoints(
|
27
|
+
endpoints: List[str],
|
28
|
+
called_data: Dict[str, Set[str]],
|
29
|
+
exclusion_patterns: List[str],
|
30
|
+
) -> Tuple[List[str], List[str], List[str]]:
|
31
|
+
"""Categorise endpoints into covered, uncovered, and excluded.
|
32
|
+
|
33
|
+
Exclusion patterns support simple wildcard matching:
|
34
|
+
- Use * for wildcard (matches any characters)
|
35
|
+
- All other characters are matched literally
|
36
|
+
- Examples: "/admin/*", "/health", "/docs/*"
|
37
|
+
"""
|
38
|
+
covered, uncovered, excluded = [], [], []
|
39
|
+
|
40
|
+
compiled_patterns = (
|
41
|
+
[re.compile("^" + re.escape(pattern).replace(r"\*", ".*") + "$") for pattern in exclusion_patterns]
|
42
|
+
if exclusion_patterns
|
43
|
+
else None
|
44
|
+
)
|
45
|
+
|
46
|
+
for endpoint in endpoints:
|
47
|
+
if compiled_patterns and any(p.match(endpoint) for p in compiled_patterns):
|
48
|
+
excluded.append(endpoint)
|
49
|
+
continue
|
50
|
+
elif contains_escape_characters(endpoint):
|
51
|
+
pattern = endpoint_to_regex(endpoint)
|
52
|
+
is_covered = any(pattern.match(ep) for ep in called_data)
|
53
|
+
else:
|
54
|
+
is_covered = endpoint in called_data
|
55
|
+
covered.append(endpoint) if is_covered else uncovered.append(endpoint)
|
56
|
+
return covered, uncovered, excluded
|
57
|
+
|
58
|
+
|
59
|
+
def print_endpoints(
|
60
|
+
console: Console,
|
61
|
+
label: str,
|
62
|
+
endpoints: List[str],
|
63
|
+
symbol: str,
|
64
|
+
style: str,
|
65
|
+
) -> None:
|
66
|
+
"""Prints a list of endpoints to the console with a label and style."""
|
67
|
+
if endpoints:
|
68
|
+
console.print(f"[{style}]{label}[/]:")
|
69
|
+
for endpoint in endpoints:
|
70
|
+
console.print(f" {symbol} [{style}]{endpoint}[/]")
|
71
|
+
|
72
|
+
|
73
|
+
def compute_coverage(covered_count: int, uncovered_count: int) -> float:
|
74
|
+
"""Compute API coverage percentage."""
|
75
|
+
total = covered_count + uncovered_count
|
76
|
+
return round(100 * covered_count / total, 2) if total > 0 else 0.0
|
77
|
+
|
78
|
+
|
79
|
+
def prepare_endpoint_detail(endpoints: List[str], called_data: Dict[str, Set[str]]) -> List[Dict[str, Any]]:
|
80
|
+
"""Prepare endpoint details by mapping each endpoint to its callers."""
|
81
|
+
details = []
|
82
|
+
for endpoint in endpoints:
|
83
|
+
if contains_escape_characters(endpoint):
|
84
|
+
pattern = endpoint_to_regex(endpoint)
|
85
|
+
callers = set()
|
86
|
+
for call in called_data:
|
87
|
+
if pattern.match(call):
|
88
|
+
callers.update(called_data[call])
|
89
|
+
else:
|
90
|
+
callers = called_data.get(endpoint, set())
|
91
|
+
details.append({"endpoint": endpoint, "callers": sorted(list(callers))})
|
92
|
+
return sorted(details, key=lambda x: len(x["callers"]))
|
93
|
+
|
94
|
+
|
95
|
+
def write_report_file(report_data: Dict[str, Any], report_path: str) -> None:
|
96
|
+
"""Write the report data to a JSON file."""
|
97
|
+
path = Path(report_path).resolve()
|
98
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
99
|
+
with open(path, "w") as f:
|
100
|
+
json.dump(report_data, f, indent=2)
|
101
|
+
|
102
|
+
|
103
|
+
def generate_pytest_api_cov_report(
|
104
|
+
api_cov_config: ApiCoverageReportConfig,
|
105
|
+
called_data: Dict[str, Set[str]],
|
106
|
+
discovered_endpoints: List[str],
|
107
|
+
) -> int:
|
108
|
+
"""Generate and print the API coverage report, returning an exit status."""
|
109
|
+
|
110
|
+
console = Console()
|
111
|
+
|
112
|
+
if not discovered_endpoints:
|
113
|
+
console.print("\n[bold red]No endpoints discovered. Please check your test setup.[/bold red]")
|
114
|
+
return 0
|
115
|
+
|
116
|
+
separator = "=" * 20
|
117
|
+
console.print(f"\n\n[bold blue]{separator} API Coverage Report {separator}[/bold blue]")
|
118
|
+
|
119
|
+
covered, uncovered, excluded = categorise_endpoints(
|
120
|
+
discovered_endpoints,
|
121
|
+
called_data,
|
122
|
+
api_cov_config.exclusion_patterns,
|
123
|
+
)
|
124
|
+
|
125
|
+
if api_cov_config.show_uncovered_endpoints:
|
126
|
+
print_endpoints(
|
127
|
+
console,
|
128
|
+
"Uncovered Endpoints",
|
129
|
+
uncovered,
|
130
|
+
"❌" if api_cov_config.force_sugar else "[X]",
|
131
|
+
"red",
|
132
|
+
)
|
133
|
+
|
134
|
+
if api_cov_config.show_covered_endpoints:
|
135
|
+
print_endpoints(
|
136
|
+
console,
|
137
|
+
"Covered Endpoints",
|
138
|
+
covered,
|
139
|
+
"✅" if api_cov_config.force_sugar else "[.]",
|
140
|
+
"green",
|
141
|
+
)
|
142
|
+
|
143
|
+
if api_cov_config.show_excluded_endpoints:
|
144
|
+
print_endpoints(
|
145
|
+
console,
|
146
|
+
label="Excluded Endpoints",
|
147
|
+
endpoints=excluded,
|
148
|
+
symbol="🚫" if api_cov_config.force_sugar else "[-]",
|
149
|
+
style="grey50",
|
150
|
+
)
|
151
|
+
|
152
|
+
coverage = compute_coverage(len(covered), len(uncovered))
|
153
|
+
status = 0
|
154
|
+
|
155
|
+
if api_cov_config.fail_under is None:
|
156
|
+
console.print(f"\n[bold green]Total API Coverage: {coverage}%[/bold green]")
|
157
|
+
elif coverage < api_cov_config.fail_under:
|
158
|
+
console.print(
|
159
|
+
f"\n[bold red]FAIL: Required coverage of {api_cov_config.fail_under}% not met. "
|
160
|
+
f"Actual coverage: {coverage}%[/bold red]"
|
161
|
+
)
|
162
|
+
status = 1
|
163
|
+
else:
|
164
|
+
console.print(
|
165
|
+
f"\n[bold green]SUCCESS: Coverage of {coverage}% meets requirement of "
|
166
|
+
f"{api_cov_config.fail_under}%[/bold green]"
|
167
|
+
)
|
168
|
+
|
169
|
+
if api_cov_config.report_path:
|
170
|
+
detail = prepare_endpoint_detail(covered + uncovered, called_data)
|
171
|
+
final_report = {
|
172
|
+
"status": status,
|
173
|
+
"coverage": coverage,
|
174
|
+
"required_coverage": api_cov_config.fail_under,
|
175
|
+
"total_endpoints": len(covered) + len(uncovered),
|
176
|
+
"covered_count": len(covered),
|
177
|
+
"uncovered_count": len(uncovered),
|
178
|
+
"excluded_count": len(excluded),
|
179
|
+
"detail": detail,
|
180
|
+
}
|
181
|
+
write_report_file(final_report, api_cov_config.report_path)
|
182
|
+
console.print(f"\n[grey50]JSON report saved to {api_cov_config.report_path}[/grey50]")
|
183
|
+
|
184
|
+
console.print(f"[bold blue]{'=' * (42 + len(' API Coverage Report '))}[/bold blue]\n")
|
185
|
+
return status
|