gendesign/backend/tests/workers/tasks/test_etl_newbuilding_crossload.py
bot-backend 464519c69b
Some checks failed
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
fix(workers): izyatie per-doc isolation + newbuilding-crossload breadcrumb (#2445-D5/D6) (#2460)
2026-07-05 20:09:12 +00:00

157 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты breadcrumb-логирования etl_newbuilding_crossload task (issue #2445 D6).
Раньше эта таска не имела ни try/except, ни breadcrumb-записи в отличие от всех
соседних nightly/weekly-тасок в этой директории (objective_etl.py и т.д.) —
сбой run_crossload() был виден только в Celery FAILURE + GlitchTip, но не в
operational-логе (objective_scrape_runs), который проверяет оператор.
Покрытие:
- success-path: run_id создаётся, run_crossload() вызывается с db=, breadcrumb
помечается 'done' со счётчиками source_rows/upserted/skipped.
- disabled-путь (TRADEIN_DATABASE_URL пуст): breadcrumb 'done', error поясняет причину.
- failure-path: run_crossload() бросает исключение → breadcrumb 'failed' +
error, задача ре-рейзит исключение (Celery должен уйти в FAILURE).
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
def _make_db_mock(run_id: int = 7) -> MagicMock:
"""Mock SessionLocal() — INSERT ... RETURNING run_id возвращает run_id."""
db = MagicMock()
db.execute.return_value.scalar_one.return_value = run_id
return db
def _capture_finish_run(mod: Any) -> tuple[list[dict[str, Any]], Any]:
"""Оборачивает mod._finish_run, чтобы записывать все вызовы (kwargs)."""
calls: list[dict[str, Any]] = []
orig = mod._finish_run
def capture(_db: Any, _run_id: int, **kwargs: Any) -> None:
calls.append({"run_id": _run_id, **kwargs})
orig(_db, _run_id, **kwargs)
return calls, capture
def test_success_writes_done_breadcrumb_with_counts() -> None:
"""Успешный прогон: run_id создан, breadcrumb → 'done' с source_rows/upserted/skipped."""
from app.workers.tasks import etl_newbuilding_crossload as mod
db = _make_db_mock(run_id=7)
finish_calls, capture = _capture_finish_run(mod)
with (
patch.object(mod, "SessionLocal", return_value=db),
patch.object(
mod,
"run_crossload",
return_value={"source_rows": 100, "upserted": 95, "skipped": 5},
) as mock_run_crossload,
patch.object(mod, "_finish_run", side_effect=capture),
):
result = mod.etl_newbuilding_crossload.run(triggered_by="manual")
assert result["run_id"] == 7
assert result["source_rows"] == 100
assert result["upserted"] == 95
assert result["skipped"] == 5
# run_crossload вызван с нашей db-сессией (переиспользуем транзакцию/savepoints).
mock_run_crossload.assert_called_once_with(db=db)
assert len(finish_calls) == 1
assert finish_calls[0]["run_id"] == 7
assert finish_calls[0]["status"] == "done"
assert finish_calls[0]["error"] is None
assert finish_calls[0]["rows_history"] == 100
assert finish_calls[0]["rows_lots"] == 95
assert finish_calls[0]["requests_count"] == 5
db.close.assert_called_once()
def test_disabled_writes_done_breadcrumb_with_explanation() -> None:
"""TRADEIN_DATABASE_URL не задан → run_crossload вернул {"disabled": True};
breadcrumb всё равно 'done' (ожидаемое поведение, не ошибка), error поясняет причину."""
from app.workers.tasks import etl_newbuilding_crossload as mod
db = _make_db_mock(run_id=11)
finish_calls, capture = _capture_finish_run(mod)
with (
patch.object(mod, "SessionLocal", return_value=db),
patch.object(mod, "run_crossload", return_value={"disabled": True}),
patch.object(mod, "_finish_run", side_effect=capture),
):
result = mod.etl_newbuilding_crossload.run(triggered_by="beat")
assert result == {"run_id": 11, "disabled": True}
assert len(finish_calls) == 1
assert finish_calls[0]["status"] == "done"
assert finish_calls[0]["error"] == "TRADEIN_DATABASE_URL not set"
def test_exception_writes_failed_breadcrumb_and_reraises() -> None:
"""run_crossload() бросает исключение (postgres timeout / schema drift) →
breadcrumb помечается 'failed' с error-сообщением, задача ре-рейзит
исключение (Celery FAILURE + GlitchTip продолжает ловить сбой)."""
from app.workers.tasks import etl_newbuilding_crossload as mod
db = _make_db_mock(run_id=13)
finish_calls, capture = _capture_finish_run(mod)
with (
patch.object(mod, "SessionLocal", return_value=db),
patch.object(mod, "run_crossload", side_effect=RuntimeError("tradein postgres timeout")),
patch.object(mod, "_finish_run", side_effect=capture),
):
try:
mod.etl_newbuilding_crossload.run(triggered_by="beat")
raised = False
except RuntimeError as e:
raised = True
assert "tradein postgres timeout" in str(e)
assert raised, "исключение должно ре-рейзиться, чтобы Celery ушёл в FAILURE"
assert len(finish_calls) == 1
assert finish_calls[0]["run_id"] == 13
assert finish_calls[0]["status"] == "failed"
assert "RuntimeError" in finish_calls[0]["error"]
assert "tradein postgres timeout" in finish_calls[0]["error"]
db.close.assert_called_once()
def test_breadcrumb_flush_failure_does_not_swallow_original_exception() -> None:
"""Если сам breadcrumb-flush (_finish_run) на failure-пути тоже падает —
исходное исключение из run_crossload всё равно должно ре-рейзиться, не
подавляться."""
from app.workers.tasks import etl_newbuilding_crossload as mod
db = _make_db_mock(run_id=21)
def _boom_finish(*_a: Any, **_kw: Any) -> None:
raise Exception("DB connection lost while writing breadcrumb")
with (
patch.object(mod, "SessionLocal", return_value=db),
patch.object(
mod, "run_crossload", side_effect=RuntimeError("schema drift on tradein side")
),
patch.object(mod, "_finish_run", side_effect=_boom_finish),
):
try:
mod.etl_newbuilding_crossload.run(triggered_by="beat")
raised_type = None
except Exception as e: # тест проверяет именно тип/сообщение исходного исключения
raised_type = type(e)
raised_msg = str(e)
assert raised_type is RuntimeError
assert "schema drift on tradein side" in raised_msg