Files
chongming-core/test/test_instance_targeting_http.py
T

208 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
HTTP 实例定向头(X-Chongming-Instance)透传测试
依赖隔离:桩掉 chongming 包链、auth 与 chongming_cache,直接加载真实
route_generator.py 与 worker_manager.py,用 FastAPI TestClient 验证:
1. 携带 X-Chongming-Instance 的请求,该实例 id 透传到 call_raw / call_stream
2. 未携带该头时 instance_id 为 None(行为与改动前完全一致,兼容旧调用方)
3. 覆盖全部端点形态:GET query / POST body / validate=false 原样透传 / SSE 流式
4. 头值为空白时视为未指定
5. 定向到离线实例(RuntimeError)-> HTTP 503,错误信息透出
运行: .venv/bin/python test/test_instance_targeting_http.py
"""
import importlib.util
import logging
import sys
import types
from pathlib import Path
import orjson
from fastapi import FastAPI
from fastapi.testclient import TestClient
TEST_DIR = Path(__file__).resolve().parent
REPO_ROOT = TEST_DIR.parent
sys.path.insert(0, str(REPO_ROOT / "gateway" / "src"))
sys.path.insert(0, str(REPO_ROOT / "utils" / "worker" / "src"))
# ---- 依赖隔离:桩掉 chongming 包链 / auth / chongming_cache ----
_pkg = types.ModuleType("chongming")
_pkg.__path__ = []
_app = types.ModuleType("chongming.app")
_app.__path__ = []
_core = types.ModuleType("chongming.app.core")
_core.__path__ = []
_logger_stub = types.ModuleType("chongming.app.core.logger")
_logger_stub.get_logger = lambda name: logging.getLogger(name)
_auth_stub = types.ModuleType("chongming.app.core.auth")
_auth_stub.store_user_to_request = lambda x: x
_cache_stub = types.ModuleType("chongming_cache")
_cache_stub.Cache = object
_cache_stub.DistributedLock = object # worker_manager 顶层导入(本测试不触达)
for mod in (_pkg, _app, _core, _logger_stub, _auth_stub, _cache_stub):
sys.modules[mod.__name__] = mod
_wm_path = REPO_ROOT / "gateway" / "src" / "chongming" / "app" / "core" / "worker_manager.py"
_wm_spec = importlib.util.spec_from_file_location(
"chongming.app.core.worker_manager", _wm_path
)
_wm_mod = importlib.util.module_from_spec(_wm_spec)
sys.modules["chongming.app.core.worker_manager"] = _wm_mod
_wm_spec.loader.exec_module(_wm_mod) # type: ignore[union-attr]
_cs_path = REPO_ROOT / "gateway" / "src" / "chongming" / "app" / "core" / "call_stats.py"
_cs_spec = importlib.util.spec_from_file_location(
"chongming.app.core.call_stats", _cs_path
)
_cs_mod = importlib.util.module_from_spec(_cs_spec)
sys.modules["chongming.app.core.call_stats"] = _cs_mod
_cs_spec.loader.exec_module(_cs_mod) # type: ignore[union-attr]
_rg_path = REPO_ROOT / "gateway" / "src" / "chongming" / "app" / "core" / "route_generator.py"
_rg_spec = importlib.util.spec_from_file_location(
"chongming.app.core.route_generator", _rg_path
)
_rg_mod = importlib.util.module_from_spec(_rg_spec)
sys.modules["chongming.app.core.route_generator"] = _rg_mod
_rg_spec.loader.exec_module(_rg_mod) # type: ignore[union-attr]
register_route_group = _rg_mod.register_route_group
INSTANCE_HEADER = _rg_mod.INSTANCE_HEADER
failures = []
def check(name, cond, detail=""):
status = "PASS" if cond else "FAIL"
print(f"[{status}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
failures.append(name)
class FakeWorkerManager:
"""记录每次调用收到的 instance_id;可指定定点失败以验证 503 映射。"""
def __init__(self, fail_instance=None):
self.fail_instance = fail_instance # 指定实例报错(模拟实例离线)
self.raw_instances = [] # call_raw 收到的 instance_id
self.stream_instances = [] # call_stream 收到的 instance_id
async def call_raw(self, worker_type, subject, payload, timeout=3.0,
retries=1, cacheable=False, instance_id=None):
self.raw_instances.append(instance_id)
if instance_id and instance_id == self.fail_instance:
raise RuntimeError(
f"类型 '{worker_type}' 的实例 '{instance_id}' 没有在线副本"
"(定向调用不跨实例回退)。"
)
return orjson.dumps({"op": subject, "instance": instance_id})
async def call_stream(self, worker_type, subject, payload, timeout=300.0,
idle_timeout=10.0, instance_id=None):
self.stream_instances.append(instance_id)
if instance_id and instance_id == self.fail_instance:
raise RuntimeError(f"实例 '{instance_id}' 没有在线副本")
async def _gen():
yield orjson.dumps({"instance": instance_id})
return _gen()
ROUTES = [
{"subject": "t.get", "path": "/calc/add", "method": "GET",
"params": ["a: float", "b: float"], "summary": "add",
"timeout": 3.0, "auth_required": False},
{"subject": "t.post", "path": "/calc/echo", "method": "POST",
"params": ["msg: str"], "summary": "echo",
"timeout": 3.0, "auth_required": False},
{"subject": "t.raw", "path": "/calc/raw", "method": "POST",
"params": ["payload: any"], "summary": "原样透传",
"validate": False, "timeout": 3.0, "auth_required": False},
{"subject": "t.stream", "path": "/stream/chat", "method": "POST",
"params": ["prompt: str"], "summary": "sse",
"stream": True, "timeout": 60.0, "stream_idle_timeout": 1.0,
"auth_required": False},
]
def build_client(fwm):
app = FastAPI()
register_route_group(app, fwm, "demotype", ROUTES, prefix="/api/v1")
return TestClient(app)
def main():
# ── 1. GET query 端点 ────────────────────────────────────
fwm = FakeWorkerManager()
c = build_client(fwm)
r = c.get("/api/v1/calc/add?a=1&b=2", headers={INSTANCE_HEADER: "edge-01"})
check("1.1 GET 携带头 -> 200", r.status_code == 200, f"status={r.status_code}")
check("1.2 GET 头透传到 call_raw", fwm.raw_instances == ["edge-01"],
f"instances={fwm.raw_instances}")
check("1.3 响应体回带实例(证明打到目标实例)",
r.json().get("instance") == "edge-01", f"body={r.json()}")
# ── 2. 未携带头:兼容旧调用方 ────────────────────────────
fwm = FakeWorkerManager()
c = build_client(fwm)
r = c.get("/api/v1/calc/add?a=1&b=2")
check("2.1 未携带头 -> 200(默认随机均衡路径不变)", r.status_code == 200)
check("2.2 未携带头时 instance_id=None", fwm.raw_instances == [None],
f"instances={fwm.raw_instances}")
check("2.3 响应体不含实例", r.json().get("instance") is None, f"body={r.json()}")
# ── 3. POST body 端点 ────────────────────────────────────
fwm = FakeWorkerManager()
c = build_client(fwm)
r = c.post("/api/v1/calc/echo", json={"msg": "hi"},
headers={INSTANCE_HEADER: "edge-02"})
check("3.1 POST body 头透传", fwm.raw_instances == ["edge-02"],
f"instances={fwm.raw_instances}")
# ── 4. validate=false 原样透传端点 ───────────────────────
fwm = FakeWorkerManager()
c = build_client(fwm)
r = c.post("/api/v1/calc/raw", content=b'{"anything": [1, 2]}',
headers={INSTANCE_HEADER: "edge-03",
"content-type": "application/json"})
check("4.1 原样透传端点头透传", fwm.raw_instances == ["edge-03"],
f"instances={fwm.raw_instances}")
# ── 5. SSE 流式端点 ──────────────────────────────────────
fwm = FakeWorkerManager()
c = build_client(fwm)
with c.stream("POST", "/api/v1/stream/chat", json={"prompt": "x"},
headers={INSTANCE_HEADER: "edge-04"}) as r:
body = b"".join(r.iter_bytes())
check("5.1 流式端点头透传", fwm.stream_instances == ["edge-04"],
f"instances={fwm.stream_instances}")
check("5.2 流式响应含目标实例", b"edge-04" in body, f"body={body[:80]!r}")
# ── 6. 空白头值视为未指定 ────────────────────────────────
fwm = FakeWorkerManager()
c = build_client(fwm)
r = c.get("/api/v1/calc/add?a=1&b=2", headers={INSTANCE_HEADER: " "})
check("6.1 空白头值 -> instance_id=None", fwm.raw_instances == [None],
f"instances={fwm.raw_instances}")
# ── 7. 定向实例离线 -> 503 ───────────────────────────────
fwm = FakeWorkerManager(fail_instance="edge-gone")
c = build_client(fwm)
r = c.get("/api/v1/calc/add?a=1&b=2", headers={INSTANCE_HEADER: "edge-gone"})
check("7.1 定向实例离线 -> 503", r.status_code == 503, f"status={r.status_code}")
check("7.2 503 详情点名实例", "edge-gone" in str(r.json().get("detail", "")),
f"detail={r.json().get('detail')}")
print()
if failures:
print(f"FAILED: {len(failures)} 项 -> {failures}")
return 1
print("全部通过")
return 0
if __name__ == "__main__":
raise SystemExit(main())