mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
import json
|
|
import os
|
|
import signal
|
|
import sys
|
|
import time
|
|
from collections.abc import Generator
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Final, Optional
|
|
|
|
import pytest
|
|
|
|
DB_ENV_KEYS = (
|
|
"IAM_TOKEN_DB_AUTH",
|
|
"AZURE_POSTGRESQL_AUTH",
|
|
"DATABASE_URL",
|
|
"DIRECT_URL",
|
|
"DATABASE_URL_READ_REPLICA",
|
|
"DATABASE_HOST",
|
|
"DATABASE_PORT",
|
|
"DATABASE_USER",
|
|
"DATABASE_USERNAME",
|
|
"DATABASE_NAME",
|
|
"DATABASE_SCHEMA",
|
|
"DATABASE_PASSWORD",
|
|
"DATABASE_HOST_READ_REPLICA",
|
|
"DATABASE_PORT_READ_REPLICA",
|
|
"DATABASE_USER_READ_REPLICA",
|
|
"DATABASE_USERNAME_READ_REPLICA",
|
|
"DATABASE_NAME_READ_REPLICA",
|
|
"DATABASE_SCHEMA_READ_REPLICA",
|
|
"DATABASE_PASSWORD_READ_REPLICA",
|
|
)
|
|
|
|
_db_env_snapshot_key = pytest.StashKey[dict[str, Optional[str]]]()
|
|
|
|
|
|
def _db_env_snapshot() -> dict[str, Optional[str]]:
|
|
return {key: os.environ.get(key) for key in DB_ENV_KEYS}
|
|
|
|
|
|
@pytest.hookimpl(wrapper=True)
|
|
def pytest_runtest_setup(item: pytest.Item) -> Generator[None, None, None]:
|
|
item.stash[_db_env_snapshot_key] = _db_env_snapshot()
|
|
return (yield)
|
|
|
|
|
|
@pytest.hookimpl(wrapper=True)
|
|
def pytest_runtest_teardown(item: pytest.Item, nextitem: Optional[pytest.Item]) -> Generator[None, None, None]:
|
|
result = yield
|
|
before = item.stash[_db_env_snapshot_key]
|
|
leaked = {key: value for key, value in _db_env_snapshot().items() if value != before[key]}
|
|
for key, original in before.items():
|
|
if original is None:
|
|
os.environ.pop(key, None)
|
|
else:
|
|
os.environ[key] = original
|
|
assert not leaked, (
|
|
f"{item.nodeid} leaked DB env vars past monkeypatch teardown: {leaked}. "
|
|
"Product code under test writes DATABASE_URL(_READ_REPLICA) into os.environ as a side effect; "
|
|
"monkeypatch only restores keys it has a record for, so a value written to a previously unset "
|
|
"key survives the test and poisons every later test in this pytest-xdist worker process "
|
|
"(DB-backed e2e tests arm themselves on DATABASE_URL and then fail to connect). "
|
|
"Use the unset_database_url fixture (or monkeypatch.setenv) so restoration is registered."
|
|
)
|
|
return result
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_entra_token_provider_cache() -> Generator[None, None, None]:
|
|
"""The Entra provider factory is cached process-wide so one Azure credential serves
|
|
the whole proxy; that cache would otherwise carry one test's stub into the next."""
|
|
from litellm.proxy.db.token_auth import build_azure_entra_token_provider
|
|
|
|
build_azure_entra_token_provider.cache_clear()
|
|
yield
|
|
build_azure_entra_token_provider.cache_clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def unset_database_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DATABASE_URL", "about-to-be-unset")
|
|
monkeypatch.delenv("DATABASE_URL")
|
|
|
|
|
|
FAKE_PRISMA_CLI = """#!{python}
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
calls_file = pathlib.Path(os.environ["FAKE_PRISMA_CALLS"])
|
|
earlier_calls = calls_file.read_text().splitlines() if calls_file.exists() else []
|
|
with calls_file.open("a") as log:
|
|
print(json.dumps(sys.argv[1:]), file=log)
|
|
if not earlier_calls and os.environ.get("FAKE_PRISMA_HANG_FIRST"):
|
|
grandchild = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(600)"])
|
|
pathlib.Path(os.environ["FAKE_PRISMA_GRANDCHILD_PIDFILE"]).write_text(str(grandchild.pid))
|
|
time.sleep(600)
|
|
sys.exit(0)
|
|
"""
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FakePrismaCli:
|
|
"""A stand-in `prisma` on PATH, recording every invocation.
|
|
|
|
With FAKE_PRISMA_HANG_FIRST set it hangs on its first call from a process tree
|
|
of its own, the way the real CLI wraps Node around a Rust schema engine, so a
|
|
timeout that kills only the direct child leaves the rest of that tree running.
|
|
"""
|
|
|
|
calls_file: Path
|
|
grandchild_pidfile: Path
|
|
|
|
@property
|
|
def calls(self) -> list[list[str]]:
|
|
if not self.calls_file.exists():
|
|
return []
|
|
return [json.loads(line) for line in self.calls_file.read_text().splitlines()]
|
|
|
|
def grandchild_is_gone(self, within_seconds: float) -> bool:
|
|
pid: Final = int(self.grandchild_pidfile.read_text())
|
|
deadline: Final = time.monotonic() + within_seconds
|
|
while time.monotonic() < deadline:
|
|
if os.name != "nt":
|
|
try:
|
|
reaped_pid, _ = os.waitpid(pid, os.WNOHANG)
|
|
if reaped_pid == pid:
|
|
return True
|
|
except ChildProcessError:
|
|
pass
|
|
try:
|
|
os.kill(pid, 0)
|
|
except ProcessLookupError:
|
|
return True
|
|
time.sleep(0.05)
|
|
return False
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_prisma_cli(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Generator[FakePrismaCli, None, None]:
|
|
bin_dir = tmp_path / "fakebin"
|
|
bin_dir.mkdir()
|
|
script = bin_dir / "prisma"
|
|
script.write_text(FAKE_PRISMA_CLI.format(python=sys.executable))
|
|
script.chmod(0o755)
|
|
cli = FakePrismaCli(
|
|
calls_file=tmp_path / "calls.jsonl",
|
|
grandchild_pidfile=tmp_path / "grandchild.pid",
|
|
)
|
|
monkeypatch.setenv("PATH", f"{bin_dir}{os.pathsep}{os.environ['PATH']}")
|
|
monkeypatch.setenv("FAKE_PRISMA_CALLS", str(cli.calls_file))
|
|
monkeypatch.setenv("FAKE_PRISMA_GRANDCHILD_PIDFILE", str(cli.grandchild_pidfile))
|
|
monkeypatch.setenv("LITELLM_PRISMA_COMMAND_TIMEOUT", "1")
|
|
monkeypatch.delenv("FAKE_PRISMA_HANG_FIRST", raising=False)
|
|
yield cli
|
|
if cli.grandchild_pidfile.exists():
|
|
try:
|
|
os.kill(int(cli.grandchild_pidfile.read_text()), signal.SIGKILL)
|
|
except ProcessLookupError:
|
|
pass
|
|
assert cli.grandchild_is_gone(within_seconds=5)
|