Files
chongming-core/test/test_plugin_deps.py

227 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
插件专属依赖清单(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("✅ 全部通过")