test(plugins): align repository imports with runtime namespace (#6368)

This commit is contained in:
InfinityPacer
2026-08-20 19:01:02 +08:00
committed by GitHub
parent 2e5cc6c669
commit cbdde6443d
2 changed files with 64 additions and 12 deletions
+15 -12
View File
@@ -2,8 +2,8 @@
主程序 ``tests/conftest.py`` 与各插件仓的极薄 shim``tests/_bootstrap.py``,仅负责把
后端定位并加入 ``sys.path``)都委托到这里,使「隔离 CONFIG_DIR / 建表 / 注入插件目录 /
按目录打 v1·v2 marker / 退出清理」等引导逻辑只在主程序维护一处,所有消费方行为与修复一致。
其中 :func:`isolate_config_dir` 为主程序与插件仓共用,``prepare_v1/v2_backend`` 与
按目录打 v1·v2·v3 marker / 退出清理」等引导逻辑只在主程序维护一处,所有消费方行为与修复一致。
其中 :func:`isolate_config_dir` 为主程序与插件仓共用,``prepare_v1/v2/v3_backend`` 与
:func:`mark_plugin_generation` 为插件仓专用。
本模块只依赖标准库,``import`` 期不触发 ``app.*``:调用方可安全地「先 import 本模块、
@@ -112,11 +112,14 @@ def isolate_config_dir() -> str:
return tmp
def _prepend_sys_path(path: Path) -> None:
"""把目录前置到 ``sys.path``(去重),使其内顶层包可被导入。"""
def _expose_plugin_source(path: Path) -> None:
"""让插件仓源码通过生产运行时的 ``app.plugins.<id>`` 命名空间导入。"""
from importlib import import_module
plugins_package = import_module("app.plugins")
value = str(path)
if value not in sys.path:
sys.path.insert(0, value)
if value not in plugins_package.__path__:
plugins_package.__path__.insert(0, value)
def ensure_sites_stub() -> None:
@@ -188,7 +191,7 @@ def prepare_backend() -> None:
def prepare_v2_backend(plugins_repo: Path) -> None:
"""v2 插件单测引导:``prepare_backend`` + 把 ``<repo>/plugins.v2`` 注入 ``sys.path``
"""v2 插件单测引导:准备后端并暴露 ``<repo>/plugins.v2`` 源码
与 :func:`prepare_v1_backend` 互斥:v1/v2 存在同名插件包,同一进程同时加载会相互覆盖,
须在各自独立的 pytest 会话中运行。
@@ -196,11 +199,11 @@ def prepare_v2_backend(plugins_repo: Path) -> None:
:param plugins_repo: 插件仓根目录(由调用方 shim 传入)
"""
prepare_backend()
_prepend_sys_path(Path(plugins_repo) / "plugins.v2")
_expose_plugin_source(Path(plugins_repo) / "plugins.v2")
def prepare_v3_backend(plugins_repo: Path) -> None:
"""v3 插件单测引导:``prepare_backend`` + 把 ``<repo>/plugins.v3`` 注入 ``sys.path``
"""v3 插件单测引导:准备后端并暴露 ``<repo>/plugins.v3`` 源码
v3 插件与旧代插件可能存在同名包,必须在独立 pytest 会话中加载,避免 Python 模块
缓存把其它代际实现复用到当前测试进程。
@@ -208,16 +211,16 @@ def prepare_v3_backend(plugins_repo: Path) -> None:
:param plugins_repo: 插件仓根目录(由调用方 shim 传入)
"""
prepare_backend()
_prepend_sys_path(Path(plugins_repo) / "plugins.v3")
_expose_plugin_source(Path(plugins_repo) / "plugins.v3")
def prepare_v1_backend(plugins_repo: Path) -> None:
"""v1 插件单测引导:``prepare_backend`` + 把 ``<repo>/plugins`` 注入 ``sys.path``(与 v2 互斥)。
"""v1 插件单测引导:准备后端并暴露 ``<repo>/plugins`` 源码(与 v2 互斥)。
:param plugins_repo: 插件仓根目录(由调用方 shim 传入)
"""
prepare_backend()
_prepend_sys_path(Path(plugins_repo) / "plugins")
_expose_plugin_source(Path(plugins_repo) / "plugins")
def mark_plugin_generation(items, pytest_module) -> None:
@@ -0,0 +1,49 @@
"""验证插件仓测试引导复现生产运行时的插件导入命名空间。"""
from importlib import import_module
from pathlib import Path
import sys
import pytest
from app.testing import bootstrap
@pytest.mark.parametrize(
("prepare_name", "source_dir"),
(
("prepare_v1_backend", "plugins"),
("prepare_v2_backend", "plugins.v2"),
("prepare_v3_backend", "plugins.v3"),
),
)
def test_prepare_plugin_backend_exposes_runtime_namespace(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
prepare_name: str,
source_dir: str,
) -> None:
"""市场仓源码只通过 ``app.plugins.<id>`` 规范身份导入。"""
plugin_id = f"namespace_probe_{source_dir.replace('.', '_')}"
plugin_dir = tmp_path / source_dir / plugin_id
plugin_dir.mkdir(parents=True)
(plugin_dir / "__init__.py").write_text(
f"PLUGIN_ID = {plugin_id!r}\n",
encoding="utf-8",
)
plugins_package = import_module("app.plugins")
monkeypatch.setattr(plugins_package, "__path__", list(plugins_package.__path__))
monkeypatch.setattr(bootstrap, "prepare_backend", lambda: None)
runtime_name = f"app.plugins.{plugin_id}"
try:
getattr(bootstrap, prepare_name)(tmp_path)
runtime = import_module(runtime_name)
assert Path(runtime.__file__).resolve() == plugin_dir / "__init__.py"
with pytest.raises(ModuleNotFoundError):
import_module(plugin_id)
finally:
sys.modules.pop(runtime_name, None)