diff --git a/.forgejo/workflows/ci-tradein.yml b/.forgejo/workflows/ci-tradein.yml index c3f6bcda..e9d38c2b 100644 --- a/.forgejo/workflows/ci-tradein.yml +++ b/.forgejo/workflows/ci-tradein.yml @@ -66,16 +66,76 @@ 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с). + services: + postgres: + # postgis, не plain postgres: tests/tasks/test_cadastral_geo_match.py + # проверяет KNN по geometry (PostGIS_Version() в connectivity-probe). + image: postgis/postgis:16-3.4 + env: + # CI-локальная одноразовая БД, не секрет (тот же паттерн, что в + # .github/workflows/ci.yml). Имя БД ОБЯЗАНО отличаться от `test`: + # `_live_session()` считает DSN с `localhost:5432/test` заглушкой и + # возвращает None — с ним сервис поднялся бы, а тесты всё равно скипались. + POSTGRES_DB: tradein + POSTGRES_USER: tradein + POSTGRES_PASSWORD: tradein + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U tradein" + --health-interval 5s + --health-timeout 5s + --health-retries 10 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 + DATABASE_URL: postgresql+psycopg://tradein:tradein@localhost:5432/tradein steps: - uses: actions/checkout@v4 + - name: Bootstrap схемы tradein (backend/data/sql/*.sql) + working-directory: ./tradein-mvp + env: + PGPASSWORD: tradein + # Тот же порядок и тот же строгий режим, что в deploy-tradein.yml: + # `ls | sort` + ON_ERROR_STOP=on, падение любой миграции → job RED. + # Никаких «применилось как получилось»: схема в CI либо та же, что на + # проде, либо гейта нет. + run: | + sudo apt-get update + sudo apt-get install -y postgresql-client + psql -h localhost -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 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 + psql -h localhost -U tradein -d tradein -v ON_ERROR_STOP=on -q -f "$sql_file" \ + || { echo "::error::миграция $fname не применилась"; exit 1; } + done + echo "✓ схема собрана: $(psql -h localhost -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 +181,12 @@ 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 # Тесты браузерного сайдкара (#2722). До этого job'а они не бежали НИГДЕ: # ci-tradein гейтил только backend/frontend, deploy-tradein — тоже, а каталог @@ -169,7 +234,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 diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index a261bc30..008a9015 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -69,6 +69,37 @@ 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`). + services: + postgres: + image: postgres:16 + env: + # CI-локальная одноразовая БД, не секрет (тот же паттерн, что в + # .github/workflows/ci.yml). Имя БД ≠ `test`, чтобы DSN не выглядел + # заглушкой для probe-ов в тестах. + POSTGRES_DB: gendesign_ci + POSTGRES_USER: gendesign + POSTGRES_PASSWORD: gendesign + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U gendesign" + --health-interval 5s + --health-timeout 5s + --health-retries 10 defaults: run: working-directory: backend @@ -76,10 +107,7 @@ 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 + DATABASE_URL: postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign_ci REDIS_URL: redis://localhost:6379/0 steps: - uses: actions/checkout@v4 @@ -136,10 +164,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 +179,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 \ diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 4a5fb6f2..a3266cb6 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -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 diff --git a/backend/tests/skip_allowlist.txt b/backend/tests/skip_allowlist.txt new file mode 100644 index 00000000..6fda8d13 --- /dev/null +++ b/backend/tests/skip_allowlist.txt @@ -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 diff --git a/tradein-mvp/backend/tests/conftest.py b/tradein-mvp/backend/tests/conftest.py index f9d02c57..9651c161 100644 --- a/tradein-mvp/backend/tests/conftest.py +++ b/tradein-mvp/backend/tests/conftest.py @@ -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 diff --git a/tradein-mvp/backend/tests/skip_allowlist.txt b/tradein-mvp/backend/tests/skip_allowlist.txt new file mode 100644 index 00000000..5c0b9a20 --- /dev/null +++ b/tradein-mvp/backend/tests/skip_allowlist.txt @@ -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