mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
test(proxy): make two proxy-infra tests independent of sibling-test state (#42581)
* test(proxy): make two proxy-infra tests independent of sibling-test state Both tests read process-global state that another module in the same xdist worker can change, so they passed or failed on shard scheduling rather than on the behaviour they assert. test_gateway_plus_backend_covers_full_app checked allowlist coverage against the live route table. gateway/main.py trims routes once, inside the lifespan, so it only ever sees what is registered at startup; a lazy feature appends its router on demand afterwards and is never filtered. Those routes cannot be dropped on the floor, but they do enter the assertion the moment a sibling test warms the feature, and 110 of them sit in neither allowlist. Subtract exactly the lazy features this process has loaded, which leaves the assertion at full strength for every eagerly registered route. test_real_proxy_child_auth_privacy_and_body_policy pins prisma_client to a bare object(). litellm.max_budget is a module global that nothing restores between tests; once a sibling leaves it above zero, user_api_key_auth takes the global-spend branch, dereferences prisma_client.db, and the AttributeError surfaces as HTTP 401. Pin max_budget next to the other globals the test already controls. * test(proxy): measure allowlist coverage in a pristine interpreter The previous revision subtracted paths matching a loaded lazy feature's prefixes. Those prefixes are broad enough to swallow eagerly registered routes: 31 of them, including /openai/deployments/*, /access_group/*, /cursor/* and /mcp, which would have made a real allowlist regression invisible. Run the coverage check in a fresh interpreter instead. No lazy feature is loaded there, so the route table is exactly the one gateway/main.py's lifespan trim sees, and nothing has to be subtracted for the result to be deterministic. The probe also reports the lazy modules it loaded and its route count, so an empty uncovered set cannot pass vacuously. Drop _component_paths and the four allowlist constants it used; the probe reproduces the predicate in the child process.
This commit is contained in:
parent
d7c27cdc08
commit
65e42526d6
1 changed files with 59 additions and 35 deletions
|
|
@ -23,8 +23,10 @@ import (which raises on a non-postgres ``DATABASE_URL`` scheme and can mint an
|
|||
RDS IAM token when ``IAM_TOKEN_DB_AUTH`` is set).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Final
|
||||
|
||||
# Importing ``litellm.proxy.proxy_server`` runs its module-level setup, which
|
||||
# reads ``DATABASE_URL`` (Prisma) and ``LITELLM_MASTER_KEY``. Tier-zero CI
|
||||
|
|
@ -49,17 +51,10 @@ _REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..",
|
|||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
|
||||
from backend.routes.allowlist import (
|
||||
BACKEND_EXACT_PATHS,
|
||||
BACKEND_MOUNT_PATHS,
|
||||
BACKEND_PATH_PREFIXES,
|
||||
)
|
||||
from gateway.routes.allowlist import (
|
||||
GATEWAY_EXACT_PATHS,
|
||||
GATEWAY_MOUNT_PATHS,
|
||||
GATEWAY_PATH_PREFIXES,
|
||||
)
|
||||
from backend.routes.allowlist import BACKEND_MOUNT_PATHS
|
||||
from gateway.routes.allowlist import GATEWAY_MOUNT_PATHS
|
||||
from litellm.proxy.proxy_server import app
|
||||
from tests.test_litellm_rust.support.child_interpreter import run_child_interpreter
|
||||
|
||||
for _key, _previous in _PRE_EXISTING_ENV.items():
|
||||
if _previous is None:
|
||||
|
|
@ -87,40 +82,69 @@ for _key, _previous in _PRE_DB_ENV.items():
|
|||
os.environ[_key] = _previous
|
||||
|
||||
|
||||
def _component_paths(routes, exact_paths, path_prefixes) -> set[str]:
|
||||
"""Reproduce ``gateway.main._is_gateway_route`` / ``backend.main._is_backend_route``."""
|
||||
out: set[str] = set()
|
||||
for route in routes:
|
||||
if isinstance(route, Mount):
|
||||
continue
|
||||
path = getattr(route, "path", None)
|
||||
if path is None:
|
||||
continue
|
||||
if path in exact_paths or any(path.startswith(p) for p in path_prefixes):
|
||||
out.add(path)
|
||||
return out
|
||||
_COVERAGE_PROBE: Final = """
|
||||
import json, os, sys
|
||||
sys.path.insert(0, os.environ["LITELLM_COMPONENT_ALLOWLIST_REPO_ROOT"])
|
||||
from fastapi.routing import Mount
|
||||
from backend.routes.allowlist import BACKEND_EXACT_PATHS, BACKEND_PATH_PREFIXES
|
||||
from gateway.routes.allowlist import GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES
|
||||
from litellm.proxy._lazy_features import loaded_lazy_modules
|
||||
from litellm.proxy.proxy_server import app
|
||||
|
||||
all_paths = {
|
||||
r.path for r in app.router.routes
|
||||
if not isinstance(r, Mount) and getattr(r, "path", None) is not None
|
||||
}
|
||||
|
||||
|
||||
def covered(exact, prefixes):
|
||||
return {p for p in all_paths if p in exact or any(p.startswith(x) for x in prefixes)}
|
||||
|
||||
|
||||
json.dump({
|
||||
"lazy_loaded": sorted(loaded_lazy_modules(app)),
|
||||
"route_count": len(all_paths),
|
||||
"uncovered": sorted(all_paths - (
|
||||
covered(GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES)
|
||||
| covered(BACKEND_EXACT_PATHS, BACKEND_PATH_PREFIXES)
|
||||
)),
|
||||
}, sys.stdout)
|
||||
"""
|
||||
|
||||
|
||||
def test_gateway_plus_backend_covers_full_app():
|
||||
"""Every route on the proxy app must be served by gateway or backend."""
|
||||
all_paths = {
|
||||
getattr(r, "path")
|
||||
for r in app.router.routes
|
||||
if not isinstance(r, Mount) and getattr(r, "path", None) is not None
|
||||
}
|
||||
gateway_paths = _component_paths(
|
||||
app.router.routes, GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES
|
||||
"""Every route on the proxy app must be served by gateway or backend.
|
||||
|
||||
``gateway.main`` and ``backend.main`` trim the route table once, inside the
|
||||
lifespan, so the set this has to cover is the one registered at startup. A
|
||||
lazy feature appends its router on demand, after that trim, and whether a
|
||||
sibling test in the same xdist worker has triggered one is not something
|
||||
this test can control. Measuring in a fresh interpreter is what makes the
|
||||
route table deterministic; nothing is subtracted, so every route the trim
|
||||
will actually see stays in the assertion.
|
||||
"""
|
||||
env: Final = {**os.environ, "LITELLM_COMPONENT_ALLOWLIST_REPO_ROOT": _REPO_ROOT}
|
||||
for key, value in _THROWAWAY_ENV.items():
|
||||
env.setdefault(key, value)
|
||||
|
||||
probe: Final = run_child_interpreter(_COVERAGE_PROBE, env=env, timeout=90)
|
||||
assert probe.returncode == 0, f"route probe failed:\n{probe.stderr}"
|
||||
report: Final = json.loads(probe.stdout)
|
||||
|
||||
assert not report["lazy_loaded"], (
|
||||
"route probe was not pristine; it loaded lazy features "
|
||||
f"{report['lazy_loaded']}, so its route table is not the startup one"
|
||||
)
|
||||
backend_paths = _component_paths(
|
||||
app.router.routes, BACKEND_EXACT_PATHS, BACKEND_PATH_PREFIXES
|
||||
assert report["route_count"] > 100, (
|
||||
f"route probe only saw {report['route_count']} routes, so an empty "
|
||||
"uncovered set would not mean anything"
|
||||
)
|
||||
|
||||
uncovered = all_paths - (gateway_paths | backend_paths)
|
||||
|
||||
uncovered: Final = report["uncovered"]
|
||||
assert not uncovered, (
|
||||
f"{len(uncovered)} route(s) are not exposed on either component. "
|
||||
f"Update gateway/routes/allowlist.py or backend/routes/allowlist.py to cover:\n "
|
||||
+ "\n ".join(sorted(uncovered))
|
||||
+ "\n ".join(uncovered)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue