All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
85 lines
4.2 KiB
Python
85 lines
4.2 KiB
Python
"""Сбой в connection-capacity не должен травить общую сессию (#2464).
|
||
|
||
`_query_power_points` и `_query_water_latest` делали `db.execute` без SAVEPOINT, тогда как
|
||
четыре соседних `_query_*` в том же файле его имеют. Сессия здесь ОБЩАЯ с вызывающим, и
|
||
цепочка выходила такая:
|
||
|
||
сбой в _query_power_points
|
||
→ исключение всплывает (в get_connection_capacity try/except нет)
|
||
→ его ГЛОТАЕТ _get_connection_capacity в full_report_pdf.py ради деградации §3
|
||
→ транзакция остаётся aborted
|
||
→ падает уже СЛЕДУЮЩИЙ запрос — рыночная цена в _generate_concept_result
|
||
→ концепция молча уходит в class_norm-фолбэк
|
||
|
||
То есть отказ проявляется не там, где случился, и выглядит как «нет рыночных данных».
|
||
|
||
Проверяется на `PostgresLikeSession` (tests/support/pg_session.py) — двойнике с настоящей
|
||
семантикой aborted-транзакции. На `MagicMock` такой тест был бы зелёным по построению:
|
||
у него нет aborted-состояния, и любой следующий execute «успешен».
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||
|
||
import pytest
|
||
|
||
from tests.support.pg_session import AbortedTransactionError, PostgresLikeSession
|
||
|
||
_QUERIES = ("_query_power_points", "_query_water_latest")
|
||
|
||
|
||
def _call(name: str, db: PostgresLikeSession):
|
||
from app.services.site_finder import connection_capacity_lookup as mod
|
||
|
||
fn = getattr(mod, name)
|
||
if name == "_query_power_points":
|
||
return fn(db, "POLYGON((0 0,0 1,1 1,1 0,0 0))", 5000)
|
||
return fn(db)
|
||
|
||
|
||
@pytest.mark.parametrize("name", _QUERIES)
|
||
def test_failure_leaves_session_usable(name: str) -> None:
|
||
"""После проглоченного вызывающим сбоя следующий запрос обязан пройти.
|
||
|
||
На origin/main второй execute падает с AbortedTransactionError — ровно та
|
||
отложенная поломка, что уводит концепцию в class_norm.
|
||
"""
|
||
db = PostgresLikeSession(fail_on=(1,))
|
||
|
||
with pytest.raises(RuntimeError): # сам сбой пробрасывается, это ожидаемо
|
||
_call(name, db)
|
||
|
||
# Вызывающий (full_report_pdf._get_connection_capacity) исключение глотает,
|
||
# и дальше по коду идёт СЛЕДУЮЩИЙ запрос на той же сессии.
|
||
db.execute("SELECT 1") # на origin/main здесь AbortedTransactionError
|
||
|
||
|
||
@pytest.mark.parametrize("name", _QUERIES)
|
||
def test_savepoint_is_actually_entered(name: str) -> None:
|
||
"""Запрос действительно идёт внутри SAVEPOINT, а не просто «не падает».
|
||
|
||
Отдельно от предыдущего: тест выше прошёл бы и если бы кто-то «починил»
|
||
функцию, убрав из неё запрос целиком.
|
||
"""
|
||
db = PostgresLikeSession(fail_on=(), rows=[]) # здоровый путь
|
||
_call(name, db)
|
||
assert db.savepoints_entered >= 1, (
|
||
f"{name}: запрос выполнен вне SAVEPOINT — при сбое сессия останется aborted"
|
||
)
|
||
assert db.calls >= 1, f"{name}: запрос вообще не выполнялся"
|
||
|
||
|
||
def test_double_reproduces_poisoning_without_savepoint() -> None:
|
||
"""Контроль на сам двойник: без SAVEPOINT он ДОЛЖЕН травить сессию.
|
||
|
||
Без этой проверки зелёные тесты выше нельзя отличить от «двойник ничего не
|
||
моделирует» — та же ловушка, что с MagicMock.
|
||
"""
|
||
db = PostgresLikeSession(fail_on=(1,))
|
||
with pytest.raises(RuntimeError):
|
||
db.execute("BOOM")
|
||
with pytest.raises(AbortedTransactionError):
|
||
db.execute("SELECT 1")
|