gendesign/backend/tests/workers/test_ird_harvest.py
lekss361 fe7727e338
Some checks are pending
Deploy / changes (push) Waiting to run
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
fix(sf): per-quarter commit в ird/opportunity harvest — не терять прогресс при краше (#1107)
ird_harvest + opportunity_harvest: commit после КАЖДОГО квартала (внутри per-quarter try) + db.rollback() в except перед continue. Прод-баг: single end-commit терял весь прогресс многочасового grid-walk при краше/таймауте/WorkerLost. Теперь durable поквартально; SAVEPOINT per-row не тронут. +2 теста (commit-count durability, flaky-quarter rollback+continue).

Refs #1067.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-06 21:47:35 +00:00

171 lines
6.2 KiB
Python
Raw Permalink 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.

"""Тесты harvest_ird_overlays (#1067 B5) — НСПД ИРД-harvest → ird_overlays.
Сеть/БД не дёргаются: мокаем NSPDClient + SessionLocal, проверяем grid-walk loop,
skip битых фич и upsert-параметры.
"""
from __future__ import annotations
from contextlib import contextmanager
from typing import Any
from app.workers.tasks import ird_harvest
class _Feat:
def __init__(self, geometry: dict[str, Any] | None, properties: dict[str, Any]) -> None:
self.geometry = geometry
self.properties = properties
class _SearchRes:
def __init__(self, feat: _Feat | None) -> None:
self.first = feat
class _FakeClient:
"""NSPDClient-двойник: квартал → полигон (3857), grid-walk → 1 фича на слой."""
def __init__(self, *a: Any, **kw: Any) -> None:
self.grid_calls: list[int] = []
def search_by_cad(self, cad: str, thematic_id: int = 1) -> _SearchRes:
ring = [[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]
geom = {"type": "Polygon", "coordinates": [ring]}
return _SearchRes(_Feat(geom, {"cad": cad}))
def get_features_in_bbox_grid(self, layer_id: int, bbox: Any, **kw: Any) -> list[_Feat]:
self.grid_calls.append(layer_id)
return [
_Feat(
{"type": "Polygon", "coordinates": [[[1, 1], [2, 1], [2, 2], [1, 1]]]},
{
"geom_data_id": 1000 + layer_id,
"reg_numb_border": "66:41-6.1",
"category": 36940,
"subcategory": 5,
},
)
]
class _FakeDB:
"""Session-двойник: пишет execute-вызовы, begin_nested как no-op CM."""
def __init__(self) -> None:
self.executed: list[tuple[Any, dict[str, Any] | None]] = []
self.committed = False
self.commit_count = 0
self.rollback_count = 0
self.closed = False
@contextmanager
def begin_nested(self) -> Any:
yield
def execute(self, sql: Any, params: dict[str, Any] | None = None) -> Any:
self.executed.append((sql, params))
return None
def commit(self) -> None:
self.committed = True
self.commit_count += 1
def rollback(self) -> None:
self.rollback_count += 1
def close(self) -> None:
self.closed = True
def _patch(monkeypatch: Any, db: _FakeDB) -> _FakeClient:
client = _FakeClient()
monkeypatch.setattr(ird_harvest, "NSPDClient", lambda *a, **kw: client)
monkeypatch.setattr(ird_harvest, "SessionLocal", lambda: db)
return client
def test_harvest_explicit_quarters_upserts_all_layers(monkeypatch: Any) -> None:
"""1 квартал × N ИРД-слоёв → upsert по фиче на слой; commit + close."""
db = _FakeDB()
client = _patch(monkeypatch, db)
n_layers = len(ird_harvest.IRD_HARVEST_LAYER_KEYS)
result = ird_harvest.harvest_ird_overlays(quarters=["66:41:0204016"])
assert result == {"quarters": 1, "features": n_layers}
assert len(client.grid_calls) == n_layers # grid-walk по каждому слою
assert len(db.executed) == n_layers # upsert по фиче на слой
assert db.committed is True
assert db.closed is True
# параметры upsert: geom как JSON-строка, layer_kind из ключей
_, params = db.executed[0]
assert params is not None
assert params["layer_kind"] in ird_harvest.IRD_HARVEST_LAYER_KEYS
assert isinstance(params["geojson"], str)
assert params["reg_numb_border"] == "66:41-6.1"
def test_harvest_skips_empty_quarter(monkeypatch: Any) -> None:
"""Квартал без геометрии в НСПД → 0 кварталов/фич, без падения."""
db = _FakeDB()
client = _patch(monkeypatch, db)
monkeypatch.setattr(client, "search_by_cad", lambda cad, thematic_id=1: _SearchRes(None))
result = ird_harvest.harvest_ird_overlays(quarters=["66:41:9999999"])
assert result == {"quarters": 0, "features": 0}
assert db.executed == []
assert db.committed is True
def test_upsert_feature_skips_missing_geom_data_id(monkeypatch: Any) -> None:
"""Фича без geom_data_id / geometry → не пишется (defensive)."""
db = _FakeDB()
no_id = _Feat({"type": "Polygon", "coordinates": []}, {"reg_numb_border": "x"})
no_geom = _Feat(None, {"geom_data_id": 5})
assert (
ird_harvest._upsert_feature(
db, layer_id=37577, layer_kind="zouit_okn", feature=no_id, fetched_at="t"
)
is False
)
assert (
ird_harvest._upsert_feature(
db, layer_id=37577, layer_kind="zouit_okn", feature=no_geom, fetched_at="t"
)
is False
)
assert db.executed == []
def test_per_quarter_commit_durability(monkeypatch: Any) -> None:
"""Каждый квартал коммитится отдельно → краш середины не теряет прошлые (фикс #1067)."""
db = _FakeDB()
_patch(monkeypatch, db)
result = ird_harvest.harvest_ird_overlays(quarters=["66:41:0000001", "66:41:0000002"])
assert result["quarters"] == 2
# commit per-quarter (2) + финальный = >=2; точно больше одного (не «раз в конце»)
assert db.commit_count >= 2
def test_quarter_failure_rolls_back_and_continues(monkeypatch: Any) -> None:
"""Сбой одного квартала → rollback + продолжение; остальные кварталы коммитятся."""
db = _FakeDB()
client = _patch(monkeypatch, db)
def _flaky(cad: str, thematic_id: int = 1) -> Any:
if cad == "66:41:0000001":
raise RuntimeError("NSPD timeout")
ring = [[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]
return _SearchRes(_Feat({"type": "Polygon", "coordinates": [ring]}, {"cad": cad}))
monkeypatch.setattr(client, "search_by_cad", _flaky)
result = ird_harvest.harvest_ird_overlays(quarters=["66:41:0000001", "66:41:0000002"])
assert db.rollback_count >= 1 # битый квартал откатился
assert result["quarters"] == 1 # второй квартал обработан
assert db.committed is True