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("✅ 全部通过")
|