mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
* test(e2e): move the harness self-tests out of tests/e2e The nightly Buildkite run copies tests/e2e into the runner image and runs bare pytest, so the 672 tests of the harness itself (fixture parsing, JUnit properties, the stack lock, the load aggregators, the Claude Code driver) counted as e2e tests on the status page even though none of them reaches a proxy. They now live in tests/e2e_harness, mirroring the tests/e2e layout, and run in the GitHub Actions lint job and the CircleCI provider_replay_harness job instead * fix(ci): point the providers replay controls at tests/e2e_harness The providers integration job still selected the four replay-control tests under tests/e2e/test_provider_edge.py, so pytest exited before they ran. The raw-HTTP check's file walk also drops to one loop per comprehension * style(tests): mark the raw-HTTP check's bindings Final
223 lines
7.6 KiB
Python
223 lines
7.6 KiB
Python
import json
|
|
from collections.abc import Iterator, Sequence
|
|
from dataclasses import dataclass
|
|
from typing import Final
|
|
|
|
import pytest
|
|
|
|
from datadog_reader import DdLogsReader
|
|
from datadog_reader import _DdAuthHeaders # pyright: ignore[reportPrivateUsage] # verifies private auth-header serialization
|
|
from e2e_config import DD_SEARCH_INTERVAL, POLL_TIMEOUT
|
|
from e2e_http import StreamingResponse
|
|
|
|
|
|
def test_failure_diagnostics_hide_credentials_without_changing_auth_headers() -> None:
|
|
api_key: Final = "test-datadog-api-secret"
|
|
app_key: Final = "test-datadog-app-secret"
|
|
reader: Final = DdLogsReader(site="datadoghq.com", api_key=api_key, app_key=app_key)
|
|
headers: Final = _DdAuthHeaders(api_key=api_key, app_key=app_key)
|
|
|
|
for value in (reader, headers):
|
|
assert api_key not in repr(value)
|
|
assert app_key not in repr(value)
|
|
|
|
assert headers.model_dump(by_alias=True) == {
|
|
"DD-API-KEY": api_key,
|
|
"DD-APPLICATION-KEY": app_key,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class Clock:
|
|
elapsed: float = 0.0
|
|
|
|
def now(self) -> float:
|
|
return self.elapsed
|
|
|
|
def sleep(self, seconds: float) -> None:
|
|
self.elapsed += seconds
|
|
|
|
|
|
@dataclass
|
|
class Search:
|
|
responses: Iterator[StreamingResponse]
|
|
calls: tuple[tuple[str, float], ...] = ()
|
|
|
|
def __call__(self, query: str, timeout: float) -> StreamingResponse:
|
|
self.calls += ((query, timeout),)
|
|
return next(self.responses)
|
|
|
|
|
|
def _page(*event_ids: str) -> StreamingResponse:
|
|
return StreamingResponse(
|
|
status_code=200,
|
|
body=json.dumps({"data": [{"attributes": {"attributes": {"id": event_id}}} for event_id in event_ids]}),
|
|
)
|
|
|
|
|
|
def _reader(responses: Sequence[StreamingResponse], clock: Clock) -> tuple[DdLogsReader, Search]:
|
|
search: Final = Search(iter(responses))
|
|
return DdLogsReader(
|
|
site="us5.datadoghq.com",
|
|
api_key="test-api-secret",
|
|
app_key="test-app-secret",
|
|
search=search,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
jitter=lambda: 0.25,
|
|
), search
|
|
|
|
|
|
def test_429_honors_server_reset_and_preserves_duplicate_events() -> None:
|
|
clock: Final = Clock()
|
|
reader, search = _reader(
|
|
(StreamingResponse(status_code=429, body="", headers={"x-ratelimit-reset": "6"}), _page("first", "duplicate")),
|
|
clock,
|
|
)
|
|
|
|
events: Final = reader.events_for_query("test-marker")
|
|
|
|
assert tuple(event.attributes["id"] for event in events) == ("first", "duplicate")
|
|
assert clock.elapsed == 6.25
|
|
assert search.calls == (("test-marker", 30.0), ("test-marker", 30.0))
|
|
|
|
|
|
@pytest.mark.parametrize("reset", ("", "invalid", "nan", "inf", "-1"))
|
|
def test_invalid_reset_uses_search_interval(reset: str) -> None:
|
|
clock: Final = Clock()
|
|
reader, _ = _reader(
|
|
(StreamingResponse(status_code=429, body="", headers={"x-ratelimit-reset": reset}), _page()), clock
|
|
)
|
|
|
|
assert reader.events_for_query("test-marker") == []
|
|
assert clock.elapsed == DD_SEARCH_INTERVAL + 0.25
|
|
|
|
|
|
def test_zero_reset_cannot_create_a_busy_retry_loop() -> None:
|
|
clock: Final = Clock()
|
|
reader, _ = _reader(
|
|
(StreamingResponse(status_code=429, body="", headers={"x-ratelimit-reset": "0"}), _page()), clock
|
|
)
|
|
|
|
assert reader.events_for_query("test-marker") == []
|
|
assert clock.elapsed == 1.25
|
|
|
|
|
|
def test_retry_after_is_not_shortened_by_an_earlier_reset() -> None:
|
|
clock: Final = Clock()
|
|
reader, _ = _reader(
|
|
(StreamingResponse(status_code=429, body="", headers={"x-ratelimit-reset": "2", "retry-after": "8"}), _page()),
|
|
clock,
|
|
)
|
|
|
|
assert reader.events_for_query("test-marker") == []
|
|
assert clock.elapsed == 8.25
|
|
|
|
|
|
def test_rate_limit_wait_stops_at_deadline_without_issuing_another_request() -> None:
|
|
clock: Final = Clock()
|
|
reader, search = _reader(
|
|
(StreamingResponse(status_code=429, body="", headers={"x-ratelimit-reset": str(POLL_TIMEOUT * 10)}),), clock
|
|
)
|
|
|
|
with pytest.raises(pytest.fail.Exception, match="remained rate-limited"):
|
|
reader.events_for_query("test-marker")
|
|
|
|
assert clock.elapsed == POLL_TIMEOUT
|
|
assert search.calls == (("test-marker", 30.0),)
|
|
|
|
|
|
def test_late_retry_cannot_receive_a_fresh_request_timeout() -> None:
|
|
clock: Final = Clock()
|
|
reader, search = _reader(
|
|
(StreamingResponse(status_code=429, body="", headers={"x-ratelimit-reset": str(POLL_TIMEOUT - 5)}), _page()),
|
|
clock,
|
|
)
|
|
|
|
assert reader.events_for_query("test-marker") == []
|
|
assert search.calls == (("test-marker", 30.0), ("test-marker", 4.75))
|
|
|
|
|
|
@pytest.mark.parametrize("status", (-1, 401, 403, 500))
|
|
def test_non_quota_failures_are_not_retried_or_treated_as_empty_results(status: int) -> None:
|
|
clock: Final = Clock()
|
|
reader, search = _reader((StreamingResponse(status_code=status, body=""), _page()), clock)
|
|
|
|
with pytest.raises(pytest.fail.Exception, match=f"failed with HTTP {status}"):
|
|
reader.events_for_query("test-marker")
|
|
|
|
assert search.calls == (("test-marker", 30.0),)
|
|
assert clock.elapsed == 0
|
|
|
|
|
|
def test_polling_quota_retries_share_the_original_deadline() -> None:
|
|
clock: Final = Clock()
|
|
reader, search = _reader(
|
|
(_page(), StreamingResponse(status_code=429, body="", headers={"x-ratelimit-reset": str(POLL_TIMEOUT)})),
|
|
clock,
|
|
)
|
|
|
|
with pytest.raises(pytest.fail.Exception, match="remained rate-limited"):
|
|
reader.poll_events_for_query("test-marker")
|
|
|
|
assert clock.elapsed == POLL_TIMEOUT
|
|
assert len(search.calls) == 2
|
|
|
|
|
|
def test_empty_polling_does_not_start_a_final_search_after_its_deadline() -> None:
|
|
clock: Final = Clock()
|
|
attempts: Final = int(POLL_TIMEOUT / DD_SEARCH_INTERVAL)
|
|
reader, search = _reader((_page(),) * attempts, clock)
|
|
|
|
assert reader.poll_events_for_query("test-marker") == []
|
|
assert clock.elapsed == POLL_TIMEOUT
|
|
assert len(search.calls) == attempts
|
|
|
|
|
|
def test_settlement_quota_retries_keep_the_remaining_readback_budget() -> None:
|
|
clock: Final = Clock()
|
|
empty_reads: Final = int(POLL_TIMEOUT / DD_SEARCH_INTERVAL) - 2
|
|
reader, search = _reader(
|
|
(_page(),) * empty_reads
|
|
+ (_page("first"), StreamingResponse(status_code=429, body="", headers={"x-ratelimit-reset": str(POLL_TIMEOUT)})),
|
|
clock,
|
|
)
|
|
|
|
with pytest.raises(pytest.fail.Exception, match="remained rate-limited"):
|
|
reader.poll_events_for_query("test-marker")
|
|
|
|
assert clock.elapsed == POLL_TIMEOUT
|
|
assert search.calls[-1] == ("test-marker", DD_SEARCH_INTERVAL)
|
|
assert len(search.calls) == empty_reads + 2
|
|
|
|
|
|
def test_settlement_detects_a_duplicate_on_the_final_search() -> None:
|
|
clock: Final = Clock()
|
|
reader, _ = _reader((_page("first"), _page("first"), _page(), _page("first", "duplicate")), clock)
|
|
|
|
events: Final = reader.poll_events_for_query("test-marker")
|
|
|
|
assert tuple(event.attributes["id"] for event in events) == ("first", "duplicate")
|
|
assert clock.elapsed == 30
|
|
|
|
|
|
def test_settlement_keeps_confirmed_events_through_empty_searches() -> None:
|
|
clock: Final = Clock()
|
|
reader, _ = _reader((_page("first"), _page(), _page(), _page()), clock)
|
|
|
|
events: Final = reader.poll_events_for_query("test-marker")
|
|
|
|
assert tuple(event.attributes["id"] for event in events) == ("first",)
|
|
assert clock.elapsed == 30
|
|
|
|
|
|
def test_late_delivery_cannot_pass_without_a_complete_settle_window() -> None:
|
|
clock: Final = Clock()
|
|
empty_reads: Final = int(POLL_TIMEOUT / DD_SEARCH_INTERVAL) - 2
|
|
reader, search = _reader((_page(),) * empty_reads + (_page("first"), _page("first")), clock)
|
|
|
|
with pytest.raises(pytest.fail.Exception, match="duplicate-detection window"):
|
|
reader.poll_events_for_query("test-marker")
|
|
|
|
assert clock.elapsed == POLL_TIMEOUT
|
|
assert len(search.calls) == empty_reads + 2
|