108 lines
3.0 KiB
Python
108 lines
3.0 KiB
Python
"""T11.1 Locust 性能基准脚本。
|
||
|
||
目标:对 T6.1 FastAPI 管理后台骨架做最小真实压测,验证计划中的
|
||
"10 并发"目标,并记录各端点的延迟/错误率基线。
|
||
|
||
压测模型:
|
||
- AdminBrowseUser:模拟已登录后台管理员,覆盖 /me /meta /orders /stats
|
||
- LoginBurstUser:模拟登录高峰,持续打 /api/auth/login
|
||
|
||
运行示例:
|
||
GAOKAO_JWT_SECRET=$(python3 - <<'PY'
|
||
print('x'*64)
|
||
PY
|
||
) \
|
||
locust -f locustfile.py --host http://127.0.0.1:18080 \
|
||
--headless -u 10 -r 2 -t 1m --csv reports/perf/t11_1
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from typing import Optional
|
||
|
||
from locust import HttpUser, between, task
|
||
|
||
|
||
ADMIN_USER = os.getenv("GAOKAO_ADMIN_USER", "admin")
|
||
ADMIN_PASS = os.getenv("GAOKAO_ADMIN_PASS", "admin123")
|
||
|
||
|
||
class AuthenticatedHttpUser(HttpUser):
|
||
abstract = True
|
||
wait_time = between(0.3, 1.2)
|
||
token: Optional[str] = None
|
||
|
||
def on_start(self) -> None:
|
||
self.login()
|
||
|
||
def login(self) -> None:
|
||
with self.client.post(
|
||
"/api/auth/login",
|
||
json={"username": ADMIN_USER, "password": ADMIN_PASS},
|
||
name="POST /api/auth/login",
|
||
catch_response=True,
|
||
) as resp:
|
||
if resp.status_code != 200:
|
||
resp.failure(f"login failed: {resp.status_code} {resp.text[:200]}")
|
||
self.token = None
|
||
return
|
||
data = resp.json()
|
||
token = data.get("access_token")
|
||
if not token:
|
||
resp.failure("login response missing access_token")
|
||
self.token = None
|
||
return
|
||
self.token = token
|
||
self.client.headers.update({"Authorization": f"Bearer {token}"})
|
||
resp.success()
|
||
|
||
|
||
class AdminBrowseUser(AuthenticatedHttpUser):
|
||
weight = 4
|
||
|
||
@task(5)
|
||
def me(self) -> None:
|
||
self.client.get("/api/auth/me", name="GET /api/auth/me")
|
||
|
||
@task(4)
|
||
def meta(self) -> None:
|
||
self.client.get("/api/meta", name="GET /api/meta")
|
||
|
||
@task(6)
|
||
def list_orders(self) -> None:
|
||
self.client.get("/api/orders?limit=50&offset=0", name="GET /api/orders")
|
||
|
||
@task(3)
|
||
def order_stats(self) -> None:
|
||
self.client.get("/api/stats/orders", name="GET /api/stats/orders")
|
||
|
||
@task(1)
|
||
def detail_missing(self) -> None:
|
||
with self.client.get(
|
||
"/api/orders/non-existent-order-id",
|
||
name="GET /api/orders/{id} [404]",
|
||
catch_response=True,
|
||
) as resp:
|
||
if resp.status_code == 404:
|
||
resp.success()
|
||
else:
|
||
resp.failure(f"expected 404, got {resp.status_code}")
|
||
|
||
|
||
class LoginBurstUser(HttpUser):
|
||
weight = 1
|
||
wait_time = between(1.0, 3.0)
|
||
|
||
@task(4)
|
||
def login(self) -> None:
|
||
self.client.post(
|
||
"/api/auth/login",
|
||
json={"username": ADMIN_USER, "password": ADMIN_PASS},
|
||
name="POST /api/auth/login",
|
||
)
|
||
|
||
@task(1)
|
||
def health(self) -> None:
|
||
self.client.get("/health", name="GET /health")
|