fix(integration): snapshot/restore seeded singletons, dispose app engine per test
This commit is contained in:
+42
-4
@@ -77,13 +77,32 @@ def db_sync(sync_engine):
|
|||||||
session.rollback()
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def _seed_snapshot():
|
||||||
|
"""Capture migration-seeded rows (singleton config like import_settings
|
||||||
|
/ ml_settings) once, so the per-test truncation can restore them."""
|
||||||
|
eng = create_engine(_sync_database_url(), future=True)
|
||||||
|
snap: dict[str, list[dict]] = {}
|
||||||
|
try:
|
||||||
|
with eng.connect() as conn:
|
||||||
|
for t in Base.metadata.sorted_tables:
|
||||||
|
rows = [
|
||||||
|
dict(m) for m in conn.execute(t.select()).mappings().all()
|
||||||
|
]
|
||||||
|
if rows:
|
||||||
|
snap[t.name] = rows
|
||||||
|
finally:
|
||||||
|
eng.dispose()
|
||||||
|
return snap
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _truncate_after_integration(request):
|
def _reset_db_after_integration(request, _seed_snapshot):
|
||||||
"""Integration tests exercise app/celery code that commits on its own
|
"""Integration tests exercise app/celery code that commits on its own
|
||||||
connection, which the rollback fixtures above can't undo — so data
|
connection, which the rollback fixtures above can't undo — so data
|
||||||
would leak between tests. Truncate every model table after each
|
would leak between tests. After each integration-marked test, truncate
|
||||||
integration-marked test. Scoped to the integration marker so the
|
every model table, then restore the migration-seeded rows. Scoped to
|
||||||
DB-less fast unit job never connects here.
|
the integration marker so the DB-less fast unit job never connects.
|
||||||
"""
|
"""
|
||||||
yield
|
yield
|
||||||
if request.node.get_closest_marker("integration") is None:
|
if request.node.get_closest_marker("integration") is None:
|
||||||
@@ -97,5 +116,24 @@ def _truncate_after_integration(request):
|
|||||||
conn.exec_driver_sql(
|
conn.exec_driver_sql(
|
||||||
f"TRUNCATE {tables} RESTART IDENTITY CASCADE"
|
f"TRUNCATE {tables} RESTART IDENTITY CASCADE"
|
||||||
)
|
)
|
||||||
|
for t in Base.metadata.sorted_tables:
|
||||||
|
rows = _seed_snapshot.get(t.name)
|
||||||
|
if rows:
|
||||||
|
conn.execute(t.insert(), rows)
|
||||||
finally:
|
finally:
|
||||||
eng.dispose()
|
eng.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(autouse=True)
|
||||||
|
async def _dispose_app_engine(request):
|
||||||
|
"""The app's shared async engine (extensions.get_engine) is a process
|
||||||
|
singleton, but pytest-asyncio gives each test a fresh event loop;
|
||||||
|
asyncpg connections are loop-bound, so reuse across tests raises
|
||||||
|
'attached to a different loop'. Dispose after each integration test so
|
||||||
|
the next test rebuilds the engine on its own loop.
|
||||||
|
"""
|
||||||
|
yield
|
||||||
|
if request.node.get_closest_marker("integration") is None:
|
||||||
|
return
|
||||||
|
from backend.app import extensions
|
||||||
|
await extensions.dispose_engine()
|
||||||
|
|||||||
Reference in New Issue
Block a user