From c094121886259063097029f296387ca8db50dd2b Mon Sep 17 00:00:00 2001 From: fxbin Date: Thu, 8 Oct 2026 20:48:57 +0800 Subject: [PATCH] =?UTF-8?q?test(conftest):=20=E6=AF=8F=E4=B8=AA=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=90=8E=E6=B8=85=E7=A9=BA=E6=A8=A1=E5=9D=97=20engine?= =?UTF-8?q?=20=E8=BF=9E=E6=8E=A5=E6=B1=A0,=E6=A0=B9=E9=99=A4=E8=B7=A8?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=BE=AA=E7=8E=AF=E5=A4=8D=E7=94=A8=20flake?= =?UTF-8?q?=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pytest-asyncio 每测试新事件循环,模块级 engine 的池化 asyncpg 连接会跨 循环存活,复用即报 attached to a different loop 并引发后续 PG 测试连环 mass-E;conftest 增加 autouse dispose fixture,并配一对顺序敏感的回归 测试(隔离回归时第二个测试必炸),教训机器化。 --- backend/tests/conftest.py | 14 ++++++++ .../test_module_engine_loop_isolation.py | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 backend/tests/test_module_engine_loop_isolation.py diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 14ac7350..1f96cc10 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -149,6 +149,20 @@ async def clean_tables(): yield +# ── Function-scoped cleanup: 每个测试后清空模块级 engine 连接池 ── +# pytest-asyncio 为每个测试创建新事件循环,而 app.core.database.engine 是 +# 模块级单例:池化的 asyncpg 连接会跨循环存活,下一个测试(或其后台任务) +# 复用旧循环连接即报 "Future attached to a different loop",且症状是此后 +# 所有 PG 测试连环 mass-E(#97)。dispose 清空连接池,让下次 checkout 在 +# 当前循环新建连接;测试 PG 在本机,重连成本可忽略。 +@pytest_asyncio.fixture(autouse=True) +async def dispose_module_engine_connections(): + yield + from app.core.database import engine + + await engine.dispose() + + @pytest_asyncio.fixture(autouse=True) async def reset_llm_provider_state(): """每个测试前重置 LLM provider 模块级 state + logging 配置。 diff --git a/backend/tests/test_module_engine_loop_isolation.py b/backend/tests/test_module_engine_loop_isolation.py new file mode 100644 index 00000000..62c511c2 --- /dev/null +++ b/backend/tests/test_module_engine_loop_isolation.py @@ -0,0 +1,35 @@ +"""模块级 engine 跨事件循环隔离的回归测试(#97)。 + +pytest-asyncio 为每个测试创建新事件循环;``app.core.database.engine`` 是 +模块级单例,其连接池若把上一个循环创建的 asyncpg 连接留给下一个测试复用, +后者一执行真实 SQL 就报 "Future ... attached to a different loop",并引发 +后续 PG 测试连环 mass-E。conftest 的 ``dispose_module_engine_connections`` +fixture 在每个测试后清空连接池。 + +下面两个**顺序敏感**的测试各自从模块 engine checkout 并执行真实 SQL: +若跨循环隔离回归(比如 fixture 被移除),第二个测试必然失败——这就是本 +缺陷的机器化回归锚点。请保持它们为两个独立测试函数、文件内字母序执行。 +""" + +from __future__ import annotations + +import pytest +from sqlalchemy import text + +from app.core.database import async_session + + +@pytest.mark.asyncio +async def test_loop_a_checkout_and_release(): + """第一个循环:从模块 engine 拿连接、执行、归还池中。""" + async with async_session() as db: + val = (await db.execute(text("SELECT 41 + 1"))).scalar() + assert val == 42 + + +@pytest.mark.asyncio +async def test_loop_b_checkout_after_previous_loop(): + """第二个循环:若复用上一循环留在池里的连接即报 different loop。""" + async with async_session() as db: + val = (await db.execute(text("SELECT 43 - 1"))).scalar() + assert val == 42