mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
* test(rust): add retained callback suite as expected failures * fix(tests): narrow retained callback xfails * test(rust): clarify retained callback contracts * test(ocr): clarify retained Rust contracts * test(ocr): restore guardrail contracts * test(ocr): require Rust file input parity * test(ocr): isolate native bridge contracts * fix(ci): repair Rust dispatch and OSV checks * test(ocr): assert explicit backend dispatch
132 lines
5 KiB
Python
132 lines
5 KiB
Python
import asyncio
|
|
import os
|
|
from collections.abc import AsyncIterator, Generator, Iterator
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from contextlib import ExitStack, contextmanager
|
|
from types import ModuleType
|
|
from typing import Final, cast
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
import litellm
|
|
from litellm import utils
|
|
from litellm.litellm_core_utils import litellm_logging
|
|
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
|
|
from litellm.rust_bridge.configuration import ( # pyright: ignore[reportPrivateUsage] # preserve raw configuration state in test isolation
|
|
_CONFIGURATION,
|
|
_parse_env_bool,
|
|
)
|
|
from tests.test_litellm_rust.support.callback_recorder import drain_logging
|
|
from tests.test_litellm_rust.support.recording_server import RecordingServer, recording_service
|
|
|
|
CALLBACK_ATTRIBUTES: Final = (
|
|
"callbacks",
|
|
"input_callback",
|
|
"success_callback",
|
|
"failure_callback",
|
|
"_async_input_callback",
|
|
"_async_success_callback",
|
|
"_async_failure_callback",
|
|
)
|
|
EXPECTED_FAILURE_REASONS: Final = {
|
|
"ocr/test_callbacks.py": "requires the OCR callback lifecycle implementation from #40070",
|
|
"ocr/test_guardrails.py": "requires the OCR guardrail lifecycle implementation from #40070",
|
|
"ocr/test_requests.py": "requires the OCR request and Azure authentication implementation from #40070",
|
|
}
|
|
|
|
|
|
def _list_attribute(container: ModuleType, attribute: str) -> list[object]:
|
|
value: Final = getattr(container, attribute)
|
|
if not isinstance(value, list):
|
|
raise AssertionError(f"{container.__name__}.{attribute} is not a list")
|
|
return cast(list[object], value)
|
|
|
|
|
|
@contextmanager
|
|
def _isolated_list(container: ModuleType, attribute: str) -> Iterator[None]:
|
|
source: Final = _list_attribute(container, attribute)
|
|
original: Final = list(source)
|
|
source.clear() # mutable-ok: test isolation mutates global registries by design
|
|
try:
|
|
yield
|
|
finally:
|
|
source.clear()
|
|
source.extend(original)
|
|
setattr(container, attribute, source)
|
|
|
|
|
|
@contextmanager
|
|
def _rebound(container: object, attribute: str, value: object) -> Iterator[None]:
|
|
original: Final[object] = getattr(container, attribute)
|
|
setattr(container, attribute, value)
|
|
try:
|
|
yield
|
|
finally:
|
|
setattr(container, attribute, original)
|
|
|
|
|
|
@pytest_asyncio.fixture(autouse=True, loop_scope="function")
|
|
async def isolate_ocr_test_state() -> AsyncIterator[None]:
|
|
with ExitStack() as stack:
|
|
for attribute in CALLBACK_ATTRIBUTES:
|
|
stack.enter_context(_isolated_list(litellm, attribute))
|
|
stack.enter_context(_isolated_list(litellm_logging, "_in_memory_loggers")) # pyright: ignore[reportPrivateUsage] # no public callback-cache accessor
|
|
stack.enter_context(_rebound(utils, "callback_list", [])) # rebind-ok: isolate legacy callback registry
|
|
stack.enter_context(_rebound(litellm, "cache", None)) # test-quality-ok: isolate process-global cache
|
|
stack.enter_context(_rebound(_CONFIGURATION, "override", None))
|
|
executor: Final = ThreadPoolExecutor(thread_name_prefix="rust-ocr-test-logging")
|
|
stack.enter_context(_rebound(utils, "executor", executor))
|
|
try:
|
|
yield
|
|
finally:
|
|
try:
|
|
await drain_logging()
|
|
finally:
|
|
await asyncio.to_thread(executor.shutdown, wait=True)
|
|
await GLOBAL_LOGGING_WORKER.stop()
|
|
|
|
|
|
@pytest.fixture
|
|
def recording_server() -> Generator[RecordingServer]:
|
|
with recording_service() as server:
|
|
yield server
|
|
|
|
|
|
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
|
for item in items:
|
|
if "test_litellm_rust" not in item.path.parts:
|
|
continue
|
|
relative_path: Final = "/".join(item.path.parts[item.path.parts.index("test_litellm_rust") + 1 :])
|
|
reason: Final = EXPECTED_FAILURE_REASONS.get(relative_path)
|
|
if reason is not None:
|
|
item.add_marker(pytest.mark.xfail(reason=reason, strict=False))
|
|
|
|
if not _parse_env_bool(os.environ.get("LITELLM_RUST")):
|
|
skip: Final = pytest.mark.skip(reason="requires LITELLM_RUST=1 and a compiled Rust extension")
|
|
for item in items:
|
|
if "test_litellm_rust" in item.path.parts:
|
|
item.add_marker(skip)
|
|
return
|
|
|
|
try:
|
|
from litellm.rust_bridge import _native # noqa: F401 # validates the installed extension
|
|
except ImportError as error:
|
|
raise pytest.UsageError("LITELLM_RUST=1 requires a compiled litellm.rust_bridge._native extension") from error
|
|
|
|
|
|
@pytest.fixture
|
|
def isolated_azure_auth(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
for name in (
|
|
"AZURE_AI_API_KEY",
|
|
"AZURE_AI_API_BASE",
|
|
"AZURE_AD_TOKEN",
|
|
"AZURE_TENANT_ID",
|
|
"AZURE_CLIENT_ID",
|
|
"AZURE_CLIENT_SECRET",
|
|
"AZURE_USERNAME",
|
|
"AZURE_PASSWORD",
|
|
):
|
|
monkeypatch.delenv(name, raising=False)
|
|
monkeypatch.setattr(litellm, "api_key", None)
|
|
monkeypatch.setattr(litellm, "enable_azure_ad_token_refresh", False)
|