227 lines
7.7 KiB
Python
227 lines
7.7 KiB
Python
"""
|
||||
|
|
插件专属依赖清单(plugins_deps.toml)测试
|
|||
|
|
|
|||
|
|
"插件专属依赖跟插件走、不混进网关构建配置"这条约定的守卫:
|
|||
|
|
1. 仓库清单可解析:doubao 专属(openai / volcengine / bytedance)与共用(httpx)
|
|||
|
|
都能被 build 的读取/合并逻辑读出来
|
|||
|
|
2. 收集清单 = config.toml [pyinstaller_build_import] + plugins_deps.toml,
|
|||
|
|
去重且网关本体依赖在前;文件缺失时不报错、不追加
|
|||
|
|
3. 拆分不回潮:插件专属依赖不再出现在 config.toml 里(防止又手工加回网关清单)
|
|||
|
|
4. 配置写错(imports 非字符串列表 / 顶层多余键 / 插件节缺 imports)直接抛错,
|
|||
|
|
不静默漏收依赖(漏了要到运行期加载插件才 ImportError)
|
|||
|
|
5. venv 里 chongming-cli 版本低于本地源码时告警:打包用的是 venv 里的 CLI,
|
|||
|
|
不先发布升级,plugins_deps.toml 不会生效
|
|||
|
|
|
|||
|
|
运行: uv run python test/test_plugin_deps.py
|
|||
|
|
"""
|
|||
|
|
import importlib.metadata
|
|||
|
|
import os
|
|||
|
|
import shutil
|
|||
|
|
import sys
|
|||
|
|
import tempfile
|
|||
|
|
import tomllib
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
ROOT = Path(__file__).parent.parent
|
|||
|
|
sys.path.insert(0, str(ROOT / "utils" / "cli" / "src"))
|
|||
|
|
|
|||
|
|
DOUBAO_DEPS = {
|
|||
|
|
"openai",
|
|||
|
|
"volcenginesdkarkruntime",
|
|||
|
|
"bytedance",
|
|||
|
|
"bytedance.volcengine_aicc_sdk",
|
|||
|
|
"bytedance.volcengine_aicc_sdk.secure_http_client",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# build 的根目录定位支持 CHONGMING_ROOT 环境变量,测试结论不应受它影响
|
|||
|
|
os.environ.pop("CHONGMING_ROOT", None)
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from chongming_cli import build as b # noqa: E402
|
|||
|
|
|
|||
|
|
HAVE_CLI = True
|
|||
|
|
except ImportError as exc: # utils/cli 子仓库未就位(干净克隆)时只跑清单一致性检查
|
|||
|
|
HAVE_CLI = False
|
|||
|
|
print(f"[SKIP] chongming_cli 源码不可用({exc}):仅跑清单一致性检查")
|
|||
|
|
|
|||
|
|
cwd = Path.cwd()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_repo_deps() -> dict:
|
|||
|
|
"""读仓库的 plugins_deps.toml(不依赖 chongming_cli,一致性检查也可用)。"""
|
|||
|
|
with open(ROOT / "plugins_deps.toml", "rb") as f:
|
|||
|
|
return tomllib.load(f)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def flat_modules(data: dict) -> set[str]:
|
|||
|
|
"""plugins_deps.toml 里的全部模块(共用 imports + 各插件节)。"""
|
|||
|
|
mods = set(data.get("imports", []))
|
|||
|
|
for value in data.values():
|
|||
|
|
if isinstance(value, dict):
|
|||
|
|
mods |= set(value.get("imports", []))
|
|||
|
|
return mods
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ========== 1. 仓库清单可解析(build 的读取逻辑直测) ==========
|
|||
|
|
if HAVE_CLI:
|
|||
|
|
os.chdir(ROOT)
|
|||
|
|
try:
|
|||
|
|
modules, summary = b._load_plugin_imports()
|
|||
|
|
finally:
|
|||
|
|
os.chdir(cwd)
|
|||
|
|
|
|||
|
|
check(
|
|||
|
|
"仓库 plugins_deps.toml 可解析且含 doubao 专属依赖",
|
|||
|
|
DOUBAO_DEPS <= set(modules),
|
|||
|
|
f"缺失: {sorted(DOUBAO_DEPS - set(modules))}",
|
|||
|
|
)
|
|||
|
|
check("共用节(httpx)并入清单", "httpx" in modules)
|
|||
|
|
check("摘要含 doubao 节名", "doubao" in summary, summary)
|
|||
|
|
|
|||
|
|
# ========== 2. 合并:网关本体在前、去重保序 ==========
|
|||
|
|
fake_config = {
|
|||
|
|
"pyinstaller_build_import": {
|
|||
|
|
"base": ["fastapi", "openai"], # openai 故意与插件清单重复,验证去重
|
|||
|
|
"linux": ["uvloop"],
|
|||
|
|
"windows": ["win_only"],
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
os.chdir(ROOT)
|
|||
|
|
try:
|
|||
|
|
merged, _ = b._collect_build_imports(fake_config)
|
|||
|
|
finally:
|
|||
|
|
os.chdir(cwd)
|
|||
|
|
|
|||
|
|
check(
|
|||
|
|
"合并后网关本体依赖在前且 openai 只出现一次",
|
|||
|
|
merged[0] == "fastapi" and merged[1] == "openai" and merged.count("openai") == 1,
|
|||
|
|
str(merged[:6]),
|
|||
|
|
)
|
|||
|
|
check(
|
|||
|
|
"插件专属依赖追加在网关清单之后",
|
|||
|
|
"httpx" in merged and merged.index("httpx") > merged.index("openai"),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ========== 3. 拆分不回潮:config.toml 里不应再有插件专属依赖 ==========
|
|||
|
|
repo_modules = flat_modules(load_repo_deps())
|
|||
|
|
with open(ROOT / "config.toml", "rb") as f:
|
|||
|
|
cfg = tomllib.load(f)
|
|||
|
|
bcfg = cfg["pyinstaller_build_import"]
|
|||
|
|
gateway_imports = set(bcfg["base"]) | set(bcfg["linux"]) | set(bcfg["windows"])
|
|||
|
|
|
|||
|
|
check(
|
|||
|
|
"doubao 专属依赖已从 config.toml 移出(只留 plugins_deps.toml)",
|
|||
|
|
not (DOUBAO_DEPS & gateway_imports),
|
|||
|
|
f"回潮: {sorted(DOUBAO_DEPS & gateway_imports)}",
|
|||
|
|
)
|
|||
|
|
check(
|
|||
|
|
"两份清单不重叠(插件专属 vs 网关本体)",
|
|||
|
|
not (repo_modules & gateway_imports),
|
|||
|
|
f"重叠: {sorted(repo_modules & gateway_imports)}",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ========== 4. 文件缺失 / 写错的行为 ==========
|
|||
|
|
tmp = Path(tempfile.mkdtemp(prefix="plugin_deps_test_"))
|
|||
|
|
(tmp / "pyproject.toml").write_text("[project]\nname = 'x'\n", encoding="utf-8")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if HAVE_CLI:
|
|||
|
|
(tmp / "plugins_deps.toml").unlink(missing_ok=True)
|
|||
|
|
cfg_only = {"pyinstaller_build_import": {"base": ["fastapi"], "linux": [], "windows": []}}
|
|||
|
|
os.chdir(tmp)
|
|||
|
|
try:
|
|||
|
|
mods, summary = b._load_plugin_imports()
|
|||
|
|
merged, _ = b._collect_build_imports(cfg_only)
|
|||
|
|
finally:
|
|||
|
|
os.chdir(cwd)
|
|||
|
|
check(
|
|||
|
|
"无 plugins_deps.toml:不报错、不追加",
|
|||
|
|
mods == [] and summary == "" and merged == ["fastapi"],
|
|||
|
|
f"{mods} / {merged}",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def expect_error(name: str, text: str) -> None:
|
|||
|
|
(tmp / "plugins_deps.toml").write_text(text, encoding="utf-8")
|
|||
|
|
os.chdir(tmp)
|
|||
|
|
try:
|
|||
|
|
try:
|
|||
|
|
b._load_plugin_imports()
|
|||
|
|
check(name, False, "未抛错")
|
|||
|
|
except ValueError as exc:
|
|||
|
|
check(name, True, str(exc))
|
|||
|
|
finally:
|
|||
|
|
os.chdir(cwd)
|
|||
|
|
|
|||
|
|
expect_error("imports 非列表 → 报错", 'imports = "openai"\n')
|
|||
|
|
expect_error("imports 含非字符串 → 报错", "[doubao]\nimports = [1, 2]\n")
|
|||
|
|
expect_error("插件节缺 imports(如写成 import)→ 报错", "[doubao]\nimport = ['openai']\n")
|
|||
|
|
expect_error("顶层多余标量键 → 报错", "foo = 1\n")
|
|||
|
|
|
|||
|
|
# 合法多节:共用 + 逐插件(去重发生在合并阶段,读取阶段保持原样)
|
|||
|
|
(tmp / "plugins_deps.toml").write_text(
|
|||
|
|
'imports = ["httpx"]\n\n[alpha]\nimports = ["pkg_a"]\n\n'
|
|||
|
|
'[beta]\nimports = ["pkg_b", "pkg_a"]\n',
|
|||
|
|
encoding="utf-8",
|
|||
|
|
)
|
|||
|
|
os.chdir(tmp)
|
|||
|
|
try:
|
|||
|
|
mods, summary = b._load_plugin_imports()
|
|||
|
|
finally:
|
|||
|
|
os.chdir(cwd)
|
|||
|
|
check(
|
|||
|
|
"多节解析:共用 + 逐插件均并入,摘要含各节",
|
|||
|
|
mods == ["httpx", "pkg_a", "pkg_b", "pkg_a"]
|
|||
|
|
and "alpha:1" in summary
|
|||
|
|
and "beta:2" in summary,
|
|||
|
|
f"{mods} / {summary}",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|||
|
|
|
|||
|
|
# ========== 5. venv 里的 CLI 版本(打包装箱用的是它,不是本文件旁边的源码) ==========
|
|||
|
|
local_ver = tomllib.loads((ROOT / "utils" / "cli" / "pyproject.toml").read_text())[
|
|||
|
|
"project"
|
|||
|
|
]["version"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def as_tuple(v: str):
|
|||
|
|
return tuple(int(x) for x in v.split("."))
|
|||
|
|
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
installed_ver = importlib.metadata.version("chongming-cli")
|
|||
|
|
except importlib.metadata.PackageNotFoundError:
|
|||
|
|
installed_ver = None
|
|||
|
|
|
|||
|
|
if installed_ver is None:
|
|||
|
|
print("[WARN] venv 未安装 chongming-cli:先 uv sync 或发布后再构建")
|
|||
|
|
else:
|
|||
|
|
try:
|
|||
|
|
outdated = as_tuple(installed_ver) < as_tuple(local_ver)
|
|||
|
|
except ValueError: # 非常规版本号(如 rc/后缀),不猜,直接提示
|
|||
|
|
outdated = True
|
|||
|
|
if outdated:
|
|||
|
|
print(
|
|||
|
|
f"[WARN] venv 里 chongming-cli {installed_ver} < 源码 {local_ver}:"
|
|||
|
|
"plugins_deps.toml 需新版 build 才生效 —— 发布 chongming-cli 后执行 "
|
|||
|
|
"uv lock --upgrade-package chongming-cli && uv sync"
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
check(f"venv chongming-cli {installed_ver} ≥ 源码 {local_ver}", True)
|
|||
|
|
|
|||
|
|
print("\n" + ("=" * 40))
|
|||
|
|
if failures:
|
|||
|
|
print(f"❌ {len(failures)} 项未通过: {failures}")
|
|||
|
|
sys.exit(1)
|
|||
|
|
print("✅ 全部通过")
|