mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 04:39:06 +00:00
fix: resolve event loop isolation issues in test suite
- Add session-scoped event loop fixture to prevent 'Event loop is closed' errors - Use NullPool for database connections to avoid asyncpg connection caching issues - Override session.commit with flush in tests to maintain transaction rollback - Configure pytest-asyncio with session-scoped loop defaults - Fixes 'coroutine Connection._cancel was never awaited' warnings - Properly dispose of database engines after each test Results: 137 tests passing (up from 116), only 8 failures remaining This addresses the SQLAlchemy 2.0 async session lifecycle issues with asyncpg
This commit is contained in:
@@ -117,6 +117,7 @@ DATABASE_URL = "postgresql+asyncpg://test_user:test_password@localhost:15432/ref
|
|||||||
addopts = "-ra -q --disable-pytest-warnings --cov --cov-report html -v"
|
addopts = "-ra -q --disable-pytest-warnings --cov --cov-report html -v"
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
|
asyncio_default_fixture_loop_scope = "session"
|
||||||
markers = [
|
markers = [
|
||||||
"model_api: tests for the unified model-serving HTTP API (backend- and hardware-agnostic)",
|
"model_api: tests for the unified model-serving HTTP API (backend- and hardware-agnostic)",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def event_loop():
|
||||||
|
"""Creates session-scoped event loop to prevent 'event loop is closed' errors"""
|
||||||
|
# Windows fix for Python 3.8+
|
||||||
|
if sys.platform.startswith("win") and sys.version_info[:2] >= (3, 8):
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||||
|
yield loop
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
@@ -89,7 +105,7 @@ except ImportError:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest_asyncio.fixture(scope="session", autouse=True)
|
||||||
async def setup_database(postgres_service):
|
async def setup_database(postgres_service):
|
||||||
"""Setup database and run migrations"""
|
"""Setup database and run migrations"""
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
@@ -108,8 +124,12 @@ async def setup_database(postgres_service):
|
|||||||
|
|
||||||
settings.DATABASE_URL = DATABASE_URL
|
settings.DATABASE_URL = DATABASE_URL
|
||||||
|
|
||||||
# Create engine and tables
|
# Create engine with NullPool to prevent connection pooling issues
|
||||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
engine = create_async_engine(
|
||||||
|
DATABASE_URL,
|
||||||
|
echo=False,
|
||||||
|
poolclass=NullPool, # Critical: Prevents connection pool issues with asyncpg
|
||||||
|
)
|
||||||
|
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
# Drop all tables first to ensure clean state
|
# Drop all tables first to ensure clean state
|
||||||
@@ -119,28 +139,60 @@ async def setup_database(postgres_service):
|
|||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup - properly dispose of the engine
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest_asyncio.fixture
|
||||||
async def session(setup_database):
|
async def session(setup_database):
|
||||||
"""Provide a transactional database session for tests"""
|
"""Provide a transactional database session for tests"""
|
||||||
import sqlalchemy.exc
|
from sqlalchemy.ext.asyncio import (
|
||||||
|
AsyncSession,
|
||||||
|
async_sessionmaker,
|
||||||
|
create_async_engine,
|
||||||
|
)
|
||||||
|
|
||||||
from reflector.db import get_session_factory
|
from reflector.settings import settings
|
||||||
|
|
||||||
|
# Create a new engine with NullPool for this session
|
||||||
|
engine = create_async_engine(
|
||||||
|
settings.DATABASE_URL,
|
||||||
|
echo=False,
|
||||||
|
poolclass=NullPool, # Use NullPool to avoid connection caching issues
|
||||||
|
)
|
||||||
|
|
||||||
|
async_session_maker = async_sessionmaker(
|
||||||
|
bind=engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False,
|
||||||
|
autoflush=False,
|
||||||
|
autocommit=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Start a savepoint instead of a transaction to handle nested commits
|
||||||
|
await session.begin()
|
||||||
|
|
||||||
|
# Override commit to use flush instead in tests
|
||||||
|
original_commit = session.commit
|
||||||
|
|
||||||
|
async def flush_instead_of_commit():
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
session.commit = flush_instead_of_commit
|
||||||
|
|
||||||
async with get_session_factory()() as session:
|
|
||||||
# Start a transaction that we'll rollback at the end
|
|
||||||
transaction = await session.begin()
|
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
|
await session.rollback()
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
try:
|
session.commit = original_commit # Restore original commit
|
||||||
await transaction.rollback()
|
await session.close()
|
||||||
except sqlalchemy.exc.ResourceClosedError:
|
|
||||||
# Transaction was already closed (e.g., by a commit), ignore
|
# Properly dispose of the engine to close all connections
|
||||||
pass
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
Reference in New Issue
Block a user