gendesign/backend/tests/support/pg_session.py
bot-backend f3626540fc
All checks were successful
Deploy / changes (push) Successful in 14s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 1m2s
Deploy / build-worker (push) Successful in 1m3s
Deploy / deploy (push) Successful in 1m17s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
test(ptica): снятие SAVEPOINT теперь роняет тест во всех местах кластера A (#2464) (#2938)
2026-08-19 16:06:59 +00:00

99 lines
4.1 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.

"""Сессия-двойник с настоящей семантикой aborted-транзакции Postgres.
Зачем отдельный помощник, а не MagicMock: у мока нет состояния «транзакция
оборвана», поэтому второй `execute` в тесте проходит независимо от того, обёрнут
первый в SAVEPOINT или нет. Проверка на отравление сессии на таком моке зелена по
построению — она может поймать только проглоченное исключение.
Замер 19.08.2026 (мутационный прогон по кластеру A эпика #2464): у трёх мест из
одиннадцати снятие SAVEPOINT не роняет НИ ОДНОГО теста — saturation (465 passed),
zone_regulation (195), pat_lookup (21). Код там правильный; беззащитны регрессии.
Семантика, которую двойник воспроизводит:
* упавший `execute` переводит транзакцию в aborted;
* любой следующий `execute` в aborted-состоянии падает с тем же текстом, что
отдаёт Postgres, — до конца транзакции;
* выход из `begin_nested()` с исключением = ROLLBACK TO SAVEPOINT: снимает
aborted, внешняя транзакция остаётся рабочей.
"""
from __future__ import annotations
from contextlib import contextmanager
from typing import Any
_ABORTED_MESSAGE = "current transaction is aborted, commands ignored until end of transaction block"
class AbortedTransactionError(RuntimeError):
"""Аналог psycopg InFailedSqlTransaction."""
class PostgresLikeSession:
"""Минимальная сессия: aborted-состояние + рабочий SAVEPOINT.
fail_on: номера вызовов `execute` (с единицы), которые должны упасть.
error: исключение, которым падать (важно — обработчики ловят конкретные типы
SQLAlchemy, а не голый Exception).
rows / row: что отдавать при успешном вызове.
"""
def __init__(
self,
*,
fail_on: tuple[int, ...] = (1,),
error: BaseException | None = None,
rows: list[Any] | None = None,
row: Any = None,
) -> None:
self.calls = 0
self.aborted = False
self.savepoints_entered = 0
self._fail_on = fail_on
self._error = error or RuntimeError("simulated DB failure")
self._rows = rows if rows is not None else []
self._row = row
@contextmanager
def begin_nested(self): # type: ignore[no-untyped-def]
self.savepoints_entered += 1
try:
yield
except BaseException:
# ROLLBACK TO SAVEPOINT — внешняя транзакция снова пригодна.
self.aborted = False
raise
def execute(self, *_args: Any, **_kwargs: Any) -> _Result:
if self.aborted:
raise AbortedTransactionError(_ABORTED_MESSAGE)
self.calls += 1
if self.calls in self._fail_on:
self.aborted = True
raise self._error
return _Result(self._rows, self._row)
# ── прочее, что могут дёрнуть обработчики ────────────────────────────────
def commit(self) -> None:
if self.aborted:
raise AbortedTransactionError(_ABORTED_MESSAGE)
def rollback(self) -> None:
self.aborted = False
class _Result:
def __init__(self, rows: list[Any], row: Any) -> None:
self._rows, self._row = rows, row
def mappings(self) -> _Result:
return self
def all(self) -> list[Any]:
return self._rows
def first(self) -> Any:
return self._row
def scalar(self) -> Any:
return self._row