gendesign/backend/tests/services/site_finder/test_riasurt_lookup.py
bot-backend ce714ecf79
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m1s
CI / backend-tests (pull_request) Successful in 15m19s
fix(backend): SAVEPOINT-isolate cluster-A db.execute swallows (batch 2) (#2464)
krt_lookup (×2), okn_lookup, ppt_tep_lookup, riasurt_lookup, premises_lookup,
ird_analyze._krt_at, and product_scoring._poi_weight_sum (both the centroid
lookup and the compute_poi_weighted_top7 call) swallowed db.execute failures
without a SAVEPOINT, leaving the shared analyze/report session aborted for
later queries. Wrap each swallow site in db.begin_nested(). locations.py's
pass-1 infra/POI block uses a plain db.rollback() instead — its session is
owned by the weekly location_refresh Celery task (own SessionLocal, verified
single caller-chain), not shared, and nothing is pending at that point.

Refs #2464 (cluster A session-poisoning, Wave 2).
2026-07-08 11:19:20 +05:00

169 lines
6 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.

"""Тесты riasurt_lookup (#108) — is_in_aglomeration_but_not_ekb + parcel_riasurt_gate.
БД мокается фейк-сессией, возвращающей заранее заданные mapping-строки. Проверяем
агломерация-гейтинг (ЕКБ-сити исключён), topic-бакеты, graceful при ошибке БД.
"""
from __future__ import annotations
import contextlib
from typing import Any
from sqlalchemy.exc import ProgrammingError
from app.services.site_finder import riasurt_lookup
from app.services.site_finder.riasurt_lookup import (
is_in_aglomeration_but_not_ekb,
parcel_riasurt_gate,
)
_WKT = "POLYGON((60 56,60.1 56,60.1 56.1,60 56.1,60 56))"
def test_is_in_aglomeration_excludes_ekb_city() -> None:
"""ЕКБ-сити (66:41) → False."""
assert is_in_aglomeration_but_not_ekb("66:41:0204016:10") is False
assert is_in_aglomeration_but_not_ekb("66:41") is False
def test_is_in_aglomeration_includes_outskirts() -> None:
"""Окраины (66:35 Берёзовский, 66:62 В.Пышма, 66:25 Сысерть) → True."""
assert is_in_aglomeration_but_not_ekb("66:35:0101001:5") is True
assert is_in_aglomeration_but_not_ekb("66:62:1234567:1") is True
assert is_in_aglomeration_but_not_ekb("66:25:0000001:99") is True
def test_is_in_aglomeration_rejects_non_sverdl_and_empty() -> None:
"""Не-Свердл (не 66:*) и пусто → False."""
assert is_in_aglomeration_but_not_ekb("50:21:0000001:1") is False
assert is_in_aglomeration_but_not_ekb(None) is False
assert is_in_aglomeration_but_not_ekb("") is False
class _Row(dict):
"""mapping-строка: поддерживает r["key"]."""
class _Result:
def __init__(self, rows: list[_Row]) -> None:
self._rows = rows
def mappings(self) -> _Result:
return self
def all(self) -> list[_Row]:
return self._rows
class _FakeDB:
def __init__(self, rows: list[_Row] | Exception) -> None:
self._rows = rows
self.begin_nested_calls = 0
@contextlib.contextmanager
def begin_nested(self) -> Any:
"""Фейковый SAVEPOINT — не глотает исключение из execute (как реальный)."""
self.begin_nested_calls += 1
yield
def execute(self, sql: Any, params: dict[str, Any] | None = None) -> _Result:
if isinstance(self._rows, Exception):
raise self._rows
return _Result(self._rows)
def test_gate_not_applicable_for_ekb_city() -> None:
"""ЕКБ-сити → applicable=False, БД не дёргается."""
db = _FakeDB([])
gate = parcel_riasurt_gate(db, _WKT, "66:41:0204016:10") # type: ignore[arg-type]
assert gate["applicable"] is False
assert gate["overlaps"] == []
def test_gate_buckets_topics() -> None:
"""Пересечения раскладываются по topic-бакетам."""
rows = [
_Row(
source_layer_id=845274,
layer_topic="territorial_zone",
mo_name="Берёзовский",
obshnz="Ж-1",
description="Зона застройки",
),
_Row(
source_layer_id=846381,
layer_topic="functional_zone",
mo_name="Берёзовский",
obshnz=None,
description="Жилая функц.зона",
),
_Row(
source_layer_id=844759,
layer_topic="red_lines",
mo_name="Берёзовский",
obshnz=None,
description=None,
),
_Row(
source_layer_id=846365,
layer_topic="szz",
mo_name="Берёзовский",
obshnz=None,
description=None,
),
_Row(
source_layer_id=845425,
layer_topic="flood_zone",
mo_name="Берёзовский",
obshnz=None,
description=None,
),
_Row(
source_layer_id=844478,
layer_topic="krt",
mo_name="Берёзовский",
obshnz=None,
description=None,
),
]
db = _FakeDB(rows)
gate = parcel_riasurt_gate(db, _WKT, "66:35:0101001:5") # type: ignore[arg-type]
assert gate["applicable"] is True
assert gate["tier"] == "Зона застройки"
assert gate["func_zone"] == "Жилая функц.зона"
assert gate["red_lines"] is True
assert gate["szz"] is True
assert gate["szo"] is False
assert gate["flood_zone"] is True
assert gate["krt"] is True
assert len(gate["overlaps"]) == 6
def test_gate_graceful_on_db_error() -> None:
"""Ошибка БД (pre-migration) → пустой gate, не падаем."""
db = _FakeDB(ProgrammingError("stmt", {}, Exception("no table")))
gate = parcel_riasurt_gate(db, _WKT, "66:35:0101001:5") # type: ignore[arg-type]
assert gate == riasurt_lookup.RIASURT_EMPTY_GATE
def test_gate_empty_wkt_returns_empty() -> None:
"""Нет WKT → пустой gate."""
db = _FakeDB([])
gate = parcel_riasurt_gate(db, None, "66:35:0101001:5") # type: ignore[arg-type]
assert gate["applicable"] is False
def test_gate_uses_savepoint_and_session_reusable_after_failure() -> None:
"""#2464 cluster A: db.execute обёрнут в begin_nested (SAVEPOINT) — сбой не
отравляет сессию. Follow-up вызов на ТОЙ ЖЕ db-инстансе успевает."""
db = _FakeDB(ProgrammingError("stmt", {}, Exception("no table")))
gate = parcel_riasurt_gate(db, _WKT, "66:35:0101001:5") # type: ignore[arg-type]
assert gate == riasurt_lookup.RIASURT_EMPTY_GATE
assert db.begin_nested_calls == 1
# follow-up на той же сессии (таблица теперь "доступна").
db._rows = []
gate2 = parcel_riasurt_gate(db, _WKT, "66:35:0101001:5") # type: ignore[arg-type]
assert gate2["applicable"] is True
assert db.begin_nested_calls == 2