""" 透传优化回归测试(依赖隔离:桩掉 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 random 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.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 _cache_stub.DistributedLock = object # worker_manager 顶层导入(本测试不触达) 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] # 加载真实 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] # 加载真实 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, instance_id=None): 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() 返回单回复迭代器 err_payload 非空时整体报错;err_target 给定时只对该 key 前缀报错(其余正常回复), 用于验证「失败换副本重试」。 """ def __init__(self, payload=b'{"result": 5}', err_payload=None, err_target=None): self.payload = payload self.err_payload = err_payload self.err_target = err_target self.get_calls = 0 self.call_keys = [] def get(self, *args, **kwargs): self.get_calls += 1 key = args[0] if args else kwargs.get("key_expr", "") self.call_keys.append(str(key)) failing = self.err_payload is not None and ( self.err_target is None or str(key).startswith(self.err_target) ) if failing: 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("单副本无其它可换时不空转重试(1 次查询)", session.get_calls == 1, str(session.get_calls)) async def test_wm_reply_err_switches_worker(): """多副本:先打的副本报错则换另一个副本重试,且不重复打已失败的副本。 首个副本由 random.choice 决定,故对多个 seed 试跑:必有一个覆盖「先失败后成功」 路径(另一个覆盖「首个即成功」),两条路径都不允许重复打同一个失败的副本。 """ ok, detail = False, "无 seed 覆盖到失败换副本路径" for seed in (0, 1): random.seed(seed) session = FakeSession( payload=b'{"result": 5}', err_payload=b'{"error": "boom"}', err_target="worker/demo/w-dead", ) wm = make_wm(session) wm._workers_cache = {"demo": ["worker/demo/w-dead", "worker/demo/w-alive"]} try: result = await wm.call_raw("demo", "calc.add", b'{}', retries=2) except RuntimeError as e: # 未换到正常副本(不该发生) detail = f"seed={seed} 意外失败: {e}" continue assert result == b'{"result": 5}', result dead_calls = session.call_keys.count("worker/demo/w-dead/calc.add") if dead_calls > 1: detail = f"seed={seed} 重复打失败副本: {session.call_keys}" continue if session.call_keys == [ "worker/demo/w-dead/calc.add", "worker/demo/w-alive/calc.add", ]: ok, detail = True, f"seed={seed} keys={session.call_keys}" check("失败副本 -> 换副本重试成功(且不重复打失败副本)", ok, detail) 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_reply_err_switches_worker()) asyncio.run(test_wm_empty_reply_fallback()) print(f"\n结果: {len(failures)} 个失败") return 1 if failures else 0 if __name__ == "__main__": sys.exit(run())