ci: пропуск теста обязан назвать себя — иначе прогон красный #2745
6 changed files with 441 additions and 12 deletions
|
|
@ -66,16 +66,104 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.backend == 'true'
|
||||
# Postgres-сервис (#2745). ДО него лэйн был mock-only: DATABASE_URL указывал на
|
||||
# заведомо мёртвый `localhost:5432/test`, и девять тестов с `_live_session()`
|
||||
# self-skip'ались — в CI они не бежали НИ РАЗУ. Так и разъехался со схемой
|
||||
# test_house_dedup_merge (#2740: houses.url стал NOT NULL), а
|
||||
# test_gar_flats_loader вообще падал до первого утверждения (#2744).
|
||||
#
|
||||
# Замер перед включением: полный сьют на mock-лэйне 122с / 3858 passed / 10 skipped,
|
||||
# тот же сьют против живой БД — 106с / 3867 passed / 1 skipped. Живая БД не
|
||||
# медленнее, поэтому НЕ добавляем второй job, а чиним этот: один прогон, на
|
||||
# девять реальных проверок больше. Накладные — только подъём контейнера и
|
||||
# bootstrap схемы (219 файлов, ~20с).
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./tradein-mvp/backend
|
||||
env:
|
||||
# psycopg v3 требует parseable URL на импорте; реального коннекта нет —
|
||||
# DB-тесты мокаются (mirror deploy-tradein.yml test-job).
|
||||
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
||||
# Имя контейнера уникально на прогон: параллельные PR не дерутся за него.
|
||||
CI_PG: ci-pg-tradein-${{ github.run_id }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Поднять Postgres и собрать схему tradein
|
||||
working-directory: .
|
||||
# ПОЧЕМУ НЕ `services:` И ПОЧЕМУ БЕЗ ПУБЛИКАЦИИ ПОРТА.
|
||||
# Раннер запускает и job, и сервис-контейнеры с `--network host` (видно в
|
||||
# логе прогона: `docker create image=... network="host"`), а на 5432 того
|
||||
# же хоста слушает ПРОДОВЫЙ Postgres. Попытка через `services:` +
|
||||
# `ports: 5432:5432` кончилась тем, что сервис-контейнер не смог занять
|
||||
# порт, а psql из job'а ушёл В ПРОД и получил
|
||||
# `password authentication failed for user "tradein"`. То есть
|
||||
# `localhost:5432` из job'а на этом раннере — боевая база, а не тестовая.
|
||||
# Поэтому контейнер поднимаем сами, в bridge-сети, БЕЗ публикации порта,
|
||||
# и ходим по его собственному IP: прод недостижим в принципе, параллельные
|
||||
# прогоны не конфликтуют, psql берём из самого контейнера.
|
||||
#
|
||||
# ОДИН шаг, а не два: между шагами контейнер успевал исчезнуть, и
|
||||
# bootstrap падал на `container is not running`.
|
||||
#
|
||||
# `pg_isready -h 127.0.0.1`, а НЕ через unix-сокет: на время initdb образ
|
||||
# поднимает ВРЕМЕННЫЙ сервер с listen_addresses='' — по сокету он уже
|
||||
# отвечает «accepting connections», хотя снаружи БД ещё не существует, а
|
||||
# впереди рестарт. Проба по TCP зеленеет только на настоящем сервере —
|
||||
# том самом, к которому пойдут тесты.
|
||||
#
|
||||
# postgis, не plain postgres: tests/tasks/test_cadastral_geo_match.py
|
||||
# проверяет KNN по geometry (PostGIS_Version() в connectivity-probe).
|
||||
# Имя БД ОБЯЗАНО отличаться от `test`: `_live_session()` считает DSN с
|
||||
# `localhost:5432/test` заглушкой и вернул бы None — контейнер поднялся
|
||||
# бы, а тесты всё равно скипались.
|
||||
run: |
|
||||
set -u
|
||||
docker rm -f "$CI_PG" >/dev/null 2>&1 || true
|
||||
docker run -d --name "$CI_PG" \
|
||||
-e POSTGRES_DB=tradein -e POSTGRES_USER=tradein -e POSTGRES_PASSWORD=tradein \
|
||||
postgis/postgis:16-3.4
|
||||
|
||||
ready=""
|
||||
for _ in $(seq 1 45); do
|
||||
if docker exec "$CI_PG" pg_isready -h 127.0.0.1 -U tradein -q 2>/dev/null; then
|
||||
ready=1; break
|
||||
fi
|
||||
[ "$(docker inspect -f '{{.State.Status}}' "$CI_PG" 2>/dev/null)" = "running" ] || break
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$ready" ]; then
|
||||
echo "::error::Postgres не поднялся; статус=$(docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' "$CI_PG" 2>&1)"
|
||||
docker logs --tail 50 "$CI_PG" 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CI_PG")
|
||||
[ -n "$ip" ] || { echo "::error::не удалось узнать IP контейнера $CI_PG"; exit 1; }
|
||||
echo "DATABASE_URL=postgresql+psycopg://tradein:tradein@${ip}:5432/tradein" >> "$GITHUB_ENV"
|
||||
echo "✓ Postgres на ${ip}:5432 (контейнер $CI_PG)"
|
||||
|
||||
# Тот же порядок и тот же строгий режим, что в deploy-tradein.yml:
|
||||
# `ls | sort` + ON_ERROR_STOP=on, падение любой миграции → job RED.
|
||||
# Никаких «применилось как получилось»: схема в CI либо та же, что на
|
||||
# проде, либо гейта нет.
|
||||
docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q -c \
|
||||
"CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE ROLE gendesign_reader;"
|
||||
for sql_file in $(ls -1 tradein-mvp/backend/data/sql/*.sql | sort); do
|
||||
fname=$(basename "$sql_file")
|
||||
# ЕДИНСТВЕННОЕ исключение, и оно названо вслух: 077 — не DDL, а
|
||||
# backfill, читающий foreign table gendesign_rosreestr_deals из БД
|
||||
# ДРУГОГО стека через postgres_fdw. В CI второй БД нет, USER MAPPING
|
||||
# создать не из чего. На пустых таблицах backfill всё равно no-op.
|
||||
if [ "$fname" = "077_dedup_hash_plain_key_backfill.sql" ]; then
|
||||
echo "⚠ пропускаю $fname — postgres_fdw к БД gendesign, которой в CI нет"
|
||||
continue
|
||||
fi
|
||||
docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q < "$sql_file" \
|
||||
|| { echo "::error::миграция $fname не применилась"; docker logs --tail 20 "$CI_PG" 2>&1 || true; exit 1; }
|
||||
done
|
||||
echo "✓ схема собрана: $(docker exec "$CI_PG" psql -U tradein -d tradein -tAc \
|
||||
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public'") таблиц"
|
||||
|
||||
- name: Install uv
|
||||
# Официальный standalone-инсталлер. НЕ astral-sh/setup-uv — он ломается
|
||||
# на Forgejo-runner с PEP 668 externally-managed-environment (#666 CI).
|
||||
|
|
@ -121,7 +209,19 @@ jobs:
|
|||
# NB: в deploy-tradein.yml (post-merge test-job) свой экземпляр этого
|
||||
# deselect'а — он остаётся до #2680, который правит тот файл. Расхождение
|
||||
# безвредно: pre-merge гейт тест гоняет, post-merge просто пропустит зелёный.
|
||||
run: uv run pytest -q
|
||||
#
|
||||
# `-rs` (#2745) — КАЖДЫЙ пропуск печатает свою причину в лог job'а. Без него
|
||||
# `-q` рисует пропуск точкой `s`, неотличимой на глаз от прогона: ровно так
|
||||
# девять DB-тестов «шли зелёными», ничего не проверяя. Пропуск, который не
|
||||
# называет себя вслух, со временем перестаёт быть верным.
|
||||
run: uv run pytest -q -rs
|
||||
|
||||
- name: Снести тестовый Postgres
|
||||
# if: always() — контейнер уходит и когда сьют красный, и когда прогон
|
||||
# отменён concurrency-группой. Иначе на раннере копятся мёртвые контейнеры.
|
||||
if: always()
|
||||
working-directory: .
|
||||
run: docker rm -f "$CI_PG" >/dev/null 2>&1 || true
|
||||
|
||||
# Тесты браузерного сайдкара (#2722). До этого job'а они не бежали НИГДЕ:
|
||||
# ci-tradein гейтил только backend/frontend, deploy-tradein — тоже, а каталог
|
||||
|
|
@ -169,7 +269,9 @@ jobs:
|
|||
- name: Run pytest (tradein-mvp/browser)
|
||||
# Каталог без pyproject/pytest.ini → дефолтная конфигурация, ничего
|
||||
# не deselect'ится. Ожидание: 108 passed, 0 failed, 0 skipped.
|
||||
run: pytest -q
|
||||
# `-rs`: если однажды появится пропуск, он назовёт причину в логе, а не
|
||||
# растворится в строке точек.
|
||||
run: pytest -q -rs
|
||||
|
||||
frontend-checks:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -69,6 +69,20 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.backend == 'true'
|
||||
# Postgres-сервис (#2745). Раньше DATABASE_URL указывал на заведомо мёртвый
|
||||
# хост, и весь tests/sql/ (10 тестов: #17 velocity-alerts, #99 ДДУ-индикатор,
|
||||
# #295 weighted AVG) self-skip'ался connectivity-probe'ом — в CI эти проверки
|
||||
# не бежали ни разу с момента написания.
|
||||
#
|
||||
# plain postgres:16, БЕЗ PostGIS: тесты tests/sql/ строят себе временные
|
||||
# таблицы (CREATE TEMP TABLE) и не трогают ни geometry, ни реальную схему —
|
||||
# проверено локально, 16 passed за 1.3с. Поэтому и bootstrap схемы здесь не
|
||||
# нужен, в отличие от tradein-лэйна.
|
||||
#
|
||||
# TEST_DATABASE_URL НАМЕРЕННО НЕ задаётся: на него завязан tests/integration/
|
||||
# (phantom-column gate), которому нужна КОПИЯ ПРОДОВОЙ схемы через pg_dump по
|
||||
# SSH-туннелю. Пустой контейнер дал бы там красноту на пустом месте, поэтому
|
||||
# integration остаётся честно пропущенным — с причиной в логе (`-rs`).
|
||||
defaults:
|
||||
run:
|
||||
working-directory: backend
|
||||
|
|
@ -76,14 +90,53 @@ jobs:
|
|||
# TESTING=1 активирует RBAC-bypass (app/main.py rbac_guard пропускает
|
||||
# запросы при settings.testing=True) — иначе 401 на всём /api/v1.
|
||||
TESTING: "1"
|
||||
# Stub DSN: psycopg v3 требует parseable URL на импорте; реального коннекта
|
||||
# нет — DB-тесты мокаются, real-DB тест (tests/sql/) self-skip'ается через
|
||||
# connectivity-probe к этому хосту (5432 недоступен → skip).
|
||||
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
||||
REDIS_URL: redis://localhost:6379/0
|
||||
# Имя контейнера уникально на прогон: параллельные PR не дерутся за него.
|
||||
CI_PG: ci-pg-backend-${{ github.run_id }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Поднять Postgres для тестов
|
||||
working-directory: .
|
||||
# ПОЧЕМУ НЕ `services:` И ПОЧЕМУ БЕЗ ПУБЛИКАЦИИ ПОРТА — подробный разбор в
|
||||
# ci-tradein.yml (тот же раннер). Кратко: job и сервис-контейнеры идут с
|
||||
# `--network host`, а на 5432 этого хоста слушает ПРОДОВЫЙ Postgres, то
|
||||
# есть `localhost:5432` из job'а — боевая база. Поднимаем контейнер сами,
|
||||
# в bridge-сети, без публикации порта, ходим по его IP.
|
||||
#
|
||||
# `pg_isready -h 127.0.0.1`, а не через unix-сокет: по сокету отвечает
|
||||
# ВРЕМЕННЫЙ сервер фазы initdb (listen_addresses=''), после которой БД
|
||||
# ещё перезапускается. Проба по TCP зеленеет только на настоящем сервере.
|
||||
#
|
||||
# plain postgres:16, БЕЗ PostGIS: тесты tests/sql/ строят себе временные
|
||||
# таблицы и не трогают ни geometry, ни реальную схему — bootstrap схемы
|
||||
# здесь не нужен вовсе, в отличие от tradein-лэйна.
|
||||
run: |
|
||||
set -u
|
||||
docker rm -f "$CI_PG" >/dev/null 2>&1 || true
|
||||
docker run -d --name "$CI_PG" \
|
||||
-e POSTGRES_DB=gendesign_ci -e POSTGRES_USER=gendesign -e POSTGRES_PASSWORD=gendesign \
|
||||
postgres:16
|
||||
|
||||
ready=""
|
||||
for _ in $(seq 1 45); do
|
||||
if docker exec "$CI_PG" pg_isready -h 127.0.0.1 -U gendesign -q 2>/dev/null; then
|
||||
ready=1; break
|
||||
fi
|
||||
[ "$(docker inspect -f '{{.State.Status}}' "$CI_PG" 2>/dev/null)" = "running" ] || break
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$ready" ]; then
|
||||
echo "::error::Postgres не поднялся; статус=$(docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' "$CI_PG" 2>&1)"
|
||||
docker logs --tail 50 "$CI_PG" 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CI_PG")
|
||||
[ -n "$ip" ] || { echo "::error::не удалось узнать IP контейнера $CI_PG"; exit 1; }
|
||||
echo "DATABASE_URL=postgresql+psycopg://gendesign:gendesign@${ip}:5432/gendesign_ci" >> "$GITHUB_ENV"
|
||||
echo "✓ Postgres на ${ip}:5432 (контейнер $CI_PG)"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
|
@ -136,10 +189,13 @@ jobs:
|
|||
# но --ignore — belt-and-suspenders на случай сбора фикстур).
|
||||
# tests/integration self-skip'ается через requires_test_db (skipif на
|
||||
# TEST_DATABASE_URL, который тут не задан) → НЕ игнорим, оно чисто skip'ается.
|
||||
# tests/sql/ mv_layout self-skip'ается через Postgres-connectivity probe
|
||||
# (5432 недоступен в этом mock-lane) → SKIP. Это intended.
|
||||
# tests/sql/ теперь РЕАЛЬНО ИДУТ — postgres-контейнер выше (#2745).
|
||||
# PDF-тесты ИДУТ (libpango выше). Target: 0 failed, skips OK.
|
||||
#
|
||||
# `-rs` (#2745): каждый оставшийся пропуск печатает причину. Под `-q` без
|
||||
# него пропуск неотличим от прогона — именно так проверка тихо перестаёт
|
||||
# исполняться и об этом узнают, когда на неё надо опереться (#2722/#2729/#2740).
|
||||
#
|
||||
# Coverage-gate (#68): --cov=app меряет покрытие пакета app/.
|
||||
# --cov-fail-under=65 → job RED если покрытие упало ниже baseline
|
||||
# (измерено 2026-06: mock-lane сьют ~71%, см. [tool.coverage] в pyproject;
|
||||
|
|
@ -148,7 +204,7 @@ jobs:
|
|||
# coverage.xml — артефакт для будущего Codecov/Coveralls upload (#68 badge).
|
||||
# term-missing → видно непокрытые строки прямо в job-логе.
|
||||
run: |
|
||||
uv run pytest -q --ignore=tests/smoke \
|
||||
uv run pytest -q -rs --ignore=tests/smoke \
|
||||
--cov=app \
|
||||
--cov-report=term-missing:skip-covered \
|
||||
--cov-report=xml:coverage.xml \
|
||||
|
|
@ -169,6 +225,13 @@ jobs:
|
|||
echo "$report"
|
||||
fi
|
||||
|
||||
- name: Снести тестовый Postgres
|
||||
# if: always() — контейнер уходит и когда сьют красный, и когда прогон
|
||||
# отменён concurrency-группой. Иначе на раннере копятся мёртвые контейнеры.
|
||||
if: always()
|
||||
working-directory: .
|
||||
run: docker rm -f "$CI_PG" >/dev/null 2>&1 || true
|
||||
|
||||
frontend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ NB: RBAC-гейт (app/main.py `rbac_guard`) требует заголовок `
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -40,3 +41,70 @@ def _clear_dependency_overrides():
|
|||
app.dependency_overrides.clear()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── Бюджет пропусков (#2745) ──────────────────────────────────────────────────
|
||||
#
|
||||
# Три инцидента за одни сутки (#2722, #2729, #2740) — один класс дефекта:
|
||||
# проверка, которая тихо не исполняется, со временем перестаёт быть верной, и
|
||||
# узнают об этом ровно тогда, когда на неё понадобилось опереться. `-rs` в CI
|
||||
# делает пропуск ГРОМКИМ; этот хук делает его УЧТЁННЫМ — пропущено может быть
|
||||
# только то, что записано в `tests/skip_allowlist.txt`. Любой новый пропуск (в
|
||||
# том числе возникший сам: отвалилась зависимость, исчезла БД, кто-то дописал
|
||||
# skipif «пока починю») роняет прогон, пока его не внесут в список осознанно.
|
||||
#
|
||||
# Список — НАДмножество: покрывает и CI (где есть Postgres и native-libs), и
|
||||
# ноутбук разработчика (где их нет). Запись, чей пропуск не случился, — не
|
||||
# ошибка; ошибка — пропуск без записи.
|
||||
#
|
||||
# Сюда же попадает xfail: pytest рапортует его как skipped, поэтому xfail без
|
||||
# `strict=True` (зеленеет и когда тест внезапно ПРОШЁЛ) тоже придётся объявить.
|
||||
#
|
||||
# Чего хук НЕ видит: `--deselect` / `-k` / `-m` — они убирают тест до прогона,
|
||||
# отчёта о пропуске не возникает вовсе. Против них работает правило «никаких
|
||||
# deselect'ов в workflow» (см. .forgejo/workflows/ci-tradein.yml).
|
||||
|
||||
_SKIP_ALLOWLIST_PATH = Path(__file__).parent / "skip_allowlist.txt"
|
||||
_observed_skips: set[str] = set()
|
||||
|
||||
|
||||
def _allowed_skips() -> set[str]:
|
||||
if not _SKIP_ALLOWLIST_PATH.exists():
|
||||
return set()
|
||||
lines = _SKIP_ALLOWLIST_PATH.read_text(encoding="utf-8").splitlines()
|
||||
return {stripped for line in lines if (stripped := line.split("#", 1)[0].strip())}
|
||||
|
||||
|
||||
def _record_skip(report) -> None:
|
||||
if report.skipped:
|
||||
# nodeid без «[параметров]»: список фиксирует, какая ПРОВЕРКА может
|
||||
# отсутствовать, а не каждую её параметризацию.
|
||||
_observed_skips.add(report.nodeid.split("[", 1)[0])
|
||||
|
||||
|
||||
def pytest_runtest_logreport(report) -> None:
|
||||
_record_skip(report)
|
||||
|
||||
|
||||
def pytest_collectreport(report) -> None:
|
||||
# Модульный pytest.skip(allow_module_level=True) приходит сюда, а не в
|
||||
# logreport, и nodeid у него — путь файла.
|
||||
_record_skip(report)
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus) -> None:
|
||||
unlisted = sorted(_observed_skips - _allowed_skips())
|
||||
if not unlisted:
|
||||
return
|
||||
print(
|
||||
f"\nНЕУЧТЁННЫЙ ПРОПУСК ({len(unlisted)}): проверка не исполнилась и не "
|
||||
f"объявлена в {_SKIP_ALLOWLIST_PATH.name}:"
|
||||
)
|
||||
for nodeid in unlisted:
|
||||
print(f" - {nodeid}")
|
||||
print(
|
||||
"Почини тест либо внеси его в skip_allowlist.txt с причиной — "
|
||||
"пропуск без записи неотличим от пройденной проверки."
|
||||
)
|
||||
if exitstatus == 0:
|
||||
session.exitstatus = 1
|
||||
|
|
|
|||
89
backend/tests/skip_allowlist.txt
Normal file
89
backend/tests/skip_allowlist.txt
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Объявленные пропуски сьюта backend/.
|
||||
#
|
||||
# Пропуск, которого здесь нет, роняет прогон (хук в tests/conftest.py). Смысл: не
|
||||
# запретить пропуски, а запретить НЕЗАМЕТНЫЕ. Проверка, которая тихо перестала
|
||||
# исполняться, со временем расходится с кодом, и узнают об этом ровно тогда,
|
||||
# когда на неё понадобилось опереться (#2722, #2729, #2740).
|
||||
#
|
||||
# Формат: один nodeid в строке, без «[параметров]»; `#` — комментарий.
|
||||
# Прежде чем добавить строку, ответь: почему эту проверку нельзя выполнить ЗДЕСЬ
|
||||
# и где она выполняется вместо этого. Нет ответа — это не пропуск, а дыра.
|
||||
#
|
||||
# Список — НАДмножество сред: на CI (ubuntu + libpango + postgres-сервис) часть
|
||||
# записей не срабатывает, на ноутбуке без них — срабатывает. Запись, чей пропуск
|
||||
# не случился, безвредна; пропуск без записи — нет.
|
||||
|
||||
# ── WeasyPrint: нужны native-либы (Pango/cairo/GObject) ───────────────────────
|
||||
# В CI ЭТИ ТЕСТЫ ИДУТ: ci.yml ставит libcairo2/libpango-1.0-0/libpangoft2-1.0-0.
|
||||
# Записи нужны только для macOS-dev, где libgobject-2.0-0 нет.
|
||||
tests/api/v1/test_parcels_forecast.py::test_export_forecast_pdf_returns_pdf
|
||||
tests/services/exporters/test_report_pdf.py::TestConfidenceSectionParity::test_dict_shaped_factor_produces_valid_pdf
|
||||
tests/services/exporters/test_report_pdf.py::TestFullReportExport::test_accepts_as_dict_input
|
||||
tests/services/exporters/test_report_pdf.py::TestFullReportExport::test_returns_non_trivial_length
|
||||
tests/services/exporters/test_report_pdf.py::TestFullReportExport::test_returns_pdf_magic_bytes
|
||||
tests/services/exporters/test_report_pdf.py::TestGracefulPartialReport::test_empty_report_still_valid_pdf
|
||||
tests/services/exporters/test_report_pdf.py::TestGracefulPartialReport::test_garbage_input_does_not_crash
|
||||
tests/services/exporters/test_report_pdf.py::TestGracefulPartialReport::test_partial_report_some_sections
|
||||
tests/services/generative/test_exporters.py::test_pdf_export_produces_pdf_bytes
|
||||
tests/test_layout_tz_pdf.py
|
||||
|
||||
# ── phantom-column gate: нужна КОПИЯ ПРОДОВОЙ схемы ───────────────────────────
|
||||
# Смысл этих тестов — поймать колонку, которой нет в реальной БД, поэтому пустой
|
||||
# сервис-контейнер их не заменяет: против пустой схемы они дали бы красноту, не
|
||||
# относящуюся к делу. Условие: TEST_DATABASE_URL на SSH-туннель к проду
|
||||
# (`ssh -N gendesign` → localhost:15432), см. tests/integration/conftest.py.
|
||||
# ЗАПУСКАТЬ ВРУЧНУЮ после правок SQL-запросов в app/services/**.
|
||||
tests/integration/test_analyze_parcels_sql.py::TestIrdOverlapSql::test_explain_ird_overlap
|
||||
tests/integration/test_analyze_parcels_sql.py::TestNeighborsSummarySql::test_explain_neighbors_summary
|
||||
tests/integration/test_phantom_columns.py::TestCadGeoTables::test_parcel_centroid_query
|
||||
tests/integration/test_phantom_columns.py::TestDomrfKnFlats::test_avg_price_query
|
||||
tests/integration/test_phantom_columns.py::TestDomrfKnFlats::test_supply_batch_query
|
||||
tests/integration/test_phantom_columns.py::TestDomrfKnObjects::test_competitors_full_cte_query
|
||||
tests/integration/test_phantom_columns.py::TestDomrfKnObjects::test_competitors_radius_query
|
||||
tests/integration/test_phantom_columns.py::TestDomrfKnObjects::test_velocity_competitor_query_columns
|
||||
tests/integration/test_phantom_columns.py::TestEkburgConstructionPermits::test_permits_rns_columns
|
||||
tests/integration/test_phantom_columns.py::TestEkburgConstructionPermits::test_permits_rve_columns
|
||||
tests/integration/test_phantom_columns.py::TestEkburgConstructionPermits::test_recent_permits_query
|
||||
tests/integration/test_phantom_columns.py::TestMvLayoutVelocity::test_velocity_by_room_bucket
|
||||
tests/integration/test_phantom_columns.py::TestObjectiveTables::test_objective_corpus_room_month_columns
|
||||
tests/integration/test_phantom_columns.py::TestObjectiveTables::test_objective_mapping_columns
|
||||
tests/integration/test_phantom_columns.py::TestObjectiveTables::test_sold_count_query
|
||||
|
||||
# ── SQL-логика на живой Postgres ──────────────────────────────────────────────
|
||||
# В CI ЭТИ ТЕСТЫ ИДУТ с #2745 (postgres-сервис в ci.yml); до него не бежали ни
|
||||
# разу с момента написания. Схема не нужна — строят себе временные таблицы.
|
||||
# Локально: docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=... postgres:16
|
||||
tests/sql/test_ddu_price_indicator.py::test_basis_and_previous_index
|
||||
tests/sql/test_ddu_price_indicator.py::test_packaged_deal_bucketed_by_per_unit_area
|
||||
tests/sql/test_ddu_price_indicator.py::test_prev_period_value_skips_filtered_quarter
|
||||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_hand_computed_weighted_average
|
||||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_no_zero_months_weighted_equals_naive
|
||||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_nullif_prevents_division_by_zero
|
||||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_weighted_differs_from_naive_sparse_project
|
||||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_zero_deal_months_skew_naive_avg
|
||||
tests/sql/test_velocity_alerts.py::test_lookback_anchors_to_latest_data_month
|
||||
tests/sql/test_velocity_alerts.py::test_sharp_drop_is_flagged
|
||||
|
||||
# ── Живая сеть NSPD (nspd.gov.ru) ─────────────────────────────────────────────
|
||||
# Ходят в внешний сервис: в гейте это флейк и зависимость от чужого аптайма.
|
||||
# Запуск вручную: uv run pytest tests/scrapers/ -m slow / -m integration -s
|
||||
tests/scrapers/test_nspd_bulk_client.py::test_list_objects_in_building_real
|
||||
tests/scrapers/test_nspd_bulk_client.py::test_search_by_quarter_empty
|
||||
tests/scrapers/test_nspd_bulk_client.py::test_search_by_quarter_real_ekb
|
||||
tests/scrapers/test_nspd_bulk_client.py::test_wms_feature_info_real
|
||||
tests/scrapers/test_nspd_grid_walk.py::test_live_nspd_grid_walk_skipped
|
||||
|
||||
# ── Образцы, которых нет в git ────────────────────────────────────────────────
|
||||
# ДОЛГ, а не норма: парсеры проверяются на реальных дампах, которые лежат только
|
||||
# в untracked `.playwright-mcp/`. Пока файлы не в репозитории, эти 8 проверок не
|
||||
# исполняются нигде. Правильная починка — закоммитить образцы в tests/fixtures/
|
||||
# (как сделано в tradein-mvp/backend/tests/fixtures/*.html), после чего строки
|
||||
# отсюда убрать.
|
||||
tests/services/scrapers/test_ekb_krt_registry.py::TestMultiSiteCountFromDump::test_no_duplicate_site_names
|
||||
tests/services/scrapers/test_ekb_krt_registry.py::TestMultiSiteCountFromDump::test_site_count_from_dump
|
||||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_phasing_has_area
|
||||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_phasing_has_rows
|
||||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_raw_tables_not_empty
|
||||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_tep_has_rows
|
||||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_zone_balance_has_itogo
|
||||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_zone_balance_has_rows
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -57,3 +58,70 @@ def _no_leaked_password_verify_slots():
|
|||
f"тест оставил {inflight} занятых слотов проверки пароля (по ключам: {by_key}) — "
|
||||
"утечка слота при пуле в 1 поток это вечный 429 всем на входе"
|
||||
)
|
||||
|
||||
|
||||
# ── Бюджет пропусков (#2745) ──────────────────────────────────────────────────
|
||||
#
|
||||
# Три инцидента за одни сутки (#2722, #2729, #2740) — один класс дефекта:
|
||||
# проверка, которая тихо не исполняется, со временем перестаёт быть верной, и
|
||||
# узнают об этом ровно тогда, когда на неё понадобилось опереться. `-rs` в CI
|
||||
# делает пропуск ГРОМКИМ; этот хук делает его УЧТЁННЫМ — пропущено может быть
|
||||
# только то, что записано в `tests/skip_allowlist.txt`. Любой новый пропуск (в
|
||||
# том числе возникший сам: отвалилась зависимость, исчезла БД, кто-то дописал
|
||||
# skipif «пока починю») роняет прогон, пока его не внесут в список осознанно.
|
||||
#
|
||||
# Список — НАДмножество: покрывает и CI (где есть Postgres и native-libs), и
|
||||
# ноутбук разработчика (где их нет). Запись, чей пропуск не случился, — не
|
||||
# ошибка; ошибка — пропуск без записи.
|
||||
#
|
||||
# Сюда же попадает xfail: pytest рапортует его как skipped, поэтому xfail без
|
||||
# `strict=True` (зеленеет и когда тест внезапно ПРОШЁЛ) тоже придётся объявить.
|
||||
#
|
||||
# Чего хук НЕ видит: `--deselect` / `-k` / `-m` — они убирают тест до прогона,
|
||||
# отчёта о пропуске не возникает вовсе. Против них работает правило «никаких
|
||||
# deselect'ов в workflow» (см. .forgejo/workflows/ci-tradein.yml).
|
||||
|
||||
_SKIP_ALLOWLIST_PATH = Path(__file__).parent / "skip_allowlist.txt"
|
||||
_observed_skips: set[str] = set()
|
||||
|
||||
|
||||
def _allowed_skips() -> set[str]:
|
||||
if not _SKIP_ALLOWLIST_PATH.exists():
|
||||
return set()
|
||||
lines = _SKIP_ALLOWLIST_PATH.read_text(encoding="utf-8").splitlines()
|
||||
return {stripped for line in lines if (stripped := line.split("#", 1)[0].strip())}
|
||||
|
||||
|
||||
def _record_skip(report) -> None:
|
||||
if report.skipped:
|
||||
# nodeid без «[параметров]»: список фиксирует, какая ПРОВЕРКА может
|
||||
# отсутствовать, а не каждую её параметризацию.
|
||||
_observed_skips.add(report.nodeid.split("[", 1)[0])
|
||||
|
||||
|
||||
def pytest_runtest_logreport(report) -> None:
|
||||
_record_skip(report)
|
||||
|
||||
|
||||
def pytest_collectreport(report) -> None:
|
||||
# Модульный pytest.skip(allow_module_level=True) приходит сюда, а не в
|
||||
# logreport, и nodeid у него — путь файла.
|
||||
_record_skip(report)
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus) -> None:
|
||||
unlisted = sorted(_observed_skips - _allowed_skips())
|
||||
if not unlisted:
|
||||
return
|
||||
print(
|
||||
f"\nНЕУЧТЁННЫЙ ПРОПУСК ({len(unlisted)}): проверка не исполнилась и не "
|
||||
f"объявлена в {_SKIP_ALLOWLIST_PATH.name}:"
|
||||
)
|
||||
for nodeid in unlisted:
|
||||
print(f" - {nodeid}")
|
||||
print(
|
||||
"Почини тест либо внеси его в skip_allowlist.txt с причиной — "
|
||||
"пропуск без записи неотличим от пройденной проверки."
|
||||
)
|
||||
if exitstatus == 0:
|
||||
session.exitstatus = 1
|
||||
|
|
|
|||
39
tradein-mvp/backend/tests/skip_allowlist.txt
Normal file
39
tradein-mvp/backend/tests/skip_allowlist.txt
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Объявленные пропуски сьюта tradein-mvp/backend.
|
||||
#
|
||||
# Пропуск, которого здесь нет, роняет прогон (хук в tests/conftest.py). Смысл: не
|
||||
# запретить пропуски, а запретить НЕЗАМЕТНЫЕ. Проверка, которая тихо перестала
|
||||
# исполняться, со временем расходится с кодом, и узнают об этом ровно тогда,
|
||||
# когда на неё понадобилось опереться (#2722, #2729, #2740).
|
||||
#
|
||||
# Формат: один nodeid в строке, без «[параметров]»; `#` — комментарий.
|
||||
# Прежде чем добавить строку, ответь: почему эту проверку нельзя выполнить ЗДЕСЬ
|
||||
# и где она выполняется вместо этого. Нет ответа — это не пропуск, а дыра.
|
||||
#
|
||||
# Список — НАДмножество сред: в CI Postgres есть (ci-tradein.yml поднимает
|
||||
# сервис-контейнер и собирает схему из backend/data/sql/), на ноутбуке без БД —
|
||||
# нет. Запись, чей пропуск не случился, безвредна; пропуск без записи — нет.
|
||||
|
||||
# ── Реальный рендер PDF: нужны native-либы WeasyPrint (Pango/cairo/GObject) ────
|
||||
# Где выполняется: внутри prod-образа — runner-stage tradein-mvp/backend/Dockerfile
|
||||
# ставит libcairo2 + libpango-1.0-0 + libpangoft2-1.0-0:
|
||||
# docker exec tradein-backend python -m pytest -q -m pdf_render tests/test_pdf_real_render.py
|
||||
# На голом ubuntu-latest в ci-tradein.yml этих либ нет (apt-шага в job'е нет).
|
||||
tests/test_pdf_real_render.py
|
||||
|
||||
# ── Тесты против живой Postgres ───────────────────────────────────────────────
|
||||
# В CI они ИДУТ (postgres-сервис в ci-tradein.yml, #2745) — записи ниже нужны
|
||||
# только для прогона на машине без БД. Именно эта группа разъехалась со схемой,
|
||||
# пока не бежала нигде: #2740 (houses.url стал NOT NULL) и #2744
|
||||
# (test_gar_flats_loader падал до первого утверждения). Поднять локально:
|
||||
# docker run -d -p 5432:5432 -e POSTGRES_DB=tradein -e POSTGRES_USER=tradein \
|
||||
# -e POSTGRES_PASSWORD=tradein postgis/postgis:16-3.4
|
||||
# psql ... -f backend/data/sql/*.sql # порядок как в deploy-tradein.yml
|
||||
tests/tasks/test_cadastral_geo_match.py::test_real_knn_nearest_within_threshold_picked
|
||||
tests/test_audit_api.py::test_real_accounts_and_analytics_aggregate_inserted_rows
|
||||
tests/test_gar_flats_loader.py::test_upsert_and_canon_match_populates_gar_flat_count
|
||||
tests/test_house_dedup_merge.py::test_real_canon_clusterkey_and_geo_guard_merge_semantics
|
||||
tests/test_house_dedup_merge.py::test_real_fias_pass_cross_guard_and_identity_carryover
|
||||
tests/test_house_dedup_merge.py::test_real_fias_pass_ignores_geo_guard
|
||||
tests/test_house_dedup_merge.py::test_real_merge_is_reversible_via_journal
|
||||
tests/test_house_dedup_merge.py::test_real_merge_repoints_dedups_deletes_and_is_idempotent
|
||||
tests/test_user_events.py::test_real_record_event_inserts_row
|
||||
Loading…
Add table
Reference in a new issue