mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
test: 隔离单测中的站点原生资源 (#6452)
* test: isolate native site resources * refactor(test): retain sites stub helper contract * fix(test): preserve sites stub probe imports --------- Co-authored-by: jxxghp <jxxghp@gmail.com>
This commit is contained in:
@@ -123,27 +123,10 @@ def _expose_plugin_source(path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def ensure_sites_stub() -> None:
|
def ensure_sites_stub() -> None:
|
||||||
"""为 ``app.application.site.sites`` 补最小垫片(仅在缺失时)。
|
"""安装确定性的站点资源垫片,隔离本机动态资源及其平台 ABI 差异。
|
||||||
|
|
||||||
``app.application.site.sites`` 由独立仓库动态拉取,CI / 全新环境无该模块,而众多 ``app.chain.*`` /
|
站点扩展由独立资源仓按平台下发,不属于普通单测的输入。主程序与各代插件测试必须在导入
|
||||||
``app.modules.*`` 在 import 期依赖它。统一补一个最小垫片,省去各测试文件各自打桩;若真实模块
|
业务模块前覆盖该模块;真实扩展的加载、ABI 与能力由资源专项验收负责。
|
||||||
已存在(本地已拉取)则用真实模块、不覆盖,不影响真实行为。须在隔离 CONFIG_DIR 之后调用,
|
|
||||||
以免试探性 ``import app.application.site.sites`` 牵入 ``app.runtime.config``、
|
|
||||||
把配置路径定型到真实目录。
|
|
||||||
"""
|
|
||||||
if "app.application.site.sites" in sys.modules:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
import app.application.site.sites # noqa: F401 本地已拉取时用真实模块
|
|
||||||
except (ModuleNotFoundError, ImportError):
|
|
||||||
install_sites_stub()
|
|
||||||
|
|
||||||
|
|
||||||
def install_sites_stub() -> None:
|
|
||||||
"""强制安装确定性的站点资源垫片,供隔离探针排除本机动态资源差异。
|
|
||||||
|
|
||||||
与 :func:`ensure_sites_stub` 的“真实资源优先”语义不同,性能与架构探针需要在开发机和
|
|
||||||
source-only CI 中使用完全相同的 import 前提,因此必须在导入被测宿主模块前覆盖资源模块。
|
|
||||||
"""
|
"""
|
||||||
from importlib.util import spec_from_loader
|
from importlib.util import spec_from_loader
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ uv run --locked --no-sync python tests/run.py --shard 1/4 # 只跑指
|
|||||||
收集任何测试模块、`import app.*` **之前**,conftest 完成两件事:
|
收集任何测试模块、`import app.*` **之前**,conftest 完成两件事:
|
||||||
|
|
||||||
1. **临时库**:把 `CONFIG_DIR` 指向临时目录并 `init_db()` 建表。引擎本身已惰性创建(`import app.db` 不再连库),但 `settings` 在 `import app.runtime.config` 那一刻就把 `CONFIG_DIR` 读进字段并建好配置子目录,之后再改环境变量对 `settings.CONFIG_PATH` 毫无影响——引擎晚点才建,连的仍是真实 `user.db`。所以隔离必须早于首个牵入 `app.runtime.config` 的 import(`app.db` / `app.chain.*` 都会牵入);空库会让运行期查表报 `no such table`,故必须建表。
|
1. **临时库**:把 `CONFIG_DIR` 指向临时目录并 `init_db()` 建表。引擎本身已惰性创建(`import app.db` 不再连库),但 `settings` 在 `import app.runtime.config` 那一刻就把 `CONFIG_DIR` 读进字段并建好配置子目录,之后再改环境变量对 `settings.CONFIG_PATH` 毫无影响——引擎晚点才建,连的仍是真实 `user.db`。所以隔离必须早于首个牵入 `app.runtime.config` 的 import(`app.db` / `app.chain.*` 都会牵入);空库会让运行期查表报 `no such table`,故必须建表。
|
||||||
2. **`app.application.site.sites` 垫片**:该模块由独立仓库动态拉取、CI 无此文件,conftest 统一补最小垫片(本地存在真实模块时优先用真实模块)。兼容层会把旧插件的 `app.helper.sites` 导入路由到同一模块。
|
2. **`app.application.site.sites` 垫片**:该模块由独立资源仓按平台下发,conftest 统一安装最小垫片,普通单测不会加载源码目录中的 `.so` / `.pyd`。兼容层会把旧插件的 `app.helper.sites` 导入路由到同一模块;真实制品由资源与 ABI 专项验收覆盖。
|
||||||
|
|
||||||
由此推出两条**硬规范**:
|
由此推出两条**硬规范**:
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|||||||
if str(PROJECT_ROOT) not in sys.path:
|
if str(PROJECT_ROOT) not in sys.path:
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
from app.testing.bootstrap import install_sites_stub, isolate_config_dir
|
from app.testing.bootstrap import ensure_sites_stub, isolate_config_dir
|
||||||
|
|
||||||
isolate_config_dir()
|
isolate_config_dir()
|
||||||
install_sites_stub()
|
ensure_sites_stub()
|
||||||
|
|
||||||
from app.startup import lifecycle
|
from app.startup import lifecycle
|
||||||
from app.startup.initializers import modules as modules_initializer
|
from app.startup.initializers import modules as modules_initializer
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from app.testing.bootstrap import install_sites_stub
|
from app.testing.bootstrap import ensure_sites_stub
|
||||||
|
|
||||||
install_sites_stub()
|
ensure_sites_stub()
|
||||||
before = set(sys.modules)
|
before = set(sys.modules)
|
||||||
started_at = time.perf_counter()
|
started_at = time.perf_counter()
|
||||||
importlib.import_module({target!r})
|
importlib.import_module({target!r})
|
||||||
@@ -100,9 +100,9 @@ import time
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from app.testing.bootstrap import install_sites_stub
|
from app.testing.bootstrap import ensure_sites_stub
|
||||||
|
|
||||||
install_sites_stub()
|
ensure_sites_stub()
|
||||||
from app.startup import lifecycle
|
from app.startup import lifecycle
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6471,7 +6471,6 @@
|
|||||||
"app.testing -> app.testing.stub",
|
"app.testing -> app.testing.stub",
|
||||||
"app.testing.bootstrap -> app.application",
|
"app.testing.bootstrap -> app.application",
|
||||||
"app.testing.bootstrap -> app.application.service",
|
"app.testing.bootstrap -> app.application.service",
|
||||||
"app.testing.bootstrap -> app.application.site",
|
|
||||||
"app.testing.bootstrap -> app.db",
|
"app.testing.bootstrap -> app.db",
|
||||||
"app.testing.bootstrap -> app.db.adapters",
|
"app.testing.bootstrap -> app.db.adapters",
|
||||||
"app.testing.bootstrap -> app.db.adapters.transaction",
|
"app.testing.bootstrap -> app.db.adapters.transaction",
|
||||||
|
|||||||
@@ -302,8 +302,7 @@ import app.monitor
|
|||||||
assert not any(name.startswith('app.doctor.') for name in sys.modules)
|
assert not any(name.startswith('app.doctor.') for name in sys.modules)
|
||||||
assert not any(name.startswith('app.monitor.') for name in sys.modules)
|
assert not any(name.startswith('app.monitor.') for name in sys.modules)
|
||||||
|
|
||||||
# CI 无 app.application.site.sites 资源模块,触发实现加载前先补 conftest 同源垫片;
|
# 独立子进程不经过 pytest 引导,触发实现加载前必须隔离站点原生制品。
|
||||||
# 独立子进程不经过 pytest 引导,必须在此显式安装,否则链式 import 会因缺模块失败。
|
|
||||||
from app.testing.bootstrap import ensure_sites_stub
|
from app.testing.bootstrap import ensure_sites_stub
|
||||||
ensure_sites_stub()
|
ensure_sites_stub()
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ IDENTITY_INDEX_SIGNATURES = {
|
|||||||
CURRENT_SCHEMA_CHAIN_SCRIPT = """
|
CURRENT_SCHEMA_CHAIN_SCRIPT = """
|
||||||
from app.testing.bootstrap import ensure_sites_stub
|
from app.testing.bootstrap import ensure_sites_stub
|
||||||
|
|
||||||
# Alembic 会导入引用业务链的旧 revision;全新 CI 环境没有动态下发的 sites 模块。
|
# Alembic 会导入引用业务链的旧 revision,迁移验证不应加载本机站点原生制品。
|
||||||
ensure_sites_stub()
|
ensure_sites_stub()
|
||||||
|
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ def test_manifest_aliases_reuse_real_canonical_modules():
|
|||||||
code = """
|
code = """
|
||||||
import importlib
|
import importlib
|
||||||
from app.runtime.compat.manifest import MODULE_ALIASES
|
from app.runtime.compat.manifest import MODULE_ALIASES
|
||||||
# CI 无 app.application.site.sites 二进制模块,先补垫片再校验全部映射(与 conftest 同源)。
|
# 独立探针不经过 pytest 引导,先隔离站点原生制品再校验兼容映射。
|
||||||
from app.testing.bootstrap import ensure_sites_stub
|
from app.testing.bootstrap import ensure_sites_stub
|
||||||
ensure_sites_stub()
|
ensure_sites_stub()
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ def test_main_script_does_not_shadow_stdlib_platform(tmp_path):
|
|||||||
(
|
(
|
||||||
"import runpy",
|
"import runpy",
|
||||||
"import sys",
|
"import sys",
|
||||||
# CI 无 app.application.site.sites 二进制模块,先补垫片再执行 main.py(与 conftest 同源);
|
# 独立探针不经过 pytest 引导,先隔离站点原生制品,再改变模块搜索路径。
|
||||||
# 必须在篡改 sys.path / 摘除 platform 之前调用,避免真实依赖链受探针环境影响。
|
|
||||||
"from app.testing.bootstrap import ensure_sites_stub",
|
"from app.testing.bootstrap import ensure_sites_stub",
|
||||||
"ensure_sites_stub()",
|
"ensure_sites_stub()",
|
||||||
f"sys.path.insert(0, {str(MAIN_PATH.parent)!r})",
|
f"sys.path.insert(0, {str(MAIN_PATH.parent)!r})",
|
||||||
|
|||||||
@@ -9,28 +9,30 @@ from pathlib import Path
|
|||||||
from app.testing import bootstrap
|
from app.testing import bootstrap
|
||||||
|
|
||||||
|
|
||||||
def test_install_sites_stub_replaces_loaded_dynamic_resource(monkeypatch):
|
def test_ensure_sites_stub_replaces_loaded_dynamic_resource(monkeypatch):
|
||||||
"""隔离探针必须覆盖本机资源模块,保证 source-only CI 与开发机前提一致。"""
|
"""隔离探针必须覆盖本机资源模块,保证 source-only CI 与开发机前提一致。"""
|
||||||
real_module = types.ModuleType("app.application.site.sites")
|
real_module = types.ModuleType("app.application.site.sites")
|
||||||
real_module.SitesHelper = object
|
real_module.SitesHelper = object
|
||||||
monkeypatch.setitem(sys.modules, "app.application.site.sites", real_module)
|
monkeypatch.setitem(sys.modules, "app.application.site.sites", real_module)
|
||||||
|
|
||||||
bootstrap.install_sites_stub()
|
|
||||||
|
|
||||||
installed = sys.modules["app.application.site.sites"]
|
|
||||||
assert installed is not real_module
|
|
||||||
assert installed.SitesHelper is bootstrap._SitesHelperStub
|
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_sites_stub_preserves_loaded_dynamic_resource(monkeypatch):
|
|
||||||
"""常规测试引导仍应优先复用已经加载的真实站点资源。"""
|
|
||||||
real_module = types.ModuleType("app.application.site.sites")
|
|
||||||
real_module.SitesHelper = object
|
|
||||||
monkeypatch.setitem(sys.modules, "app.application.site.sites", real_module)
|
|
||||||
|
|
||||||
bootstrap.ensure_sites_stub()
|
bootstrap.ensure_sites_stub()
|
||||||
|
|
||||||
assert sys.modules["app.application.site.sites"] is real_module
|
installed = sys.modules["app.application.site.sites"]
|
||||||
|
assert installed is not real_module
|
||||||
|
assert installed.SitesHelper is bootstrap._SitesHelperStub
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_backend_replaces_loaded_dynamic_resource(monkeypatch):
|
||||||
|
"""普通测试引导必须隔离源码目录中已存在的站点原生制品。"""
|
||||||
|
real_module = types.ModuleType("app.application.site.sites")
|
||||||
|
real_module.SitesHelper = object
|
||||||
|
monkeypatch.setitem(sys.modules, "app.application.site.sites", real_module)
|
||||||
|
|
||||||
|
bootstrap.prepare_backend()
|
||||||
|
|
||||||
|
installed = sys.modules["app.application.site.sites"]
|
||||||
|
assert installed is not real_module
|
||||||
|
assert installed.SitesHelper is bootstrap._SitesHelperStub
|
||||||
|
|
||||||
|
|
||||||
def test_isolate_config_cleanup_uses_loaded_db_module_without_late_import(monkeypatch):
|
def test_isolate_config_cleanup_uses_loaded_db_module_without_late_import(monkeypatch):
|
||||||
|
|||||||
Reference in New Issue
Block a user