Files
litellm/tests/test_litellm/proxy/proxy_server/_coverage_check.py
T
yuneng-jiang f38c16c71e test(proxy): add harness for proxy_server.py behavior-pinning (#28827)
* test(proxy): add harness for proxy_server.py behavior-pinning

Creates tests/test_litellm/proxy/proxy_server/ with:
- conftest.py: 11 shared fixtures (app, client, mock_prisma, auth_as,
  mock_router with parametrized response builders, normalize, etc.)
- _coverage_check.py: per-PR coverage gate (line + branch) against a
  baseline, self-selects target by inspecting which placeholder files
  have been filled
- _pin_check.py: AST-based gate that verifies every pin-list item has
  >=1 happy + >=1 error test with a real assertion (no status-only)
- test_harness_smoke.py: 19 smoke tests covering every fixture +
  both scripts end-to-end
- 26 placeholder test files (one docstring each) reserved for
  follow-up PRs per the directory ownership in the Notion plan
- .coverage_baseline pinned at 0% so future PRs measure deltas
  against new-tests-only and aren't entangled with the broader
  scattered test suite

Adds a dedicated proxy-server job to test-unit-proxy-endpoints.yml
so this directory's runtime + coverage are tracked independently.

Plan: https://www.notion.so/36c43b8acdab81ee845fd5365128a2fc

* ci(proxy-endpoints): allow workflow_dispatch

Lets the workflow be triggered manually on a branch via
`gh workflow run`, which is needed for the verify-first
flow on workflow changes before opening a PR.

* test(proxy): address review feedback on proxy_server harness

- conftest.py: anchor sys.path insert to __file__ (Path(__file__).resolve().parents[4])
  instead of CWD-relative os.path.abspath("../../../../") which resolved
  to the wrong directory when pytest is launched from the repo root.
- _coverage_check.py: actually read .coverage_baseline and use it as
  the floor (line_min = max(target, baseline)). Closes the gap between
  the PR description's "delta semantics" and what the script was doing.
  With baseline=0.0 today this is a no-op; future PRs that update the
  baseline cause regressions (test deletions etc.) to trip the gate
  even if the static PR target is still met.
- _pin_check.py: drop unreachable startswith("_") guard
  (test_*.py glob never yields underscore-prefixed names) and read
  each test file once instead of twice.
2026-05-25 20:26:44 -07:00

202 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Coverage gate for the proxy_server.py behavior-pinning project.
Reads a coverage XML report (produced by ``pytest --cov-branch
--cov-report=xml:<path>``) and asserts that line + branch coverage on
``litellm/proxy/proxy_server.py`` meets the per-PR target.
Target selection:
--pr-target {1|2|3} explicit target
(none) self-selected by inspecting which placeholder
test files have been filled (PR1 fills before
PR2, PR2 before PR3). With nothing filled, the
target is "PR0" (baseline, no minimum).
Exits 0 on PASS, non-zero on FAIL.
"""
from __future__ import annotations
import argparse
import ast
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Dict, List, Tuple
HERE = Path(__file__).resolve().parent
SOURCE_FILE = "litellm/proxy/proxy_server.py"
# PR target gates: (line%, branch%)
TARGETS: Dict[str, Tuple[float, float]] = {
"PR0": (0.0, 0.0),
"PR1": (25.0, 18.0),
"PR2": (50.0, 38.0),
"PR3": (70.0, 55.0),
}
# Which placeholder files each PR is expected to fill (see Notion plan).
PR1_FILES: List[str] = [
"test_lifecycle.py",
"test_proxy_config.py",
"test_spend_counters.py",
"test_background_health.py",
"test_openapi_customization.py",
"test_exception_handlers.py",
"test_streaming_helpers.py",
]
PR2_FILES: List[str] = [
"test_routes_models.py",
"test_routes_chat_completions.py",
"test_routes_completions.py",
"test_routes_embeddings.py",
"test_routes_moderations.py",
"test_routes_audio.py",
"test_routes_assistants.py",
"test_routes_threads.py",
"test_routes_utils.py",
"test_routes_model_info.py",
"test_routes_model_metrics.py",
"test_routes_queue.py",
]
PR3_FILES: List[str] = [
"test_routes_login_sso.py",
"test_routes_onboarding.py",
"test_routes_invitation.py",
"test_routes_config.py",
"test_routes_model_cost_map.py",
"test_routes_anthropic_beta.py",
"test_routes_misc.py",
]
def file_has_tests(path: Path) -> bool:
"""A test file is considered filled if it defines at least one ``test_*``."""
if not path.is_file():
return False
try:
tree = ast.parse(path.read_text())
except SyntaxError:
return False
for node in ast.walk(tree):
if isinstance(
node, (ast.FunctionDef, ast.AsyncFunctionDef)
) and node.name.startswith("test_"):
return True
return False
def detect_pr_target(dir_path: Path) -> str:
"""Pick the strictest PR whose files are fully filled in this directory."""
pr3_filled = all(file_has_tests(dir_path / f) for f in PR3_FILES)
pr2_filled = all(file_has_tests(dir_path / f) for f in PR2_FILES)
pr1_filled = all(file_has_tests(dir_path / f) for f in PR1_FILES)
if pr3_filled and pr2_filled and pr1_filled:
return "PR3"
if pr2_filled and pr1_filled:
return "PR2"
if pr1_filled:
return "PR1"
return "PR0"
def parse_coverage_xml(xml_path: Path) -> Tuple[float, float]:
"""Extract (line%, branch%) for proxy_server.py from a coverage XML report.
Returns (0.0, 0.0) if the file isn't found in the report.
"""
if not xml_path.is_file():
raise FileNotFoundError(f"Coverage XML not found at {xml_path}")
tree = ET.parse(xml_path)
root = tree.getroot()
for class_elem in root.iter("class"):
filename = class_elem.get("filename", "")
# Coverage tools emit either a repo-relative path or just the basename
# depending on configuration. Match by suffix.
if filename.endswith("proxy/proxy_server.py") or filename.endswith(
"proxy_server.py"
):
line_rate = float(class_elem.get("line-rate", "0"))
branch_rate = float(class_elem.get("branch-rate", "0"))
return line_rate * 100.0, branch_rate * 100.0
return 0.0, 0.0
def parse_baseline(baseline_path: Path) -> Tuple[float, float]:
"""Parse ``line:<float> branch:<float>`` baseline; missing file -> (0, 0)."""
if not baseline_path.is_file():
return 0.0, 0.0
line_pct = 0.0
branch_pct = 0.0
for token in baseline_path.read_text().split():
if ":" not in token:
continue
key, _, value = token.partition(":")
try:
num = float(value)
except ValueError:
continue
if key == "line":
line_pct = num
elif key == "branch":
branch_pct = num
return line_pct, branch_pct
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--pr-target",
choices=["1", "2", "3"],
default=None,
help="Explicit PR target (1, 2, or 3). If omitted, self-selected.",
)
parser.add_argument(
"--coverage-xml",
default=str(HERE.parent.parent.parent.parent / ".cov_new.xml"),
help="Path to coverage XML (default: <repo>/.cov_new.xml)",
)
args = parser.parse_args()
if args.pr_target:
target = f"PR{args.pr_target}"
else:
target = detect_pr_target(HERE)
target_line, target_branch = TARGETS[target]
# The effective floor is the max of the PR target and the committed
# baseline. The baseline is updated as each PR lands so a future
# regression (e.g. a test deletion) trips this gate even if the
# static PR target is already met.
baseline_line, baseline_branch = parse_baseline(HERE / ".coverage_baseline")
line_min = max(target_line, baseline_line)
branch_min = max(target_branch, baseline_branch)
xml_path = Path(args.coverage_xml)
try:
line_pct, branch_pct = parse_coverage_xml(xml_path)
except FileNotFoundError as exc:
print(f"FAIL: {exc}", file=sys.stderr)
return 2
line_ok = line_pct >= line_min
branch_ok = branch_pct >= branch_min
status = "PASS" if (line_ok and branch_ok) else "FAIL"
print(
f"target={target} baseline=(line:{baseline_line:.2f} branch:{baseline_branch:.2f})"
)
print(
f"line: {line_pct:6.2f}% / {line_min:6.2f}% " f"{'OK' if line_ok else 'MISS'}"
)
print(
f"branch: {branch_pct:6.2f}% / {branch_min:6.2f}% "
f"{'OK' if branch_ok else 'MISS'}"
)
print(status)
return 0 if status == "PASS" else 1
if __name__ == "__main__":
sys.exit(main())