2026-09-18 15:22:53 +08:00
|
|
|
|
"""
|
|
|
|
|
|
inline 插件 [workers.config] 对齐(网关侧兜底)单测 —— WorkerManager.align_inline_config。
|
|
|
|
|
|
|
|
|
|
|
|
网关侧在创建 inline 副本之前,按「配置优先来源模式」(根 config.toml 的
|
|
|
|
|
|
[cache].launch_worker_count)对齐插件配置:cache 模式取共享缓存里的段,
|
|
|
|
|
|
toml 模式以 config/workers/<type>/config.toml 为准并覆盖缓存。目的是让网关与
|
|
|
|
|
|
worker 包版本解耦——worker 包仍是旧版时,旧版 on_init 只拿到 toml 展平配置,
|
|
|
|
|
|
会出现“前端显示缓存值、实际跑 toml”。
|
|
|
|
|
|
|
|
|
|
|
|
用 dict 作缓存桩,验证两种模式下插件拿到的配置与缓存/文件两侧的取值一致。
|
|
|
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
2026-10-13 17:29:38 +08:00
|
|
|
|
sys.path.insert(0, os.path.join(REPO, "gateway", "src"))
|
2026-09-18 15:22:53 +08:00
|
|
|
|
os.chdir(REPO) # 网关模块导入期读根 config.toml / 日志配置
|
|
|
|
|
|
|
|
|
|
|
|
from chongming.app.core.worker_manager import _CONFIGS_KEY, WorkerManager # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
PASS = 0
|
|
|
|
|
|
FAIL = 0
|
|
|
|
|
|
WT = "aibox_sophon_adapter"
|
|
|
|
|
|
|
|
|
|
|
|
# _discover_plugins 展平出来的形态(toml 值)
|
|
|
|
|
|
FLAT = {"type": WT, "count": 1, "data_dir": "./data", "mediamtx_addr": "rtsp://toml:8554"}
|
|
|
|
|
|
# 该插件 config.toml 的 [workers.config] 原始段
|
|
|
|
|
|
SECTION = {"data_dir": "./data", "mediamtx_addr": "rtsp://toml:8554", "heartbeat": {"timeout": 15}}
|
|
|
|
|
|
# 前端下发后缓存里的段(改过 mediamtx_addr 与 heartbeat)
|
|
|
|
|
|
CACHED = {"data_dir": "./data", "mediamtx_addr": "rtsp://cache:9999", "heartbeat": {"timeout": 30}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def report(name, cond, detail=""):
|
|
|
|
|
|
global PASS, FAIL
|
|
|
|
|
|
if cond:
|
|
|
|
|
|
PASS += 1
|
|
|
|
|
|
print(f"✅ {name}" + (f" — {detail}" if detail else ""))
|
|
|
|
|
|
else:
|
|
|
|
|
|
FAIL += 1
|
|
|
|
|
|
print(f"❌ {name}" + (f" — {detail}" if detail else ""))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def manager(cache, mode):
|
|
|
|
|
|
m = WorkerManager.__new__(WorkerManager) # 免会话构造:仅用到 cache + config_mode
|
|
|
|
|
|
m.cache = cache
|
|
|
|
|
|
m._launch_worker_count = mode
|
|
|
|
|
|
return m
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run() -> int:
|
|
|
|
|
|
key = _CONFIGS_KEY.format(type=WT)
|
|
|
|
|
|
|
|
|
|
|
|
# ── 1. cache 模式:缓存有段 → 插件拿到缓存值(本次修复目标)──────────
|
|
|
|
|
|
cache = {key: dict(CACHED)}
|
|
|
|
|
|
got = manager(cache, "cache").align_inline_config(WT, dict(FLAT), dict(SECTION))
|
|
|
|
|
|
report(
|
|
|
|
|
|
"cache 模式:插件配置取缓存值(覆盖 toml 同名键)",
|
|
|
|
|
|
got.get("mediamtx_addr") == "rtsp://cache:9999"
|
|
|
|
|
|
and got.get("heartbeat") == {"timeout": 30},
|
|
|
|
|
|
f"mediamtx_addr={got.get('mediamtx_addr')}",
|
|
|
|
|
|
)
|
|
|
|
|
|
report("cache 模式:不覆盖已存在的缓存段", cache[key] == CACHED, f"cache={cache[key]}")
|
|
|
|
|
|
|
|
|
|
|
|
# ── 2. cache 模式:缓存缺失 → 用 toml 段引导并写回 ──────────────────
|
|
|
|
|
|
cache2 = {}
|
|
|
|
|
|
got2 = manager(cache2, "cache").align_inline_config(WT, dict(FLAT), dict(SECTION))
|
|
|
|
|
|
report(
|
|
|
|
|
|
"cache 模式:缓存缺失用 config.toml 引导并写回",
|
|
|
|
|
|
cache2.get(key) == SECTION and got2.get("mediamtx_addr") == "rtsp://toml:8554",
|
|
|
|
|
|
f"cache={cache2.get(key)}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ── 3. toml 模式:以文件为准并覆盖缓存(前端随之显示文件值)─────────
|
|
|
|
|
|
cache3 = {key: dict(CACHED)}
|
|
|
|
|
|
got3 = manager(cache3, "toml").align_inline_config(WT, dict(FLAT), dict(SECTION))
|
|
|
|
|
|
report(
|
|
|
|
|
|
"toml 模式:插件配置为 toml 值",
|
|
|
|
|
|
got3.get("mediamtx_addr") == "rtsp://toml:8554",
|
|
|
|
|
|
f"mediamtx_addr={got3.get('mediamtx_addr')}",
|
|
|
|
|
|
)
|
|
|
|
|
|
report(
|
|
|
|
|
|
"toml 模式:启动时用 toml 覆盖缓存(前端与运行值一致)",
|
|
|
|
|
|
cache3.get(key) == SECTION,
|
|
|
|
|
|
f"cache={cache3.get(key)}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ── 4. 边界:无缓存后端 / 无段(旧 config/workers/config.toml 兼容路径)──
|
|
|
|
|
|
cache4 = {key: dict(CACHED)}
|
|
|
|
|
|
m4 = manager(cache4, "cache")
|
|
|
|
|
|
m4.cache = None
|
|
|
|
|
|
report(
|
|
|
|
|
|
"无缓存后端:原样返回(经典 toml 行为)",
|
|
|
|
|
|
m4.align_inline_config(WT, dict(FLAT), dict(SECTION)) == FLAT
|
|
|
|
|
|
and cache4[key] == CACHED,
|
|
|
|
|
|
)
|
|
|
|
|
|
cache5 = {}
|
|
|
|
|
|
got5 = manager(cache5, "toml").align_inline_config(WT, dict(FLAT), None)
|
|
|
|
|
|
report("无 [workers.config] 段(旧写法):不写缓存、原样返回", got5 == FLAT and not cache5)
|
|
|
|
|
|
|
|
|
|
|
|
# ── 5. 与 SDK 侧一致:缓存键必须相同;SDK 为本地版本时才比对解析结果 ──
|
|
|
|
|
|
# 注意:venv 里的 chongming-worker 可能是索引里的发布版(uv sync 会把可编辑安装
|
|
|
|
|
|
# 换回锁文件版本),新版才有 resolve_startup_config / 正确的 toml 分支语义。
|
|
|
|
|
|
sys.path.insert(0, os.path.join(REPO, "utils", "worker", "src"))
|
|
|
|
|
|
import chongming_worker.runtime as r # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
report(
|
|
|
|
|
|
"缓存键与 SDK 一致(两端必须读写同一键)",
|
|
|
|
|
|
r._configs_cache_key(WT) == _CONFIGS_KEY.format(type=WT),
|
|
|
|
|
|
f"sdk={r._configs_cache_key(WT)}",
|
|
|
|
|
|
)
|
|
|
|
|
|
local_src = os.path.join(REPO, "utils", "worker", "src") in os.path.abspath(r.__file__)
|
|
|
|
|
|
if not local_src:
|
|
|
|
|
|
print(f"⚠️ 跳过解析结果比对:当前 SDK 为 {r.__file__}(非本地源码,缺本次改动)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
c = {r._configs_cache_key(WT): dict(CACHED)}
|
|
|
|
|
|
sdk = r.resolve_runtime_config(WT, dict(FLAT), c, "cache")
|
|
|
|
|
|
report(
|
|
|
|
|
|
"cache 模式:网关侧兜底与 SDK 解析结果一致",
|
|
|
|
|
|
sdk.get("mediamtx_addr") == CACHED["mediamtx_addr"],
|
|
|
|
|
|
f"sdk={sdk.get('mediamtx_addr')}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ── 6. 端到端:网关对齐 + 插件初始化(部署组合:网关新、worker 包为发布版)──
|
|
|
|
|
|
# 该断言即线上现象的复现与验证:插件 on_init 拿到的必须是网关对齐后的缓存值,
|
|
|
|
|
|
# 与 venv 里 chongming-worker 是否为含本次改动的版本无关。
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
|
|
from chongming_worker.plugin import Plugin # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
seen = {}
|
|
|
|
|
|
|
|
|
|
|
|
class DemoPlugin(Plugin):
|
|
|
|
|
|
type = WT
|
|
|
|
|
|
auto_bind = False
|
|
|
|
|
|
|
|
|
|
|
|
async def _bootstrap(self): # 隔离数据库/对象存储引导,只看配置来源
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
async def on_init(self):
|
|
|
|
|
|
seen["on_init"] = self.config.get("mediamtx_addr")
|
|
|
|
|
|
|
|
|
|
|
|
def _create_workers_impl(self, session, config, count=1, worker_manager=None):
|
|
|
|
|
|
seen["impl"] = (config or {}).get("mediamtx_addr")
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
aligned = manager({key: dict(CACHED)}, "cache").align_inline_config(
|
|
|
|
|
|
WT, dict(FLAT), dict(SECTION)
|
|
|
|
|
|
)
|
|
|
|
|
|
asyncio.run(DemoPlugin().create_workers(None, aligned, 1))
|
|
|
|
|
|
report(
|
|
|
|
|
|
"端到端:网关对齐后插件 on_init 读到缓存值(不依赖 worker 包版本)",
|
|
|
|
|
|
seen.get("on_init") == CACHED["mediamtx_addr"] == seen.get("impl"),
|
|
|
|
|
|
f"on_init={seen.get('on_init')}, impl={seen.get('impl')}, "
|
|
|
|
|
|
f"sdk={os.path.abspath(__import__('chongming_worker').__file__)}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
print("\n" + "=" * 56)
|
|
|
|
|
|
print(f"测试结果: ✅ {PASS} 通过 ❌ {FAIL} 失败")
|
|
|
|
|
|
return 0 if FAIL == 0 else 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
sys.exit(run())
|