thaman / tests /test_load.py
Turki Almurahhem
THAMAN dual-city AVM β€” Stack v22 (NYC) + v12 (Riyadh)
eb7e023
Raw
History Blame Contribute Delete
6.13 kB
"""
THAMAN β€” API Load / Stress Tests
==================================
Concurrency and latency benchmarks. Uses FastAPI TestClient for
deterministic in-process load (no network overhead). For true network
stress testing, run against a live server with locust or k6.
Benchmarks (pass/fail thresholds):
β€’ 10 sequential NYC requests < 30 s total (3 s each)
β€’ 10 sequential Riyadh requests < 30 s total (3 s each)
β€’ 10 concurrent requests complete without errors
β€’ Batch (50 items) < 60 s
Run with: pytest tests/test_load.py -v -s
"""
import time
import threading
import pytest
from fastapi.testclient import TestClient
from api.main import app
_NYC_REQ = {
"latitude": 40.6892, "longitude": -73.9442,
"gross_square_feet": 1800, "building_age": 55,
"bldgclass": "A1", "borough": 3,
"numfloors": 2, "residential_units": 1,
}
_RYD_REQ = {
"latitude": 24.7136, "longitude": 46.6753,
"property_type": "villa", "area_sqm": 300,
}
_SEQ_N = 10
_CONC_N = 10
_SEQ_LIMIT_S = 30.0 # total for _SEQ_N requests
_REQ_LIMIT_S = 5.0 # per-request cap
_BATCH_LIMIT_S = 60.0
@pytest.fixture(scope="module")
def client():
with TestClient(app) as c:
yield c
# ── Sequential throughput ─────────────────────────────────────────────
def test_nyc_sequential_throughput(client):
"""10 NYC predictions complete in under 30 seconds total."""
t0 = time.perf_counter()
for _ in range(_SEQ_N):
resp = client.post("/predict", json=_NYC_REQ)
assert resp.status_code == 200
elapsed = time.perf_counter() - t0
print(f"\n NYC sequential: {_SEQ_N} requests in {elapsed:.2f}s ({elapsed/_SEQ_N:.2f}s each)")
assert elapsed < _SEQ_LIMIT_S, f"Too slow: {elapsed:.2f}s > {_SEQ_LIMIT_S}s"
def test_riyadh_sequential_throughput(client):
"""10 Riyadh predictions complete in under 30 seconds total."""
t0 = time.perf_counter()
for _ in range(_SEQ_N):
resp = client.post("/predict/riyadh", json=_RYD_REQ)
assert resp.status_code == 200
elapsed = time.perf_counter() - t0
print(f"\n Riyadh sequential: {_SEQ_N} requests in {elapsed:.2f}s ({elapsed/_SEQ_N:.2f}s each)")
assert elapsed < _SEQ_LIMIT_S, f"Too slow: {elapsed:.2f}s > {_SEQ_LIMIT_S}s"
def test_per_request_latency(client):
"""Individual request latency should be under 5 seconds."""
latencies = []
for _ in range(5):
t0 = time.perf_counter()
client.post("/predict", json=_NYC_REQ)
latencies.append(time.perf_counter() - t0)
p95 = sorted(latencies)[int(0.95 * len(latencies))]
print(f"\n p95 latency: {p95:.3f}s (max allowed {_REQ_LIMIT_S}s)")
assert p95 < _REQ_LIMIT_S, f"p95 latency {p95:.3f}s exceeds {_REQ_LIMIT_S}s"
# ── Concurrent load ───────────────────────────────────────────────────
def test_concurrent_requests_no_errors(client):
"""10 concurrent NYC predictions all succeed (no 500s, no crashes)."""
results: list[int] = []
errors: list[str] = []
lock = threading.Lock()
def _predict():
try:
resp = client.post("/predict", json=_NYC_REQ)
with lock:
results.append(resp.status_code)
except Exception as e:
with lock:
errors.append(str(e))
threads = [threading.Thread(target=_predict) for _ in range(_CONC_N)]
t0 = time.perf_counter()
for t in threads:
t.start()
for t in threads:
t.join(timeout=60)
elapsed = time.perf_counter() - t0
print(f"\n Concurrent: {_CONC_N} threads in {elapsed:.2f}s")
assert not errors, f"Thread errors: {errors}"
assert len(results) == _CONC_N, f"Only {len(results)}/{_CONC_N} threads completed"
failed = [s for s in results if s != 200]
assert not failed, f"Non-200 responses: {failed}"
def test_concurrent_riyadh_no_errors(client):
"""10 concurrent Riyadh predictions all succeed."""
results: list[int] = []
lock = threading.Lock()
def _predict():
resp = client.post("/predict/riyadh", json=_RYD_REQ)
with lock:
results.append(resp.status_code)
threads = [threading.Thread(target=_predict) for _ in range(_CONC_N)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=60)
failed = [s for s in results if s != 200]
assert not failed, f"Riyadh concurrent failures: {failed}"
assert len(results) == _CONC_N
# ── Batch endpoint ─────────────────────────────────────────────────────
def test_batch_50_completes_in_time(client):
"""Batch of 50 properties completes under 60 seconds."""
payload = [_NYC_REQ] * 50
t0 = time.perf_counter()
resp = client.post("/batch", json=payload)
elapsed = time.perf_counter() - t0
print(f"\n Batch 50: {elapsed:.2f}s")
assert resp.status_code == 200, f"Batch failed: {resp.text[:200]}"
assert elapsed < _BATCH_LIMIT_S, f"Batch too slow: {elapsed:.2f}s"
assert resp.json()["count"] == 50
def test_batch_all_results_valid(client):
"""All batch results must have positive prices."""
payload = [
{**_NYC_REQ, "latitude": 40.6892 + i * 0.001}
for i in range(10)
]
data = client.post("/batch", json=payload).json()
for r in data["results"]:
assert r.get("predicted_price", 0) > 0
# ── Health endpoint latency ───────────────────────────────────────────
def test_health_endpoint_fast(client):
"""Health endpoint should respond in under 100ms."""
t0 = time.perf_counter()
resp = client.get("/health")
elapsed = time.perf_counter() - t0
assert resp.status_code == 200
assert elapsed < 0.1, f"Health check took {elapsed:.3f}s (limit: 0.1s)"