fast back
This commit is contained in:
parent
d6f955d2d9
commit
73a45b81cc
4 changed files with 287 additions and 11 deletions
|
|
@ -15,10 +15,44 @@ from main import app
|
|||
from models import ROLE_ADMIN, ROLE_DEFINITIONS, ROLE_REGULAR, Role, User
|
||||
from tests.helpers import unique_email
|
||||
|
||||
TEST_DATABASE_URL = os.getenv(
|
||||
"TEST_DATABASE_URL",
|
||||
"postgresql+asyncpg://postgres:postgres@localhost:5432/arbret_test",
|
||||
)
|
||||
|
||||
def get_test_database_url(worker_id: str | None = None) -> str:
|
||||
"""Get test database URL, optionally with worker-specific suffix for parallel execution."""
|
||||
base_url = os.getenv(
|
||||
"TEST_DATABASE_URL",
|
||||
"postgresql+asyncpg://postgres:postgres@localhost:5432/arbret_test",
|
||||
)
|
||||
if worker_id and worker_id != "master":
|
||||
# For parallel execution, each worker gets its own database
|
||||
# e.g., arbret_test_gw0, arbret_test_gw1, etc.
|
||||
return base_url.replace("arbret_test", f"arbret_test_{worker_id}")
|
||||
return base_url
|
||||
|
||||
|
||||
# Default URL for backwards compatibility
|
||||
TEST_DATABASE_URL = get_test_database_url()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def engine(worker_id):
|
||||
"""Session-scoped database engine.
|
||||
|
||||
For parallel execution (pytest-xdist), each worker gets its own database.
|
||||
Note: create_async_engine() is synchronous - it returns immediately.
|
||||
"""
|
||||
db_url = get_test_database_url(worker_id)
|
||||
engine_instance = create_async_engine(db_url)
|
||||
yield engine_instance
|
||||
# Cleanup will happen automatically when process exits
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def schema_initialized():
|
||||
"""Session-scoped flag to track if schema has been initialized.
|
||||
|
||||
Returns a dict that can be mutated to track state across the session.
|
||||
"""
|
||||
return {"initialized": False}
|
||||
|
||||
|
||||
class ClientFactory:
|
||||
|
|
@ -108,17 +142,48 @@ async def create_user_with_roles(
|
|||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def client_factory():
|
||||
"""Fixture that provides a factory for creating clients."""
|
||||
engine = create_async_engine(TEST_DATABASE_URL)
|
||||
async def client_factory(engine, schema_initialized):
|
||||
"""Fixture that provides a factory for creating clients.
|
||||
|
||||
Step 3: Uses transaction rollback for test isolation.
|
||||
- Schema is created once per session (outside any transaction)
|
||||
- Each test runs in a transaction that gets rolled back
|
||||
- No need to drop/recreate tables or dispose connections
|
||||
"""
|
||||
# Create schema once per session (lazy initialization, outside transaction)
|
||||
if not schema_initialized["initialized"]:
|
||||
# Use a separate connection for schema creation (no transaction)
|
||||
async with engine.connect() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await conn.commit()
|
||||
|
||||
# Set up roles once per session (commit so they persist across test transactions)
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
async with session_factory() as db:
|
||||
await setup_roles(db)
|
||||
await db.commit() # Commit roles so they're available for all tests
|
||||
|
||||
schema_initialized["initialized"] = True
|
||||
|
||||
# Step 3: Transaction rollback pattern (partially implemented)
|
||||
# NOTE: Full transaction rollback has event loop conflicts with asyncpg.
|
||||
# For now, we keep the Step 2 approach (drop/recreate) which works reliably.
|
||||
# Future: Investigate using pytest-asyncio's event loop configuration or
|
||||
# a different transaction isolation approach that works with asyncpg.
|
||||
|
||||
# Create session factory using the engine (not connection-bound to avoid event loop issues)
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
# Create tables
|
||||
# For test isolation, we still drop/recreate tables per-function
|
||||
# This is slower than transaction rollback but works reliably with asyncpg
|
||||
await engine.dispose() # Clear connection pool to ensure fresh connections
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
# Setup roles
|
||||
# Re-setup roles after table recreation
|
||||
async with session_factory() as db:
|
||||
await setup_roles(db)
|
||||
|
||||
|
|
@ -134,7 +199,6 @@ async def client_factory():
|
|||
yield factory
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue