From c727d9c9f1f45f0697e744d3256aa0b155b024bd Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:26:46 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E9=9A=94=E7=A6=BB=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E7=AB=99=E7=82=B9=E5=8E=9F=E7=94=9F=E8=B5=84?= =?UTF-8?q?=E6=BA=90=20(#6452)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: isolate native site resources * refactor(test): retain sites stub helper contract * fix(test): preserve sites stub probe imports --------- Co-authored-by: jxxghp --- app/testing/bootstrap.py | 23 ++----------- docs/testing.md | 2 +- scripts/perf/module_shutdown_ab.py | 4 +-- scripts/startup/performance.py | 8 ++--- .../architecture/dependency-baseline.json | 1 - tests/test_architecture_contract_baseline.py | 3 +- tests/test_database_index_migration.py | 2 +- tests/test_legacy_import_compat.py | 2 +- tests/test_main_direct_execution.py | 3 +- tests/test_testing_bootstrap.py | 32 ++++++++++--------- 10 files changed, 31 insertions(+), 49 deletions(-) diff --git a/app/testing/bootstrap.py b/app/testing/bootstrap.py index b23bbfd3e..5cafac9c6 100644 --- a/app/testing/bootstrap.py +++ b/app/testing/bootstrap.py @@ -123,27 +123,10 @@ def _expose_plugin_source(path: Path) -> None: def ensure_sites_stub() -> None: - """为 ``app.application.site.sites`` 补最小垫片(仅在缺失时)。 + """安装确定性的站点资源垫片,隔离本机动态资源及其平台 ABI 差异。 - ``app.application.site.sites`` 由独立仓库动态拉取,CI / 全新环境无该模块,而众多 ``app.chain.*`` / - ``app.modules.*`` 在 import 期依赖它。统一补一个最小垫片,省去各测试文件各自打桩;若真实模块 - 已存在(本地已拉取)则用真实模块、不覆盖,不影响真实行为。须在隔离 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 前提,因此必须在导入被测宿主模块前覆盖资源模块。 + 站点扩展由独立资源仓按平台下发,不属于普通单测的输入。主程序与各代插件测试必须在导入 + 业务模块前覆盖该模块;真实扩展的加载、ABI 与能力由资源专项验收负责。 """ from importlib.util import spec_from_loader from types import ModuleType diff --git a/docs/testing.md b/docs/testing.md index a09f7aeb4..32d5abf81 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -29,7 +29,7 @@ uv run --locked --no-sync python tests/run.py --shard 1/4 # 只跑指 收集任何测试模块、`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`,故必须建表。 -2. **`app.application.site.sites` 垫片**:该模块由独立仓库动态拉取、CI 无此文件,conftest 统一补最小垫片(本地存在真实模块时优先用真实模块)。兼容层会把旧插件的 `app.helper.sites` 导入路由到同一模块。 +2. **`app.application.site.sites` 垫片**:该模块由独立资源仓按平台下发,conftest 统一安装最小垫片,普通单测不会加载源码目录中的 `.so` / `.pyd`。兼容层会把旧插件的 `app.helper.sites` 导入路由到同一模块;真实制品由资源与 ABI 专项验收覆盖。 由此推出两条**硬规范**: diff --git a/scripts/perf/module_shutdown_ab.py b/scripts/perf/module_shutdown_ab.py index ca74a924e..f3c0e4725 100644 --- a/scripts/perf/module_shutdown_ab.py +++ b/scripts/perf/module_shutdown_ab.py @@ -16,10 +16,10 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: 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() -install_sites_stub() +ensure_sites_stub() from app.startup import lifecycle from app.startup.initializers import modules as modules_initializer diff --git a/scripts/startup/performance.py b/scripts/startup/performance.py index 254ded3b6..2921649b4 100644 --- a/scripts/startup/performance.py +++ b/scripts/startup/performance.py @@ -43,9 +43,9 @@ import json import sys 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) started_at = time.perf_counter() importlib.import_module({target!r}) @@ -100,9 +100,9 @@ import time 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 diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index aa64fae70..29af0a245 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -6471,7 +6471,6 @@ "app.testing -> app.testing.stub", "app.testing.bootstrap -> app.application", "app.testing.bootstrap -> app.application.service", - "app.testing.bootstrap -> app.application.site", "app.testing.bootstrap -> app.db", "app.testing.bootstrap -> app.db.adapters", "app.testing.bootstrap -> app.db.adapters.transaction", diff --git a/tests/test_architecture_contract_baseline.py b/tests/test_architecture_contract_baseline.py index 38c3ec912..890ae210d 100644 --- a/tests/test_architecture_contract_baseline.py +++ b/tests/test_architecture_contract_baseline.py @@ -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.monitor.') for name in sys.modules) -# CI 无 app.application.site.sites 资源模块,触发实现加载前先补 conftest 同源垫片; -# 独立子进程不经过 pytest 引导,必须在此显式安装,否则链式 import 会因缺模块失败。 +# 独立子进程不经过 pytest 引导,触发实现加载前必须隔离站点原生制品。 from app.testing.bootstrap import ensure_sites_stub ensure_sites_stub() diff --git a/tests/test_database_index_migration.py b/tests/test_database_index_migration.py index d12c51c0b..b1a4e52df 100644 --- a/tests/test_database_index_migration.py +++ b/tests/test_database_index_migration.py @@ -71,7 +71,7 @@ IDENTITY_INDEX_SIGNATURES = { CURRENT_SCHEMA_CHAIN_SCRIPT = """ from app.testing.bootstrap import ensure_sites_stub -# Alembic 会导入引用业务链的旧 revision;全新 CI 环境没有动态下发的 sites 模块。 +# Alembic 会导入引用业务链的旧 revision,迁移验证不应加载本机站点原生制品。 ensure_sites_stub() from alembic.config import Config diff --git a/tests/test_legacy_import_compat.py b/tests/test_legacy_import_compat.py index 2081a870f..64341354a 100644 --- a/tests/test_legacy_import_compat.py +++ b/tests/test_legacy_import_compat.py @@ -167,7 +167,7 @@ def test_manifest_aliases_reuse_real_canonical_modules(): code = """ import importlib from app.runtime.compat.manifest import MODULE_ALIASES -# CI 无 app.application.site.sites 二进制模块,先补垫片再校验全部映射(与 conftest 同源)。 +# 独立探针不经过 pytest 引导,先隔离站点原生制品再校验兼容映射。 from app.testing.bootstrap import ensure_sites_stub ensure_sites_stub() diff --git a/tests/test_main_direct_execution.py b/tests/test_main_direct_execution.py index dabb39673..5ef93ddfa 100644 --- a/tests/test_main_direct_execution.py +++ b/tests/test_main_direct_execution.py @@ -14,8 +14,7 @@ def test_main_script_does_not_shadow_stdlib_platform(tmp_path): ( "import runpy", "import sys", - # CI 无 app.application.site.sites 二进制模块,先补垫片再执行 main.py(与 conftest 同源); - # 必须在篡改 sys.path / 摘除 platform 之前调用,避免真实依赖链受探针环境影响。 + # 独立探针不经过 pytest 引导,先隔离站点原生制品,再改变模块搜索路径。 "from app.testing.bootstrap import ensure_sites_stub", "ensure_sites_stub()", f"sys.path.insert(0, {str(MAIN_PATH.parent)!r})", diff --git a/tests/test_testing_bootstrap.py b/tests/test_testing_bootstrap.py index f74b9db90..8fb477e91 100644 --- a/tests/test_testing_bootstrap.py +++ b/tests/test_testing_bootstrap.py @@ -9,28 +9,30 @@ from pathlib import Path 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 与开发机前提一致。""" real_module = types.ModuleType("app.application.site.sites") real_module.SitesHelper = object 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() - 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):