diff --git a/backend/tests/services/scrapers/test_ekb_krt_registry.py b/backend/tests/services/scrapers/test_ekb_krt_registry.py index 2463fc8b..9972e217 100644 --- a/backend/tests/services/scrapers/test_ekb_krt_registry.py +++ b/backend/tests/services/scrapers/test_ekb_krt_registry.py @@ -197,32 +197,74 @@ class TestParseBabushkinaBlock: assert site.krt_kind == "жилой застройки" -class TestMultiSiteCountFromDump: - """Проверка количества площадок из реального дампа страницы.""" +# ── Многоблочная страница: два года, три площадки, повторяющийся номер ──────── +# +# Здесь до #2778 стоял TestMultiSiteCountFromDump, читавший +# `.playwright-mcp/ekb-docs/krt_page_raw.html`. Дампа нет ни в репозитории, ни у +# кого-либо на диске, ни на проде: `ekb_krt_sites.raw_block` хранит не HTML, а +# уже очищенный текст (`block_text[:4000]` в ekb_krt_registry.py:338), поэтому +# восстановить вход парсера из БД нельзя. Оба теста не исполнялись НИГДЕ. +# +# Свойство, ради которого они писались, от дампа не зависит и выражается +# страницей из блоков, которые уже лежат в этом файле: +# • несколько

-секций с годами разбираются в одном проходе; +# • нумерация площадок на реальной странице НАЧИНАЕТСЯ ЗАНОВО В КАЖДОМ ГОДУ, +# поэтому «2.» встречается дважды — и site_name всё равно обязан быть +# уникальным (номер сам по себе ключом не является); +# • блоки лежат внутри контейнера
— это ветка `_extract_krt_section` +# через `find_parent`, которой одиночные блоки выше не касаются вовсе. +# +# ЧЕГО ЭТА ЗАМЕНА НЕ ПРОВЕРЯЕТ (осознанная потеря, зафиксирована в #2778): +# «на живой странице ЕКБ ≥14 площадок». Это утверждение о полноте разбора +# реального документа, синтетической страницей оно не заменяется. На проде +# ekb_krt_sites = 15 строк, то есть по факту сейчас всё разбирается, но +# автоматической проверки этого больше нет — она вернётся, когда в репозитории +# появится образец страницы. +_MULTISITE_PAGE = ( + "
" + + _BRUSNIKA_BLOCK + + _BABUSHKINA_BLOCK + + textwrap.dedent( + """\ +

 2. КРТ нежилой застройки в районе ул. Краснофлотцев:

+

Площадь территории: 3,65 га;
\ +Градостроительный потенциал: 117 640 кв. м;
\ +Всего многоквартирных домов: 13.

+ """ + ) + + "
" +) + + +class TestMultiSitePage: + """Разбор страницы из нескольких блоков — без локального дампа.""" @pytest.fixture(scope="class") - def dump_html(self): - """Загружает локальный дамп страницы. + def sites(self): + return parse_krt_page(_MULTISITE_PAGE) - Тест пропускается если файл отсутствует (CI без дампа). - """ - import os + def test_all_three_sites_parsed(self, sites): + """Три блока в двух годовых секциях → три площадки.""" + assert len(sites) == 3, f"Ожидалось 3 площадки, получено {len(sites)}: {sites}" - dump_path = os.path.join( - os.path.dirname(__file__), - "../../../../.playwright-mcp/ekb-docs/krt_page_raw.html", - ) - if not os.path.exists(dump_path): - pytest.skip("krt_page_raw.html dump not found — skip real-dump test") - with open(dump_path, encoding="utf-8") as fh: - return fh.read() - - def test_site_count_from_dump(self, dump_html): - """Из дампа должно распарситься ≥ 14 площадок (реальных на странице 15).""" - sites = parse_krt_page(dump_html) - assert len(sites) >= 14, f"Expected ≥ 14 sites, got {len(sites)}" - - def test_no_duplicate_site_names(self, dump_html): - sites = parse_krt_page(dump_html) + def test_no_duplicate_site_names(self, sites): + """Номер «2.» встречается дважды (2022 и 2024) — имена всё равно уникальны.""" names = [s.site_name for s in sites] - assert len(names) == len(set(names)), "Дубликаты site_name в результате" + assert sum(n.startswith("2.") for n in names) == 2, names + assert len(names) == len(set(names)), f"Дубликаты site_name: {names}" + + def test_blocks_do_not_leak_into_each_other(self, sites): + """Границы блоков соблюдены: договор и застройщик остались у своей площадки. + + Регресс, который этим ловится: если block_end съезжает, «Брусника» и + договор 1-2/21.7-14 из первого блока прилипают ко второму и третьему. + """ + with_developer = [s for s in sites if s.developer_name] + assert len(with_developer) == 1, [s.developer_name for s in sites] + assert "Брусника" in with_developer[0].developer_name + assert [s.contract_number for s in sites].count("1-2/21.7-14") == 1 + + def test_kinds_are_read_per_block(self, sites): + """Третий блок — «нежилой застройки», не должен унаследовать вид соседа.""" + kinds = sorted(s.krt_kind for s in sites if s.krt_kind) + assert kinds == ["жилой застройки", "жилой застройки", "нежилой застройки"], kinds diff --git a/backend/tests/skip_allowlist.txt b/backend/tests/skip_allowlist.txt index 6fda8d13..02ca21bc 100644 --- a/backend/tests/skip_allowlist.txt +++ b/backend/tests/skip_allowlist.txt @@ -73,14 +73,22 @@ 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 +# ── Образец, которого нет в git ─────────────────────────────────────────────── +# ДОЛГ, а не норма. Было 8 записей; 2 из них (test_ekb_krt_registry.py:: +# TestMultiSiteCountFromDump) сняты в #2778: дампа `krt_page_raw.html` не +# оказалось НИГДЕ — ни в репозитории, ни на дисках, ни на проде (в БД лежит +# `raw_block` = уже очищенный текст, вход парсера из него не восстановить), — +# зато свойство, которое они проверяли (несколько блоков на странице, уникальные +# site_name), выразимо без дампа и теперь проверяется на многоблочной странице, +# собранной из блоков, уже лежащих в том же тест-файле. +# +# Оставшиеся 6 так не лечатся: они проверяют извлечение таблиц из НАСТОЯЩЕГО PDF +# через pdfplumber, соседний класс TestParsePptTep уже мокает pdfplumber целиком. +# Нужен файл `ppt2018_22823_poyasnit_PPT.pdf` (или рабочий URL на ГИСОГД ЕКБ) — +# запрошен у Anton в #2778, там же список того, что оживёт. Причин не коммитить +# нет: официальный градостроительный документ, без персональных данных. +# После получения — положить в tests/fixtures/, поправить `_REAL_PDF_PATH`, +# убрать эти шесть строк. 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