diff --git a/tests/conftest.py b/tests/conftest.py index 1877e1e..5157dfa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,6 +85,29 @@ def db_sync(sync_engine): session.rollback() +@pytest.fixture +def celery_conf(): + """Set Celery config keys for one test: `celery_conf(task_always_eager=True)`. + The values they had before the test come back after it. + + Use this, not `monkeypatch.setattr(celery.conf, ...)`. Celery serves config + keys through `__getattr__`, so monkeypatch sees no attribute to restore and + deletes the key on undo instead; the delete raises, and every later undo of + that test is skipped, leaking its patches into the tests after it. + """ + from backend.app.celery_app import celery + + saved: dict[str, object] = {} + + def _set(**values: object) -> None: + for key in values: + saved.setdefault(key, celery.conf[key]) + celery.conf.update(values) + + yield _set + celery.conf.update(saved) + + @pytest_asyncio.fixture async def app(): return create_app() diff --git a/tests/test_api_cleanup.py b/tests/test_api_cleanup.py index 2456101..0fa8ccb 100644 --- a/tests/test_api_cleanup.py +++ b/tests/test_api_cleanup.py @@ -11,15 +11,14 @@ import pytest from PIL import Image from sqlalchemy import func, select -from backend.app.celery_app import celery from backend.app.models import ImageRecord, LibraryAuditRun pytestmark = pytest.mark.integration @pytest.fixture(autouse=True) -def disable_celery_eager(monkeypatch): - monkeypatch.setattr(celery.conf, "task_always_eager", False) +def disable_celery_eager(celery_conf): + celery_conf(task_always_eager=False) def _sha256_min_dim_token(min_w: int, min_h: int) -> str: diff --git a/tests/test_download_source_task.py b/tests/test_download_source_task.py index 6f31167..07582d6 100644 --- a/tests/test_download_source_task.py +++ b/tests/test_download_source_task.py @@ -167,7 +167,7 @@ async def test_finalize_soft_limited_is_noop_without_running_event(db_sync): @pytest.mark.integration @pytest.mark.asyncio async def test_download_source_catches_soft_limit_and_salvages_event( - db_sync, monkeypatch, + db_sync, monkeypatch, celery_conf, ): """End-to-end wiring: when the inner run raises SoftTimeLimitExceeded, download_source's handler must flip the in-flight event to error @@ -178,8 +178,7 @@ async def test_download_source_catches_soft_limit_and_salvages_event( import backend.app.tasks.download as dl from backend.app.models import DownloadEvent - monkeypatch.setattr(celery.conf, "task_always_eager", True) - monkeypatch.setattr(celery.conf, "task_eager_propagates", False) + celery_conf(task_always_eager=True, task_eager_propagates=False) # This test exercises the soft-limit salvage path, not the per-platform # serialization. Neutralize the Redis lock so it can't defer/recurse under # eager mode (and so it doesn't contend with test_platform_lock). diff --git a/tests/test_task_run_signals.py b/tests/test_task_run_signals.py index 991debd..7de5e04 100644 --- a/tests/test_task_run_signals.py +++ b/tests/test_task_run_signals.py @@ -20,10 +20,9 @@ pytestmark = pytest.mark.integration @pytest.fixture(autouse=True) -def _eager_celery(monkeypatch): +def _eager_celery(celery_conf): """task_always_eager so signals fire in-process during the test.""" - monkeypatch.setattr(celery.conf, "task_always_eager", True) - monkeypatch.setattr(celery.conf, "task_eager_propagates", False) + celery_conf(task_always_eager=True, task_eager_propagates=False) @pytest.mark.asyncio diff --git a/tests/test_tasks_admin.py b/tests/test_tasks_admin.py index ed54402..586bf3b 100644 --- a/tests/test_tasks_admin.py +++ b/tests/test_tasks_admin.py @@ -15,9 +15,8 @@ pytestmark = pytest.mark.integration @pytest.fixture(autouse=True) -def _eager_celery(monkeypatch): - monkeypatch.setattr(celery.conf, "task_always_eager", True) - monkeypatch.setattr(celery.conf, "task_eager_propagates", False) +def _eager_celery(celery_conf): + celery_conf(task_always_eager=True, task_eager_propagates=False) @pytest.fixture(autouse=True) diff --git a/tests/test_tasks_backup.py b/tests/test_tasks_backup.py index 583a202..c179b41 100644 --- a/tests/test_tasks_backup.py +++ b/tests/test_tasks_backup.py @@ -10,16 +10,14 @@ from pathlib import Path import pytest from sqlalchemy import select -from backend.app.celery_app import celery from backend.app.models import BackupRun, ImportSettings pytestmark = pytest.mark.integration @pytest.fixture(autouse=True) -def _eager_celery(monkeypatch): - monkeypatch.setattr(celery.conf, "task_always_eager", True) - monkeypatch.setattr(celery.conf, "task_eager_propagates", False) +def _eager_celery(celery_conf): + celery_conf(task_always_eager=True, task_eager_propagates=False) @pytest.fixture(autouse=True) diff --git a/tests/test_tasks_library_audit.py b/tests/test_tasks_library_audit.py index f26d528..17d3ce2 100644 --- a/tests/test_tasks_library_audit.py +++ b/tests/test_tasks_library_audit.py @@ -9,7 +9,6 @@ from PIL import Image from sqlalchemy import select import backend.app.tasks.library_audit # noqa: F401 — celery registration -from backend.app import celery_app from backend.app.models import ImageRecord, LibraryAuditRun pytestmark = pytest.mark.integration @@ -37,7 +36,7 @@ def _mk_image(db_sync, tmp_path, *, mode, color, name): def test_scan_library_for_rule_populates_matched_ids_for_transparency( - db_sync, tmp_path, monkeypatch, + db_sync, tmp_path, celery_conf, ): transparent_rec, _ = _mk_image( db_sync, tmp_path, mode="RGBA", color=(0, 0, 0, 0), name="trans.png", @@ -53,10 +52,10 @@ def test_scan_library_for_rule_populates_matched_ids_for_transparency( db_sync.commit() audit_id = audit.id - monkeypatch.setattr(celery_app.celery.conf, "task_always_eager", True) + celery_conf(task_always_eager=True) from backend.app.tasks.library_audit import scan_library_for_rule scan_library_for_rule.run(audit_id) - monkeypatch.setattr(celery_app.celery.conf, "task_always_eager", False) + celery_conf(task_always_eager=False) matched = db_sync.execute( select(LibraryAuditRun.matched_ids).where(LibraryAuditRun.id == audit_id) @@ -124,7 +123,7 @@ def test_scan_resumes_and_accumulates_matched(db_sync, tmp_path, monkeypatch): def test_scan_library_for_rule_skips_missing_files_gracefully( - db_sync, tmp_path, monkeypatch, + db_sync, tmp_path, celery_conf, ): rec, path = _mk_image( db_sync, tmp_path, mode="RGBA", color=(0, 0, 0, 0), name="ghost.png", @@ -138,10 +137,10 @@ def test_scan_library_for_rule_skips_missing_files_gracefully( db_sync.commit() audit_id = audit.id - monkeypatch.setattr(celery_app.celery.conf, "task_always_eager", True) + celery_conf(task_always_eager=True) from backend.app.tasks.library_audit import scan_library_for_rule scan_library_for_rule.run(audit_id) - monkeypatch.setattr(celery_app.celery.conf, "task_always_eager", False) + celery_conf(task_always_eager=False) status = db_sync.execute( select(LibraryAuditRun.status).where(LibraryAuditRun.id == audit_id)