2026-08-22 03:53:31 +08:00
|
|
|
|
"""
|
|
|
|
|
|
透传优化回归测试(依赖隔离:桩掉 chongming 包链、auth 与 chongming_cache)
|
|
|
|
|
|
|
|
|
|
|
|
覆盖:
|
|
|
|
|
|
A. 回复路径 bytes 透传:
|
|
|
|
|
|
1. 普通端点返回 worker 原始字节(byte-for-byte,零再序列化)+ application/json
|
|
|
|
|
|
2. 框架错误(call_raw 抛 RuntimeError)-> HTTP 503
|
|
|
|
|
|
3. 业务错误({"code":1,...} 普通回复)-> HTTP 200 透传(网关不解析)
|
|
|
|
|
|
B. validate=false 请求透传:
|
|
|
|
|
|
4. POST 信封字节拼接精确断言(网关零解析/零转义)
|
|
|
|
|
|
5. 空 body 兜底 {"meta":...,"data":{}}
|
|
|
|
|
|
6. validate=true 端点仍走 Pydantic 校验(422);validate=false 跳过校验(透传)
|
|
|
|
|
|
7. GET 端点不受 validate 影响(validate 仅对 body 端点生效)
|
|
|
|
|
|
C. OpenAPI:
|
|
|
|
|
|
8. validate=false 端点 requestBody $ref 注入 + components schema 完整
|
|
|
|
|
|
D. WorkerManager 单测(FakeSession):
|
|
|
|
|
|
9. call_raw 零解析返回原始字节;call() 仍解析为 dict(MQTT 语义保留)
|
|
|
|
|
|
10. cacheable: 缓存原始 bytes、二次命中不再发查询、业务错误不缓存
|
|
|
|
|
|
11. reply.err -> 重试 -> RuntimeError;空回复兜底 b"{}"
|
|
|
|
|
|
|
|
|
|
|
|
运行: uv run python test_passthrough_optimization.py
|
|
|
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import importlib.util
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import types
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
|
|
|
|
|
|
|
import orjson
|
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent
|
|
|
|
|
|
sys.path.insert(0, str(REPO_ROOT / "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 # 仅用于 Depends,桩掉鉴权
|
|
|
|
|
|
_cache_stub = types.ModuleType("chongming_cache")
|
|
|
|
|
|
_cache_stub.Cache = object
|
|
|
|
|
|
for mod in (_pkg, _app, _core, _logger_stub, _auth_stub, _cache_stub):
|
|
|
|
|
|
sys.modules[mod.__name__] = mod
|
|
|
|
|
|
|
|
|
|
|
|
# 加载真实 worker_manager(route_generator 依赖其 StreamIdleTimeoutError)
|
|
|
|
|
|
_wm_path = REPO_ROOT / "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]
|
|
|
|
|
|
|
2026-08-23 04:34:05 +08:00
|
|
|
|
# 加载真实 call_stats(route_generator 依赖其 spawn/record_call)
|
|
|
|
|
|
_cs_path = REPO_ROOT / "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]
|
|
|
|
|
|
|
2026-08-22 03:53:31 +08:00
|
|
|
|
# 加载真实 route_generator
|
|
|
|
|
|
_rg_path = REPO_ROOT / "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
|
|
|
|
|
|
_inject_schemas_to_openapi = _rg_mod._inject_schemas_to_openapi
|
|
|
|
|
|
WorkerManager = _wm_mod.WorkerManager
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
"""模拟 WorkerManager:call_raw 返回原始字节;error="no_worker" 时抛 RuntimeError"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, reply=b'{"result": 5}', error=None):
|
|
|
|
|
|
self.reply = reply
|
|
|
|
|
|
self.error = error
|
|
|
|
|
|
self.call_raw_payloads = [] # 捕获的预序列化请求字节
|
|
|
|
|
|
|
|
|
|
|
|
async def call_raw(self, worker_type, subject, payload, timeout=3.0, retries=1, cacheable=False):
|
|
|
|
|
|
self.call_raw_payloads.append(payload)
|
|
|
|
|
|
if self.error == "no_worker":
|
|
|
|
|
|
raise RuntimeError(f"类型 '{worker_type}' 没有可用 Worker。")
|
|
|
|
|
|
return self.reply
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ROUTES = [
|
|
|
|
|
|
# validate=true 对照(默认)
|
|
|
|
|
|
{
|
|
|
|
|
|
"subject": "demo.echo", "path": "/echo", "method": "POST",
|
|
|
|
|
|
"params": ["msg: str"], "summary": "echo",
|
|
|
|
|
|
"timeout": 3.0, "auth_required": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"subject": "demo.echo2", "path": "/echo2", "method": "POST",
|
|
|
|
|
|
"params": ["n: int"], "summary": "typed echo",
|
|
|
|
|
|
"timeout": 3.0, "auth_required": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"subject": "demo.add", "path": "/add", "method": "GET",
|
|
|
|
|
|
"params": ["a: int", "b: int"], "summary": "add",
|
|
|
|
|
|
"timeout": 3.0, "auth_required": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
# validate=false:请求体原样透传
|
|
|
|
|
|
{
|
|
|
|
|
|
"subject": "demo.raw", "path": "/raw/echo", "method": "POST",
|
|
|
|
|
|
"params": ["msg: str"], "summary": "raw echo",
|
|
|
|
|
|
"validate": False, "timeout": 3.0, "auth_required": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
# validate=false + GET 对照(validate 仅对 body 端点生效)
|
|
|
|
|
|
{
|
|
|
|
|
|
"subject": "demo.rawget", "path": "/raw/get", "method": "GET",
|
|
|
|
|
|
"params": ["a: int"], "summary": "raw get",
|
|
|
|
|
|
"validate": False, "timeout": 3.0, "auth_required": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_app(fwm):
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
extra_schemas = {}
|
|
|
|
|
|
raw_body_models = {}
|
|
|
|
|
|
register_route_group(
|
|
|
|
|
|
app, fwm, "demo", ROUTES, prefix="/api/v1",
|
|
|
|
|
|
extra_schemas=extra_schemas, raw_body_models=raw_body_models,
|
|
|
|
|
|
)
|
|
|
|
|
|
if extra_schemas:
|
|
|
|
|
|
_inject_schemas_to_openapi(app, lambda: extra_schemas, lambda: raw_body_models)
|
|
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- A. 回复路径 bytes 透传 ----
|
|
|
|
|
|
|
|
|
|
|
|
def test_reply_bytes_passthrough():
|
|
|
|
|
|
fwm = FakeWorkerManager(reply=b'{"result": 5}')
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
r = client.get("/api/v1/add?a=1&b=2")
|
|
|
|
|
|
check("GET 状态码 200", r.status_code == 200, str(r.status_code))
|
|
|
|
|
|
check(
|
|
|
|
|
|
"回复为 worker 原始字节(byte-for-byte 零再序列化)",
|
|
|
|
|
|
r.content == b'{"result": 5}',
|
|
|
|
|
|
repr(r.content),
|
|
|
|
|
|
)
|
|
|
|
|
|
check(
|
|
|
|
|
|
"Content-Type application/json",
|
|
|
|
|
|
r.headers["content-type"].startswith("application/json"),
|
|
|
|
|
|
r.headers.get("content-type", ""),
|
|
|
|
|
|
)
|
|
|
|
|
|
data = orjson.loads(fwm.call_raw_payloads[0])["data"]
|
|
|
|
|
|
check("query 参数透传到信封 data", data == {"a": 1, "b": 2}, str(data))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_framework_error_503():
|
|
|
|
|
|
fwm = FakeWorkerManager(error="no_worker")
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
r = client.get("/api/v1/add?a=1&b=2")
|
|
|
|
|
|
check("框架错误(无 worker)-> 503", r.status_code == 503, str(r.status_code))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_business_error_passthrough_200():
|
|
|
|
|
|
fwm = FakeWorkerManager(reply=b'{"code": 1, "error": "nope"}')
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
r = client.post("/api/v1/echo", json={"msg": "hi"})
|
|
|
|
|
|
check("业务错误(普通回复)-> 200 透传", r.status_code == 200, str(r.status_code))
|
|
|
|
|
|
check("业务错误体原样透传", r.content == b'{"code": 1, "error": "nope"}', repr(r.content))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- B. validate=false 请求透传 ----
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_false_envelope_splice():
|
|
|
|
|
|
fwm = FakeWorkerManager()
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
r = client.post("/api/v1/raw/echo", json={"msg": "hello"})
|
|
|
|
|
|
check("validate=false POST 状态码 200", r.status_code == 200, str(r.status_code))
|
|
|
|
|
|
expected = b'{"meta":{"method":"POST"},"data":{"msg":"hello"}}'
|
|
|
|
|
|
check(
|
|
|
|
|
|
"信封字节拼接(网关零解析/零转义)",
|
|
|
|
|
|
fwm.call_raw_payloads[0] == expected,
|
|
|
|
|
|
repr(fwm.call_raw_payloads[0]),
|
|
|
|
|
|
)
|
|
|
|
|
|
check("响应透传", r.content == b'{"result": 5}', repr(r.content))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_false_empty_body():
|
|
|
|
|
|
fwm = FakeWorkerManager()
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
r = client.post("/api/v1/raw/echo", content=b"")
|
|
|
|
|
|
check("空 body 状态码 200", r.status_code == 200, str(r.status_code))
|
|
|
|
|
|
check(
|
|
|
|
|
|
"空 body 兜底 data:{}",
|
|
|
|
|
|
fwm.call_raw_payloads[0] == b'{"meta":{"method":"POST"},"data":{}}',
|
|
|
|
|
|
repr(fwm.call_raw_payloads[0]),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_true_still_422():
|
|
|
|
|
|
fwm = FakeWorkerManager()
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
r = client.post("/api/v1/echo2", json={"n": "not-a-number"})
|
|
|
|
|
|
check("validate=true 仍 Pydantic 校验(422)", r.status_code == 422, str(r.status_code))
|
|
|
|
|
|
check("校验失败不发起调用", len(fwm.call_raw_payloads) == 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_false_skips_validation():
|
|
|
|
|
|
fwm = FakeWorkerManager()
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
r = client.post("/api/v1/raw/echo", json={"n": "not-a-number"})
|
|
|
|
|
|
check("validate=false 跳过校验(200)", r.status_code == 200, str(r.status_code))
|
|
|
|
|
|
check(
|
|
|
|
|
|
"原始 body 透传(含类型错误输入)",
|
|
|
|
|
|
fwm.call_raw_payloads[0] == b'{"meta":{"method":"POST"},"data":{"n":"not-a-number"}}',
|
|
|
|
|
|
repr(fwm.call_raw_payloads[0]),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_false_get_unaffected():
|
|
|
|
|
|
fwm = FakeWorkerManager()
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
r = client.get("/api/v1/raw/get?a=5")
|
|
|
|
|
|
check("validate=false GET 不受影响(200)", r.status_code == 200, str(r.status_code))
|
|
|
|
|
|
data = orjson.loads(fwm.call_raw_payloads[0])["data"]
|
|
|
|
|
|
check("GET 参数正常透传", data == {"a": 5}, str(data))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- C. OpenAPI ----
|
|
|
|
|
|
|
|
|
|
|
|
def test_openapi_request_body_injected():
|
|
|
|
|
|
fwm = FakeWorkerManager()
|
|
|
|
|
|
client = build_app(fwm)
|
|
|
|
|
|
schema = client.app.openapi()
|
|
|
|
|
|
|
|
|
|
|
|
# validate=false 端点:requestBody $ref 注入
|
|
|
|
|
|
raw_op = schema["paths"]["/api/v1/raw/echo"]["post"]
|
|
|
|
|
|
check(
|
|
|
|
|
|
"validate=false 端点 requestBody 已注入",
|
|
|
|
|
|
raw_op.get("requestBody", {}).get("content", {}).get("application/json", {}).get("schema", {}).get("$ref")
|
|
|
|
|
|
== "#/components/schemas/demo_raw_echo_POST_Request",
|
|
|
|
|
|
str(raw_op.get("requestBody")),
|
|
|
|
|
|
)
|
|
|
|
|
|
check(
|
|
|
|
|
|
"请求模型注册到 components/schemas",
|
|
|
|
|
|
"demo_raw_echo_POST_Request" in schema["components"]["schemas"],
|
|
|
|
|
|
)
|
|
|
|
|
|
# validate=true 端点:FastAPI 原生 requestBody(不受影响)
|
|
|
|
|
|
ok_op = schema["paths"]["/api/v1/echo"]["post"]
|
|
|
|
|
|
check(
|
|
|
|
|
|
"validate=true 端点原生 requestBody 保留",
|
|
|
|
|
|
"requestBody" in ok_op,
|
|
|
|
|
|
str(ok_op.get("requestBody")),
|
|
|
|
|
|
)
|
|
|
|
|
|
# GET 端点无 requestBody
|
|
|
|
|
|
check(
|
|
|
|
|
|
"GET 端点无 requestBody",
|
|
|
|
|
|
"requestBody" not in schema["paths"]["/api/v1/raw/get"]["get"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- D. WorkerManager 单测(FakeSession)----
|
|
|
|
|
|
|
|
|
|
|
|
class FakeSession:
|
|
|
|
|
|
"""模拟 zenoh Session:get() 返回单回复迭代器"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, payload=b'{"result": 5}', err_payload=None):
|
|
|
|
|
|
self.payload = payload
|
|
|
|
|
|
self.err_payload = err_payload
|
|
|
|
|
|
self.get_calls = 0
|
|
|
|
|
|
|
|
|
|
|
|
def get(self, *args, **kwargs):
|
|
|
|
|
|
self.get_calls += 1
|
|
|
|
|
|
if self.err_payload is not None:
|
|
|
|
|
|
reply = SimpleNamespace(ok=None, err=SimpleNamespace(payload=self.err_payload))
|
|
|
|
|
|
else:
|
|
|
|
|
|
reply = SimpleNamespace(ok=SimpleNamespace(payload=self.payload), err=None)
|
|
|
|
|
|
return iter([reply])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_wm(session, cache=None):
|
|
|
|
|
|
wm = WorkerManager(session, cache=cache if cache is not None else {})
|
|
|
|
|
|
wm._workers_cache = {"demo": ["worker/demo/w1"]}
|
|
|
|
|
|
return wm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_wm_call_raw_no_parse():
|
|
|
|
|
|
session = FakeSession(payload=b'{"result": 5}')
|
|
|
|
|
|
wm = make_wm(session)
|
|
|
|
|
|
result = await wm.call_raw("demo", "calc.add", b'{"meta":{},"data":{}}')
|
|
|
|
|
|
check(
|
|
|
|
|
|
"call_raw 返回原始字节(零解析)",
|
|
|
|
|
|
isinstance(result, bytes) and result == b'{"result": 5}',
|
|
|
|
|
|
repr(result),
|
|
|
|
|
|
)
|
|
|
|
|
|
check("session.get 仅调用 1 次", session.get_calls == 1, str(session.get_calls))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_wm_call_dict_semantics():
|
|
|
|
|
|
session = FakeSession(payload=b'{"result": 5}')
|
|
|
|
|
|
wm = make_wm(session)
|
|
|
|
|
|
result = await wm.call("demo", "calc.add", {"meta": {}, "data": {}})
|
|
|
|
|
|
check("call() 仍解析为 dict(MQTT 语义保留)", result == {"result": 5}, repr(result))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_wm_cache_bytes():
|
|
|
|
|
|
cache = {}
|
|
|
|
|
|
session = FakeSession(payload=b'{"result": 5}')
|
|
|
|
|
|
wm = make_wm(session, cache)
|
|
|
|
|
|
r1 = await wm.call_raw("demo", "calc.add", b'{"meta":{},"data":{}}', cacheable=True)
|
|
|
|
|
|
r2 = await wm.call_raw("demo", "calc.add", b'{"meta":{},"data":{}}', cacheable=True)
|
|
|
|
|
|
check("缓存命中返回原始 bytes", r1 == r2 == b'{"result": 5}', repr(r1))
|
|
|
|
|
|
check("二次调用命中缓存(不再发查询)", session.get_calls == 1, str(session.get_calls))
|
|
|
|
|
|
check("缓存值为原始 bytes", all(isinstance(v, bytes) for v in cache.values()))
|
|
|
|
|
|
|
|
|
|
|
|
# 业务错误(code!=0)不缓存
|
|
|
|
|
|
session2 = FakeSession(payload=b'{"code": 1, "error": "x"}')
|
|
|
|
|
|
wm2 = make_wm(session2, {})
|
|
|
|
|
|
await wm2.call_raw("demo", "calc.add", b'{"meta":{},"data":{}}', cacheable=True)
|
|
|
|
|
|
check("业务错误不写入缓存", len(wm2.cache) == 0, str(wm2.cache))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_wm_reply_err_retry():
|
|
|
|
|
|
session = FakeSession(err_payload=b'{"error": "boom"}')
|
|
|
|
|
|
wm = make_wm(session)
|
|
|
|
|
|
try:
|
|
|
|
|
|
await wm.call_raw("demo", "calc.add", b'{}')
|
|
|
|
|
|
check("reply.err 应抛 RuntimeError", False)
|
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
|
check("reply.err 重试后抛 RuntimeError", "boom" in str(e), str(e))
|
|
|
|
|
|
check("reply.err 触发重试(2 次查询)", session.get_calls == 2, str(session.get_calls))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_wm_empty_reply_fallback():
|
|
|
|
|
|
session = FakeSession(payload=b"")
|
|
|
|
|
|
wm = make_wm(session)
|
|
|
|
|
|
check("call_raw 空回复兜底 b\"{}\"", await wm.call_raw("demo", "calc.add", b"{}") == b"{}")
|
|
|
|
|
|
check("call() 空回复 → {}", await wm.call("demo", "calc.add", {"meta": {}, "data": {}}) == {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run():
|
|
|
|
|
|
# HTTP 层(TestClient)
|
|
|
|
|
|
test_reply_bytes_passthrough()
|
|
|
|
|
|
test_framework_error_503()
|
|
|
|
|
|
test_business_error_passthrough_200()
|
|
|
|
|
|
test_validate_false_envelope_splice()
|
|
|
|
|
|
test_validate_false_empty_body()
|
|
|
|
|
|
test_validate_true_still_422()
|
|
|
|
|
|
test_validate_false_skips_validation()
|
|
|
|
|
|
test_validate_false_get_unaffected()
|
|
|
|
|
|
test_openapi_request_body_injected()
|
|
|
|
|
|
|
|
|
|
|
|
# WorkerManager 层(FakeSession)
|
|
|
|
|
|
asyncio.run(test_wm_call_raw_no_parse())
|
|
|
|
|
|
asyncio.run(test_wm_call_dict_semantics())
|
|
|
|
|
|
asyncio.run(test_wm_cache_bytes())
|
|
|
|
|
|
asyncio.run(test_wm_reply_err_retry())
|
|
|
|
|
|
asyncio.run(test_wm_empty_reply_fallback())
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n结果: {len(failures)} 个失败")
|
|
|
|
|
|
return 1 if failures else 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
sys.exit(run())
|