fix(integration): lazy seed-snapshot (unblock unit job); isolate recover test from eager import
This commit is contained in:
+33
-23
@@ -77,35 +77,45 @@ def db_sync(sync_engine):
|
|||||||
session.rollback()
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
# Migration-seeded baseline (singleton config like import_settings /
|
||||||
def _seed_snapshot():
|
# ml_settings), captured lazily at the FIRST integration test's setup —
|
||||||
"""Capture migration-seeded rows (singleton config like import_settings
|
# when the CI integration job has just run `alembic upgrade head` so the DB
|
||||||
/ ml_settings) once, so the per-test truncation can restore them."""
|
# is pristine. Cached module-wide. Never populated in the DB-less unit job
|
||||||
eng = create_engine(_sync_database_url(), future=True)
|
# because only integration-marked tests trigger the connection.
|
||||||
snap: dict[str, list[dict]] = {}
|
_SEED_SNAPSHOT: dict[str, list[dict]] | None = None
|
||||||
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 _reset_db_after_integration(request, _seed_snapshot):
|
def _reset_db_after_integration(request):
|
||||||
"""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. After each integration-marked test, truncate
|
would leak between tests. Capture the seeded baseline at the first
|
||||||
every model table, then restore the migration-seeded rows. Scoped to
|
integration test, then after each integration-marked test truncate
|
||||||
the integration marker so the DB-less fast unit job never connects.
|
every model table and restore that baseline. Connects to the DB ONLY
|
||||||
|
for integration-marked tests, so the no-DB fast unit job is untouched.
|
||||||
"""
|
"""
|
||||||
|
global _SEED_SNAPSHOT
|
||||||
|
is_integration = request.node.get_closest_marker("integration") is not None
|
||||||
|
|
||||||
|
if is_integration and _SEED_SNAPSHOT is None:
|
||||||
|
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()
|
||||||
|
_SEED_SNAPSHOT = snap
|
||||||
|
|
||||||
yield
|
yield
|
||||||
if request.node.get_closest_marker("integration") is None:
|
|
||||||
|
if not is_integration:
|
||||||
return
|
return
|
||||||
tables = ", ".join(t.name for t in Base.metadata.sorted_tables)
|
tables = ", ".join(t.name for t in Base.metadata.sorted_tables)
|
||||||
if not tables:
|
if not tables:
|
||||||
@@ -117,7 +127,7 @@ def _reset_db_after_integration(request, _seed_snapshot):
|
|||||||
f"TRUNCATE {tables} RESTART IDENTITY CASCADE"
|
f"TRUNCATE {tables} RESTART IDENTITY CASCADE"
|
||||||
)
|
)
|
||||||
for t in Base.metadata.sorted_tables:
|
for t in Base.metadata.sorted_tables:
|
||||||
rows = _seed_snapshot.get(t.name)
|
rows = (_SEED_SNAPSHOT or {}).get(t.name)
|
||||||
if rows:
|
if rows:
|
||||||
conn.execute(t.insert(), rows)
|
conn.execute(t.insert(), rows)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def _make_batch(session) -> int:
|
|||||||
return batch.id
|
return batch.id
|
||||||
|
|
||||||
|
|
||||||
def test_recover_interrupted_only_old(db_sync):
|
def test_recover_interrupted_only_old(db_sync, monkeypatch):
|
||||||
batch_id = _make_batch(db_sync)
|
batch_id = _make_batch(db_sync)
|
||||||
now = datetime.now(UTC)
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
@@ -41,6 +41,16 @@ def test_recover_interrupted_only_old(db_sync):
|
|||||||
db_sync.add_all([fresh, stale])
|
db_sync.add_all([fresh, stale])
|
||||||
db_sync.commit()
|
db_sync.commit()
|
||||||
|
|
||||||
|
# Isolate the recover task: under eager Celery, the real
|
||||||
|
# import_media_file.delay() would run inline against the nonexistent
|
||||||
|
# /import/b.jpg and flip the just-requeued row 'queued' -> 'skipped'.
|
||||||
|
from backend.app.tasks import import_file
|
||||||
|
|
||||||
|
dispatched: list[int] = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
import_file.import_media_file, "delay", dispatched.append
|
||||||
|
)
|
||||||
|
|
||||||
from backend.app.tasks.maintenance import recover_interrupted_tasks
|
from backend.app.tasks.maintenance import recover_interrupted_tasks
|
||||||
recovered = recover_interrupted_tasks.apply().get()
|
recovered = recover_interrupted_tasks.apply().get()
|
||||||
assert recovered == 1
|
assert recovered == 1
|
||||||
@@ -50,6 +60,7 @@ def test_recover_interrupted_only_old(db_sync):
|
|||||||
assert fresh.status == "processing"
|
assert fresh.status == "processing"
|
||||||
assert stale.status == "queued"
|
assert stale.status == "queued"
|
||||||
assert stale.started_at is None
|
assert stale.started_at is None
|
||||||
|
assert dispatched == [stale.id]
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup_old_deletes_finished_old(db_sync):
|
def test_cleanup_old_deletes_finished_old(db_sync):
|
||||||
|
|||||||
Reference in New Issue
Block a user