Files

227 lines
7.7 KiB
Python
Raw Permalink Normal View History

"""
插件专属依赖清单(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("✅ 全部通过")