Source code for pytest_routes.reporting.route_coverage

"""Route coverage metrics for pytest-routes."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from pytest_routes.discovery.base import RouteInfo


[docs] @dataclass class RouteCoverage: """Coverage information for a single route. Attributes: route_path: The route path. method: HTTP method. tested: Whether this route was tested. test_count: Number of tests run against this route. status_codes_seen: Status codes returned during testing. parameters_tested: Parameter names that were tested. body_tested: Whether request body was tested. """ route_path: str method: str tested: bool = False test_count: int = 0 status_codes_seen: set[int] = field(default_factory=set) parameters_tested: set[str] = field(default_factory=set) body_tested: bool = False @property def coverage_score(self) -> float: """Calculate coverage score (0-100).""" if not self.tested: return 0.0 score = 50.0 if len(self.status_codes_seen) > 1: score += 20.0 if self.parameters_tested: score += 15.0 if self.body_tested: score += 15.0 return min(score, 100.0)
[docs] def mark_tested( self, status_code: int, parameters: set[str] | None = None, *, has_body: bool = False, ) -> None: """Mark this route as tested. Args: status_code: Status code returned. parameters: Parameters that were tested. has_body: Whether request body was included. """ self.tested = True self.test_count += 1 self.status_codes_seen.add(status_code) if parameters: self.parameters_tested.update(parameters) if has_body: self.body_tested = True
[docs] def to_dict(self) -> dict[str, Any]: """Convert to dictionary for serialization.""" return { "route_path": self.route_path, "method": self.method, "tested": self.tested, "test_count": self.test_count, "status_codes_seen": sorted(self.status_codes_seen), "parameters_tested": sorted(self.parameters_tested), "body_tested": self.body_tested, "coverage_score": round(self.coverage_score, 1), }
[docs] @dataclass class CoverageMetrics: """Aggregate coverage metrics across all routes. Attributes: total_routes: Total number of routes in the application. tested_routes: Number of routes that were tested. untested_routes: List of routes that were not tested. route_coverage: Coverage information per route. """ total_routes: int = 0 route_coverage: dict[str, RouteCoverage] = field(default_factory=dict) @property def tested_routes(self) -> int: """Number of routes tested.""" return sum(1 for rc in self.route_coverage.values() if rc.tested) @property def untested_routes(self) -> list[str]: """List of routes that were not tested.""" return [f"{rc.method} {rc.route_path}" for rc in self.route_coverage.values() if not rc.tested] @property def coverage_percentage(self) -> float: """Overall route coverage as a percentage.""" if self.total_routes == 0: return 0.0 return (self.tested_routes / self.total_routes) * 100 @property def average_coverage_score(self) -> float: """Average coverage score across all tested routes.""" tested = [rc for rc in self.route_coverage.values() if rc.tested] if not tested: return 0.0 return sum(rc.coverage_score for rc in tested) / len(tested)
[docs] def add_route(self, route: RouteInfo) -> RouteCoverage: """Add a route to track coverage. Args: route: The route to track. Returns: RouteCoverage instance for the route. """ key = f"{route.methods[0]}:{route.path}" if key not in self.route_coverage: self.route_coverage[key] = RouteCoverage( route_path=route.path, method=route.methods[0], ) self.total_routes += 1 return self.route_coverage[key]
[docs] def get_route_coverage(self, route: RouteInfo) -> RouteCoverage | None: """Get coverage for a specific route. Args: route: The route to get coverage for. Returns: RouteCoverage or None if not tracked. """ key = f"{route.methods[0]}:{route.path}" return self.route_coverage.get(key)
[docs] def to_dict(self) -> dict[str, Any]: """Convert to dictionary for serialization.""" return { "total_routes": self.total_routes, "tested_routes": self.tested_routes, "untested_routes": self.untested_routes, "coverage_percentage": round(self.coverage_percentage, 1), "average_coverage_score": round(self.average_coverage_score, 1), "routes": {k: v.to_dict() for k, v in self.route_coverage.items()}, }
[docs] def calculate_coverage( all_routes: list[RouteInfo], tested_routes: list[RouteInfo], ) -> CoverageMetrics: """Calculate coverage metrics. Args: all_routes: All routes in the application. tested_routes: Routes that were actually tested. Returns: CoverageMetrics with coverage information. """ metrics = CoverageMetrics() for route in all_routes: metrics.add_route(route) tested_keys = {f"{r.methods[0]}:{r.path}" for r in tested_routes} for key in tested_keys: if key in metrics.route_coverage: metrics.route_coverage[key].tested = True return metrics