mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
The collector read @pytest.mark.covers off every collected item, and collection does not evaluate skips, so a test carrying both a skip and a covers marker reported its cell as covered while asserting nothing. 17 files under tests/e2e do exactly that, which inflated the headline from 290/434 to 311/434. A cell now counts as covered only when at least one test pytest would actually run declares it; a cell claimed by both a live and a skipped test stays covered. Skip state comes from pytest's own evaluator, so skip, skipif (bool and string conditions), and module-level pytestmark resolve exactly as they do in the e2e run. Cells left uncovered this way are listed under the headline and exported as skipped_markers (JSON) and litellm_e2e_coverage_skipped_markers (Prometheus) so the gap surfaces instead of disappearing; the Loki line contract is unchanged. A marker on a skipped test that points outside the registry is still an orphan, so --strict keeps its reach. Because skipif resolves against the environment the collector runs in, the number now depends on that environment; run it where the e2e suite runs. A pytest.skip() call inside a test body remains invisible to a static pass, which the module docstring and README both state.
372 lines
12 KiB
Python
372 lines
12 KiB
Python
"""Diff the registry (denominator) against the @pytest.mark.covers markers on the
|
|
live tests (numerator) and report coverage per module.
|
|
|
|
Coverage here is static: it reads the markers via a collect-only pass, so it runs
|
|
no test and needs no live proxy. Whether a covered cell currently passes or fails
|
|
(covered_pass vs covered_fail) is a separate, live concern layered on top later.
|
|
|
|
A skipped test asserts nothing, so its markers do not count: a cell is covered
|
|
only when at least one test that pytest would actually run declares it. Skip
|
|
state is read with pytest's own evaluator, so `skip` and `skipif` are resolved
|
|
exactly as the e2e run resolves them in this environment. The one skip the
|
|
collector cannot see is `pytest.skip()` called from inside a test body, which
|
|
does not exist until the test runs.
|
|
|
|
cd tests/e2e && PYTHONPATH=. python -m coverage_registry.collector
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import sys
|
|
from argparse import ArgumentParser
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
import pytest
|
|
from _pytest.skipping import evaluate_skip_marks
|
|
from pydantic import BaseModel
|
|
|
|
from .registry import load_registry
|
|
from .schema import MODULE_ORDER, Cell, Tier, dashboard_module, loki_module_label
|
|
|
|
E2E_DIR = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CollectedMarkers:
|
|
"""What a collect-only pass saw: cell ids declared by tests that would run,
|
|
cell ids only ever declared by skipped tests, and nodes that failed to import."""
|
|
|
|
covered: frozenset[str]
|
|
skipped_only: frozenset[str]
|
|
collection_errors: tuple[str, ...]
|
|
|
|
|
|
def _is_skipped(item: pytest.Item) -> bool:
|
|
"""True when pytest would skip this test instead of running it.
|
|
|
|
A marker pytest cannot evaluate (for example a bare boolean `skipif` with no
|
|
reason) turns into a setup failure at run time, so the test asserts nothing
|
|
either way and is treated the same as a skip.
|
|
"""
|
|
try:
|
|
return evaluate_skip_marks(item) is not None
|
|
except (pytest.fail.Exception, TypeError):
|
|
return True
|
|
|
|
|
|
class _CoversSink:
|
|
"""Pytest plugin: after collection, capture every cell id declared via
|
|
@pytest.mark.covers(...) split by whether its test would run, plus any nodes
|
|
that failed to import."""
|
|
|
|
def __init__(self) -> None:
|
|
self.covered_ids: frozenset[str] = frozenset()
|
|
self.skipped_only_ids: frozenset[str] = frozenset()
|
|
self.collection_errors: tuple[str, ...] = ()
|
|
|
|
def pytest_collection_finish(self, session: pytest.Session) -> None:
|
|
marker_args: tuple[tuple[bool, tuple[object, ...]], ...] = tuple(
|
|
(skipped, marker.args)
|
|
for item, skipped in ((i, _is_skipped(i)) for i in session.items)
|
|
for marker in item.iter_markers(name="covers")
|
|
)
|
|
declared = tuple(
|
|
(skipped, arg)
|
|
for skipped, args in marker_args
|
|
for arg in args
|
|
if isinstance(arg, str)
|
|
)
|
|
self.covered_ids = frozenset(
|
|
cell_id for skipped, cell_id in declared if not skipped
|
|
)
|
|
self.skipped_only_ids = (
|
|
frozenset(cell_id for skipped, cell_id in declared if skipped)
|
|
- self.covered_ids
|
|
)
|
|
|
|
def pytest_collectreport(self, report: pytest.CollectReport) -> None:
|
|
if report.failed:
|
|
self.collection_errors = (*self.collection_errors, report.nodeid)
|
|
|
|
|
|
def collect_markers(e2e_dir: Path = E2E_DIR) -> CollectedMarkers:
|
|
"""Read every @pytest.mark.covers marker in `e2e_dir` via a collect-only pass."""
|
|
sink = _CoversSink()
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
pytest.main(
|
|
[
|
|
"--collect-only",
|
|
"-qq",
|
|
"--continue-on-collection-errors",
|
|
"-p",
|
|
"no:cacheprovider",
|
|
str(e2e_dir),
|
|
],
|
|
plugins=[sink],
|
|
)
|
|
return CollectedMarkers(
|
|
covered=sink.covered_ids,
|
|
skipped_only=sink.skipped_only_ids,
|
|
collection_errors=sink.collection_errors,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ModuleCoverage:
|
|
module: str
|
|
total: int
|
|
covered: int
|
|
p0_total: int
|
|
p0_covered: int
|
|
|
|
@property
|
|
def coverage_percent(self) -> float:
|
|
return _percent(self.covered, self.total)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CoverageReport:
|
|
modules: tuple[ModuleCoverage, ...]
|
|
total: int
|
|
covered: int
|
|
p0_total: int
|
|
p0_covered: int
|
|
p0_gaps: tuple[str, ...]
|
|
orphan_markers: tuple[str, ...]
|
|
skipped_markers: tuple[str, ...]
|
|
collection_errors: tuple[str, ...]
|
|
|
|
@property
|
|
def coverage_percent(self) -> float:
|
|
return _percent(self.covered, self.total)
|
|
|
|
|
|
def _percent(covered: int, total: int) -> float:
|
|
return (100.0 * covered / total) if total else 0.0
|
|
|
|
|
|
def _module_coverage(
|
|
module: str, cells: tuple[Cell, ...], covered: frozenset[str]
|
|
) -> ModuleCoverage:
|
|
in_module = tuple(c for c in cells if dashboard_module(c) == module)
|
|
p0 = tuple(c for c in in_module if c.tier is Tier.P0)
|
|
return ModuleCoverage(
|
|
module=module,
|
|
total=len(in_module),
|
|
covered=sum(1 for c in in_module if c.id in covered),
|
|
p0_total=len(p0),
|
|
p0_covered=sum(1 for c in p0 if c.id in covered),
|
|
)
|
|
|
|
|
|
def compute_coverage(
|
|
cells: tuple[Cell, ...],
|
|
covered: frozenset[str],
|
|
collection_errors: tuple[str, ...] = (),
|
|
skipped_only: frozenset[str] = frozenset(),
|
|
) -> CoverageReport:
|
|
p0_cells = tuple(c for c in cells if c.tier is Tier.P0)
|
|
registry_ids = frozenset(c.id for c in cells)
|
|
return CoverageReport(
|
|
modules=tuple(_module_coverage(m, cells, covered) for m in MODULE_ORDER),
|
|
total=len(cells),
|
|
covered=sum(1 for c in cells if c.id in covered),
|
|
p0_total=len(p0_cells),
|
|
p0_covered=sum(1 for c in p0_cells if c.id in covered),
|
|
p0_gaps=tuple(sorted(c.id for c in p0_cells if c.id not in covered)),
|
|
orphan_markers=tuple(sorted((covered | skipped_only) - registry_ids)),
|
|
skipped_markers=tuple(sorted(skipped_only & registry_ids)),
|
|
collection_errors=collection_errors,
|
|
)
|
|
|
|
|
|
def _row(label: str, covered: int, total: int) -> str:
|
|
frac = f"{covered}/{total}"
|
|
return f"{label:30}{frac:>12}{_percent(covered, total):>11.1f}%"
|
|
|
|
|
|
def render(report: CoverageReport) -> str:
|
|
rows = tuple(_row(m.module, m.covered, m.total) for m in report.modules)
|
|
lines = (
|
|
f"{'MODULE':30}{'COVERED':>12}{'COVERAGE':>12}",
|
|
*rows,
|
|
"-" * 54,
|
|
_row("ALL", report.covered, report.total),
|
|
"",
|
|
f"Headline coverage: {report.covered}/{report.total} ({report.coverage_percent:.1f}%)",
|
|
)
|
|
orphans = (
|
|
(
|
|
f"\n{len(report.orphan_markers)} marker(s) point at ids not in the registry "
|
|
f"(reconcile: fix the marker or add the cell):\n "
|
|
+ "\n ".join(report.orphan_markers),
|
|
)
|
|
if report.orphan_markers
|
|
else ()
|
|
)
|
|
skipped = (
|
|
(
|
|
f"\n{len(report.skipped_markers)} cell(s) are claimed only by skipped tests, "
|
|
f"so they count as uncovered (unskip the test or drop the marker):\n "
|
|
+ "\n ".join(report.skipped_markers),
|
|
)
|
|
if report.skipped_markers
|
|
else ()
|
|
)
|
|
warning = (
|
|
(
|
|
f"\nWARNING: {len(report.collection_errors)} node(s) failed to import during "
|
|
f"collection, so coverage may undercount:\n "
|
|
+ "\n ".join(report.collection_errors),
|
|
)
|
|
if report.collection_errors
|
|
else ()
|
|
)
|
|
return "\n".join((*lines, *orphans, *skipped, *warning))
|
|
|
|
|
|
def _report_dict(report: CoverageReport) -> dict[str, object]:
|
|
return {
|
|
"covered": report.covered,
|
|
"total": report.total,
|
|
"coverage_percent": report.coverage_percent,
|
|
"modules": [
|
|
{
|
|
"module": m.module,
|
|
"covered": m.covered,
|
|
"total": m.total,
|
|
"coverage_percent": m.coverage_percent,
|
|
"p0_covered": m.p0_covered,
|
|
"p0_total": m.p0_total,
|
|
}
|
|
for m in report.modules
|
|
],
|
|
"orphan_markers": list(report.orphan_markers),
|
|
"skipped_markers": list(report.skipped_markers),
|
|
"collection_errors": list(report.collection_errors),
|
|
}
|
|
|
|
|
|
def render_json(report: CoverageReport) -> str:
|
|
return json.dumps(_report_dict(report), indent=2, sort_keys=True)
|
|
|
|
|
|
def _label_value(value: str) -> str:
|
|
return value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
|
|
|
|
def render_prometheus(report: CoverageReport) -> str:
|
|
lines = [
|
|
"# HELP litellm_e2e_coverage_cells E2E coverage registry cells by module and state.",
|
|
"# TYPE litellm_e2e_coverage_cells gauge",
|
|
]
|
|
for module in report.modules:
|
|
label = _label_value(module.module)
|
|
lines.append(
|
|
f'litellm_e2e_coverage_cells{{module="{label}",state="covered"}} {module.covered}'
|
|
)
|
|
lines.append(
|
|
f'litellm_e2e_coverage_cells{{module="{label}",state="total"}} {module.total}'
|
|
)
|
|
lines.extend(
|
|
[
|
|
f'litellm_e2e_coverage_cells{{module="ALL",state="covered"}} {report.covered}',
|
|
f'litellm_e2e_coverage_cells{{module="ALL",state="total"}} {report.total}',
|
|
"# HELP litellm_e2e_coverage_percent E2E coverage percent by module.",
|
|
"# TYPE litellm_e2e_coverage_percent gauge",
|
|
]
|
|
)
|
|
for module in report.modules:
|
|
label = _label_value(module.module)
|
|
lines.append(
|
|
f'litellm_e2e_coverage_percent{{module="{label}"}} {module.coverage_percent:.6f}'
|
|
)
|
|
lines.extend(
|
|
[
|
|
f'litellm_e2e_coverage_percent{{module="ALL"}} {report.coverage_percent:.6f}',
|
|
"# HELP litellm_e2e_coverage_orphan_markers Coverage markers not found in the registry.",
|
|
"# TYPE litellm_e2e_coverage_orphan_markers gauge",
|
|
f"litellm_e2e_coverage_orphan_markers {len(report.orphan_markers)}",
|
|
"# HELP litellm_e2e_coverage_skipped_markers Registry cells claimed only by skipped tests.",
|
|
"# TYPE litellm_e2e_coverage_skipped_markers gauge",
|
|
f"litellm_e2e_coverage_skipped_markers {len(report.skipped_markers)}",
|
|
"# HELP litellm_e2e_coverage_collection_errors Pytest nodes that failed during collection.",
|
|
"# TYPE litellm_e2e_coverage_collection_errors gauge",
|
|
f"litellm_e2e_coverage_collection_errors {len(report.collection_errors)}",
|
|
]
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_loki(report: CoverageReport) -> str:
|
|
lines = [
|
|
(
|
|
f"COVERAGE_TOTAL percent={report.coverage_percent:.1f} "
|
|
f"covered={report.covered} total={report.total}"
|
|
)
|
|
]
|
|
lines.extend(
|
|
(
|
|
f"COVERAGE_MODULE module={loki_module_label(module.module)} "
|
|
f"percent={module.coverage_percent:.1f} "
|
|
f"covered={module.covered} total={module.total}"
|
|
)
|
|
for module in report.modules
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
class _CliArgs(BaseModel):
|
|
format: Literal["text", "json", "prometheus", "loki"]
|
|
strict: bool
|
|
fail_on_collection_errors: bool
|
|
|
|
|
|
def main() -> int:
|
|
parser = ArgumentParser()
|
|
parser.add_argument(
|
|
"--format",
|
|
choices=("text", "json", "prometheus", "loki"),
|
|
default="text",
|
|
help="Output format. Use loki for structured stdout lines in the e2e job.",
|
|
)
|
|
parser.add_argument(
|
|
"--strict",
|
|
action="store_true",
|
|
help="Exit non-zero if markers outside the registry are found.",
|
|
)
|
|
parser.add_argument(
|
|
"--fail-on-collection-errors",
|
|
action="store_true",
|
|
help="Exit non-zero if pytest collection errors are found.",
|
|
)
|
|
args = _CliArgs.model_validate(vars(parser.parse_args()))
|
|
cells = load_registry()
|
|
markers = collect_markers()
|
|
report = compute_coverage(
|
|
cells,
|
|
markers.covered,
|
|
markers.collection_errors,
|
|
markers.skipped_only,
|
|
)
|
|
output = {
|
|
"text": render,
|
|
"json": render_json,
|
|
"prometheus": render_prometheus,
|
|
"loki": render_loki,
|
|
}[args.format](report)
|
|
print(output) # noqa: T201 # CLI entrypoint output
|
|
if args.strict and report.orphan_markers:
|
|
return 1
|
|
if args.fail_on_collection_errors and report.collection_errors:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|