modify: 测试

This commit is contained in:
2026-08-23 05:10:27 +08:00
parent a1be33376e
commit eeffc06841
24 changed files with 2 additions and 2 deletions
+180
View File
@@ -0,0 +1,180 @@
"""
真实 Zenoh 会话下的流式端到端测试(peer 模式,无需 router)
单进程内起两个 Zenoh 会话(listen + connect),跑通完整生产链路:
@rpc_method(stream=True) 注册 -> create_worker 声明 Queryable
-> WorkerManager.call_stream 消费多回复 -> 逐块到达
验证:
1. 流式方法:3+1 块按序到达(真实 Zenoh 多回复 + ConsolidationMode.NONE)
2. 流式中途异常:{"error": ...} 收尾块转发后终止
3. 非流式方法:仍是单回复(兼容性回归)
4. generate_route_configs 输出 stream 标记、非流式不含
5. decorators 对生成器函数的调用不消费生成器
运行: uv run python test_stream_e2e_zenoh.py
"""
import asyncio
import importlib.util
import logging
import sys
import types
from pathlib import Path
import orjson
import zenoh
REPO_ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(REPO_ROOT / "utils" / "worker" / "src"))
sys.path.insert(0, str(REPO_ROOT / "src"))
# ---- 网关依赖隔离(同 test_stream_gateway):桩掉 chongming 包链与 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)
_cache_stub = types.ModuleType("chongming_cache")
_cache_stub.Cache = object
for mod in (_pkg, _app, _core, _logger_stub, _cache_stub):
sys.modules[mod.__name__] = mod
_wm_path = REPO_ROOT / "src" / "chongming" / "app" / "core" / "worker_manager.py"
_spec = importlib.util.spec_from_file_location(
"chongming.app.core.worker_manager", _wm_path
)
_wm_mod = importlib.util.module_from_spec(_spec)
sys.modules["chongming.app.core.worker_manager"] = _wm_mod
_spec.loader.exec_module(_wm_mod) # type: ignore[union-attr]
WorkerManager = _wm_mod.WorkerManager
StreamIdleTimeoutError = _wm_mod.StreamIdleTimeoutError
from chongming_worker import clear_registry, rpc_method # noqa: E402
from chongming_worker.decorators import generate_route_configs # noqa: E402
from chongming_worker.runtime import create_worker # noqa: E402
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)
LISTEN_PORT = 7447
LISTEN = f"tcp/127.0.0.1:{LISTEN_PORT}"
def open_session(listen=None):
"""peer 模式会话:worker 侧 listen,网关侧 connect(显式端点,禁用组播)"""
c = zenoh.Config()
c.insert_json5("scouting/multicast/enabled", "false")
if listen:
c.insert_json5("listen/endpoints", f'["{listen}"]')
else:
c.insert_json5("connect/endpoints", f'["{LISTEN}"]')
return zenoh.open(c)
async def run_checks():
clear_registry()
# ---- 流式端点(返回异步生成器)----
@rpc_method(subject="streamtest.echo", method="POST", stream=True)
async def echo_stream(prompt: str = "hi", tokens: int = 3):
for i in range(tokens):
await asyncio.sleep(0.01)
yield {"token": f"{prompt}-{i}"}
yield {"done": True}
# ---- 流式端点:中途异常(框架应发 {"error": ...} 收尾块)----
@rpc_method(subject="streamtest.fail", method="POST", stream=True)
async def fail_stream():
yield {"token": "a"}
raise ValueError("mid-boom")
# ---- 非流式端点(兼容性对照)----
@rpc_method(subject="streamtest.add", method="GET")
async def add(a: float, b: float):
return a + b
# 1. 配置生成:流式标记
configs = generate_route_configs("streamtest")
stream_cfg = next(c for c in configs if c["subject"] == "streamtest.echo")
check("generate_route_configs 输出 stream 标记", stream_cfg.get("stream") is True)
add_cfg = next(c for c in configs if c["subject"] == "streamtest.add")
check("非流式配置不含 stream 标记", "stream" not in add_cfg)
# 2. 真实 Zenoh 会话(peer 模式,无 router)
session_worker = open_session(listen=LISTEN)
session_gw = open_session()
# 3. 启动真实 Worker(create_worker 声明 Queryable)
worker_task = asyncio.create_task(
create_worker(session_worker, "streamtest", "w1")
)
await asyncio.sleep(0.5) # 等待 queryable 在 peer 间可见
try:
# 4. 网关 WorkerManager.call_stream 真实消费流式端点
wm = WorkerManager(session_gw)
wm.register_worker("streamtest", "w1")
async def _consume(subject, data, idle_timeout=2.0):
gen = await wm.call_stream(
"streamtest", subject,
{"meta": {"method": "POST"}, "data": data},
timeout=10.0, idle_timeout=idle_timeout,
)
return [orjson.loads(b) async for b in gen]
chunks = await _consume("streamtest.echo", {"prompt": "hi", "tokens": 3})
check(
"流式 3+1 块按序到达(真实多回复)",
chunks == [
{"token": "hi-0"}, {"token": "hi-1"}, {"token": "hi-2"}, {"done": True},
],
f"chunks={chunks}",
)
# 5. 流式中途异常:错误块收尾
chunks = await _consume("streamtest.fail", {})
check(
"流式中途异常 error 块收尾",
chunks == [{"token": "a"}, {"error": "mid-boom"}],
f"chunks={chunks}",
)
# 6. 非流式兼容:call() 仍单回复
result = await wm.call(
"streamtest", "streamtest.add",
{"meta": {"method": "GET"}, "data": {"a": 2, "b": 3}},
timeout=10.0,
)
check("非流式 call() 正常单回复", result == 5.0, f"result={result}")
# 7. 负载均衡路径兼容:liveliness 发现的 worker 也能流式(同 call)
worker_expr = await wm.get_worker("streamtest")
check("get_worker 仍可解析本地 worker", worker_expr is not None)
finally:
worker_task.cancel()
await asyncio.gather(worker_task, return_exceptions=True)
session_gw.close()
session_worker.close()
def run():
asyncio.run(run_checks())
print(f"\n结果: {len(failures)} 个失败")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(run())