gendesign/backend/tests/services/test_zone_regulation_write_session.py
bot-backend f249212695
All checks were successful
Deploy / build-worker (push) Successful in 2m57s
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy (push) Successful in 1m23s
Deploy / build-backend (push) Successful in 1m48s
fix(site-finder): isolate zone-regulation cache write off the request session (#1850 item 4)
Cache-miss upsert now runs in a fresh SessionLocal() instead of committing the shared /analyze transaction mid-request. Read-back via shared session unchanged. Refs #1850
2026-06-25 11:16:26 +00:00

164 lines
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.

"""Тесты изолированной write-сессии в get_or_fetch_zone_regulation (#1850 item 4).
На cache-MISS резолвер пишет регламент в кэш в СОБСТВЕННОЙ короткой сессии (SessionLocal()),
а НЕ коммитит SHARED-сессию вызывающего (/analyze коммитит её ровно один раз в конце через
persist_analysis_run). Проверяем:
- happy path: shared db.commit НИКОГДА не вызывается, fresh write-session коммитит РОВНО раз,
сессия закрыта; read-back идёт по SHARED db (форма результата сохранена для callers);
- graceful: upsert_zone_regulation вернул None (таблица недоступна) → fresh session rollback,
shared db не тронута, функция возвращает результат read-back без падения.
Сеть/БД не дёргаются: фейковый клиент + monkeypatch SessionLocal (recording-фейк) +
отдельная фейковая SHARED Session, считающая commit/rollback.
"""
from __future__ import annotations
from typing import Any
import pytest
from app.services.scrapers.ekb_geoportal_client import ZoneRegulation
from app.services.site_finder import zone_regulation as zr
def _reg(idx: str) -> ZoneRegulation:
return ZoneRegulation(
zone_index=idx,
zone_full_name=f"{idx} зона",
main_vri=[],
conditional_vri=[],
auxiliary_vri=[],
limit_params=[],
)
class _MissThenResolveClient:
"""Фейковый WFS-клиент: индекс зоны есть, но кэш промахивается → live-резолв регламента."""
def __init__(self, zone_index: str) -> None:
self._zone_index = zone_index
self.index_calls = 0
self.regulation_calls = 0
def zone_index_at(self, lon: float, lat: float) -> str:
self.index_calls += 1
return self._zone_index
def zone_regulation_at(self, lon: float, lat: float) -> ZoneRegulation:
self.regulation_calls += 1
return _reg(self._zone_index)
class _SharedDB:
"""Фейковая SHARED Session вызывающего: считает commit/rollback, ничего не пишет.
commit() должен остаться НЕТРОНУТЫМ — фикс #1850 item 4 запрещает преждевременный
коммит in-flight analyze-транзакции из резолвера.
"""
def __init__(self) -> None:
self.commits = 0
self.rollbacks = 0
def commit(self) -> None:
self.commits += 1
def rollback(self) -> None:
self.rollbacks += 1
class _WriteDB:
"""Фейковая короткоживущая write-сессия (то, что отдаёт monkeypatched SessionLocal)."""
def __init__(self) -> None:
self.commits = 0
self.rollbacks = 0
self.closed = False
def commit(self) -> None:
self.commits += 1
def rollback(self) -> None:
self.rollbacks += 1
def close(self) -> None:
self.closed = True
@pytest.fixture(autouse=True)
def _reset_memo() -> Any:
"""Чистый memo до/после теста (процесс-локальный кэш течёт между тестами)."""
zr._reset_zone_index_memo()
yield
zr._reset_zone_index_memo()
def test_cache_miss_writes_in_isolated_session(monkeypatch: Any) -> None:
"""Cache-miss: shared db.commit НЕ вызывается, fresh write-session коммитит РОВНО раз."""
write_db = _WriteDB()
created: list[_WriteDB] = []
def _session_local() -> _WriteDB:
created.append(write_db)
return write_db
monkeypatch.setattr(zr, "SessionLocal", _session_local)
upserts: list[ZoneRegulation] = []
# upsert вернул params_numeric (не None) → write-сессия должна закоммитить.
monkeypatch.setattr(
zr, "upsert_zone_regulation", lambda db, reg, **kw: upserts.append(reg) or {"x": [1]}
)
# get_cached дёргается на SHARED db ДВАЖДЫ: 1) до fetch (cache MISS → None, форсит резолв),
# 2) read-back после commit write-сессии (READ COMMITTED видит строку → dict для callers).
cache_reads: list[Any] = []
def _cached(db: Any, zone_index: str | None, **kw: Any) -> dict[str, Any] | None:
cache_reads.append(db)
assert db is shared # резолвер читает кэш ИМЕННО по SHARED db, не по write-сессии
return None if len(cache_reads) == 1 else {"zone_index": zone_index}
monkeypatch.setattr(zr, "get_cached_zone_regulation", _cached)
shared = _SharedDB()
client = _MissThenResolveClient("Ц-1")
result = zr.get_or_fetch_zone_regulation(shared, 60.6, 56.8, client=client) # type: ignore[arg-type]
# SHARED транзакция НЕ тронута резолвером.
assert shared.commits == 0
assert shared.rollbacks == 0
# Свежая write-сессия закоммитила РОВНО один раз и закрыта.
assert len(created) == 1
assert write_db.commits == 1
assert write_db.rollbacks == 0
assert write_db.closed is True
# Апсёрт ушёл в write-сессию, не в shared.
assert upserts == [_reg("Ц-1")]
# get_cached на SHARED db: cache-check (miss) + read-back (hit) = 2 чтения.
assert len(cache_reads) == 2
# Read-back вернул кэшированную форму (dict) для callers parcels.py / ird_analyze.py.
assert result == {"zone_index": "Ц-1"}
def test_cache_miss_table_missing_rolls_back_gracefully(monkeypatch: Any) -> None:
"""upsert вернул None (таблица недоступна) → fresh session rollback, shared не тронута."""
write_db = _WriteDB()
monkeypatch.setattr(zr, "SessionLocal", lambda: write_db)
# upsert_zone_regulation graceful-возврат None (OperationalError/ProgrammingError внутри).
monkeypatch.setattr(zr, "upsert_zone_regulation", lambda db, reg, **kw: None)
# Кэш недоступен и для read-back → None (форма как при недоступной таблице у callers).
monkeypatch.setattr(zr, "get_cached_zone_regulation", lambda *a, **kw: None)
shared = _SharedDB()
client = _MissThenResolveClient("Ж-2")
result = zr.get_or_fetch_zone_regulation(shared, 60.6, 56.8, client=client) # type: ignore[arg-type]
# Write-сессия откатилась (нечего персистить), не коммитила, закрыта.
assert write_db.commits == 0
assert write_db.rollbacks == 1
assert write_db.closed is True
# SHARED транзакция вообще не тронута.
assert shared.commits == 0
assert shared.rollbacks == 0
# Функция отработала gracefully (read-back None при недоступном кэше).
assert result is None