litellm/tests/e2e/load/test_locust_load.py
Kerry Lu 1270ecb781 test(load): drive /v1/messages alongside /chat/completions in the Redis chaos test
The Anthropic Messages route reaches the same Redis touchpoints and cost-tracking
callback through its own request path, so a failure-path regression there would not
surface from chat completions alone. Each simulated user now picks one endpoint round
robin and stays on it, and the per-endpoint split is asserted and reported so a run
that silently drove only one route fails instead of passing.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-11 12:28:02 -07:00

232 lines
8.8 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Final
from locust_load import (
LoadError,
LoadResult,
LocustStatEntry,
aggregate_stats,
percentile_seconds,
read_errors,
read_generator_warnings,
)
_FAILURES_HEADER = "Method,Name,Error,Occurrences,First Seen,Last Seen\n"
def _entry(
*,
num_requests: int,
name: str = "/chat/completions",
num_failures: int = 0,
start_time: float = 1000.0,
last_request_timestamp: float = 1010.0,
response_times: dict[int, int] | None = None,
) -> LocustStatEntry:
return LocustStatEntry(
name=name,
num_requests=num_requests,
num_failures=num_failures,
start_time=start_time,
last_request_timestamp=last_request_timestamp,
response_times=response_times if response_times is not None else {50: num_requests},
)
def _result(
*,
errors: tuple[LoadError, ...] = (),
generator_warnings: tuple[str, ...] = (),
) -> LoadResult:
return LoadResult(
requests=10,
failures=10,
requests_per_second=1.0,
p50_seconds=0.05,
p90_seconds=0.08,
p99_seconds=0.1,
endpoints=(),
errors=errors,
generator_warnings=generator_warnings,
)
class TestPercentiles:
def test_median_is_the_middle_sample_not_the_mean_a_slow_tail_would_drag(self) -> None:
# Nine fast requests and one very slow one: the mean is 1.99s, the median is 20ms.
entry = _entry(num_requests=10, response_times={20: 9, 20000: 1})
assert percentile_seconds([entry], 0.5) == 0.02
def test_the_tail_percentiles_reach_the_slow_samples_the_median_hides(self) -> None:
# 100 samples: 89 fast, 10 slow, 1 very slow. p50 sits in the fast bucket, p90 in the
# slow one, and p99 lands on the single very slow sample.
entry = _entry(num_requests=100, response_times={20: 89, 500: 10, 20000: 1})
assert percentile_seconds([entry], 0.5) == 0.02
assert percentile_seconds([entry], 0.9) == 0.5
assert percentile_seconds([entry], 0.99) == 0.5
assert percentile_seconds([entry], 1.0) == 20.0
def test_percentiles_merge_the_histograms_of_every_stats_entry(self) -> None:
# Per entry the median would be 10ms and 90ms; merged, the middle of the five samples is 90ms.
entries = [
_entry(num_requests=2, response_times={10: 2}),
_entry(num_requests=3, response_times={90: 3}),
]
assert percentile_seconds(entries, 0.5) == 0.09
def test_an_even_split_takes_the_lower_middle_sample_as_locust_itself_does(self) -> None:
entry = _entry(num_requests=4, response_times={10: 2, 90: 2})
assert percentile_seconds([entry], 0.5) == 0.01
def test_no_samples_reports_zero_rather_than_dividing_by_an_empty_histogram(self) -> None:
assert percentile_seconds([], 0.5) == 0.0
class TestAggregate:
def test_throughput_spans_the_whole_window_and_latency_comes_from_the_histogram(self) -> None:
entry = _entry(
num_requests=180,
start_time=1000.0,
last_request_timestamp=1060.0,
response_times={57: 180},
)
result = aggregate_stats([entry], (), ())
assert result.requests_per_second == 3.0
assert result.p50_seconds == 0.057
assert result.p99_seconds == 0.057
assert result.failure_ratio == 0.0
def test_tail_percentiles_come_from_the_slow_end_of_the_histogram(self) -> None:
entry = _entry(num_requests=100, response_times={20: 89, 500: 10, 3000: 1})
result = aggregate_stats([entry], (), ())
assert result.p50_seconds == 0.02
assert result.p90_seconds == 0.5
assert result.p99_seconds == 0.5
assert result.latency_summary() == "p50 0.020s, p90 0.500s, p99 0.500s"
def test_throughput_spans_from_the_earliest_start_when_locust_reports_several_entries(self) -> None:
entries = [
_entry(num_requests=60, start_time=1000.0, last_request_timestamp=1030.0),
_entry(num_requests=60, start_time=1020.0, last_request_timestamp=1060.0),
]
result = aggregate_stats(entries, (), ())
assert result.requests_per_second == 2.0
def test_a_run_that_drove_no_traffic_reports_a_total_failure_ratio(self) -> None:
result = aggregate_stats([], (), ())
assert result.requests == 0
assert result.requests_per_second == 0.0
assert result.failure_ratio == 1.0
assert result.endpoints == ()
class TestPerEndpoint:
def test_each_route_keeps_its_own_requests_failures_and_median(self) -> None:
entries: Final = (
_entry(name="/chat/completions", num_requests=100, response_times={20: 100}),
_entry(name="/v1/messages", num_requests=40, num_failures=3, response_times={900: 40}),
)
result: Final = aggregate_stats(entries, (), ())
assert tuple((one.name, one.requests, one.failures, one.p50_seconds) for one in result.endpoints) == (
("/chat/completions", 100, 0, 0.02),
("/v1/messages", 40, 3, 0.9),
)
def test_several_stats_entries_for_one_route_fold_into_a_single_row(self) -> None:
entries: Final = (
_entry(name="/v1/messages", num_requests=10, response_times={30: 10}),
_entry(name="/v1/messages", num_requests=30, num_failures=1, response_times={30: 30}),
)
result: Final = aggregate_stats(entries, (), ())
assert tuple((one.name, one.requests, one.failures) for one in result.endpoints) == (("/v1/messages", 40, 1),)
def test_a_route_that_never_ran_is_absent_so_a_one_sided_run_cannot_pass_unnoticed(self) -> None:
result: Final = aggregate_stats((_entry(name="/chat/completions", num_requests=10),), (), ())
assert tuple(one.name for one in result.endpoints) == ("/chat/completions",)
def test_the_summary_names_every_route_with_its_counts(self) -> None:
entries: Final = (
_entry(name="/chat/completions", num_requests=2, response_times={20: 2}),
_entry(name="/v1/messages", num_requests=1, num_failures=1, response_times={500: 1}),
)
result: Final = aggregate_stats(entries, (), ())
assert result.endpoint_summary() == (
"/chat/completions 2 requests, 0 failures, p50 0.020s, /v1/messages 1 requests, 1 failures, p50 0.500s"
)
class TestErrorBreakdown:
def test_locust_failure_rows_become_the_error_breakdown(self, tmp_path: Path) -> None:
failures_csv = tmp_path / "locust_failures.csv"
failures_csv.write_text(
_FAILURES_HEADER
+ 'POST,/chat/completions,"LocustBadStatusCode(code=401)",381,2026-07-30 12:42:01,2026-07-30 12:45:00\n'
)
assert read_errors(failures_csv) == (
LoadError(name="/chat/completions", error="LocustBadStatusCode(code=401)", occurrences=381),
)
def test_a_run_with_no_failures_writes_no_csv_and_reports_no_errors(self, tmp_path: Path) -> None:
assert read_errors(tmp_path / "locust_failures.csv") == ()
def test_diagnosis_leads_with_the_most_common_error(self) -> None:
result = _result(
errors=(
LoadError(name="/chat/completions", error="ConnectionRefused", occurrences=12),
LoadError(name="/chat/completions", error="LocustBadStatusCode(code=503)", occurrences=43675),
)
)
assert result.diagnosis().startswith("43675x /chat/completions: LocustBadStatusCode(code=503)")
def test_diagnosis_caps_the_list_and_says_how_many_it_left_out(self) -> None:
result = _result(
errors=tuple(
LoadError(name="/chat/completions", error=f"error-{index}", occurrences=index) for index in range(1, 9)
)
)
assert result.diagnosis().count("x /chat/completions") == 5
assert "and 3 more distinct errors" in result.diagnosis()
def test_diagnosis_says_so_when_locust_recorded_nothing(self) -> None:
assert _result().diagnosis() == "locust recorded no error breakdown"
class TestGeneratorSaturation:
def test_repeated_cpu_warnings_collapse_to_one_and_reach_the_diagnosis(self) -> None:
stderr = (
"[2026-07-31 12:47:01] WARNING/locust.runners: CPU usage above 90%!\n"
"[2026-07-31 12:47:02] INFO/locust.main: Run time limit reached\n"
"[2026-07-31 12:47:03] WARNING/locust.runners: CPU usage above 90%!\n"
)
warnings = read_generator_warnings(stderr)
assert len(warnings) == 1
assert "CPU usage above 90%!" in warnings[0]
assert "CPU usage above 90%!" in _result(generator_warnings=warnings).diagnosis()
def test_ordinary_locust_chatter_is_not_reported_as_a_warning(self) -> None:
assert read_generator_warnings("[2026-07-31] INFO/locust.main: Shutting down (exit code 0)\n") == ()