"""Тесты числового экстракта предельных параметров ПЗЗ (#1067 C8b). Строки limit_params — РЕАЛЬНЫЕ otherDescription с живого ЕКБ-геопортала urbanCard (probe 2026-06-06: зоны Ц-1, ЦС-1, ЦС-6). Проверяют что regex берёт числа из «– NUN кв.м/%/этаж», игнорит «не подлежат установлению», тянет per-ВРИ повторы и десятичную запятую. """ from __future__ import annotations from app.services.scrapers.ekb_geoportal_client import ZoneRegulation from app.services.site_finder.zone_regulation import ( _headline_scalars, parse_limit_params, upsert_zone_regulation, ) # Реальный otherDescription зоны Ц-1 (среднеэтажная 1500 / многоэтажная 3000 + прочее). _C1 = ( "минимальная площадь земельного участка - 1500 кв. м;\n" "минимальные отступы от границ земельных участков ... не подлежат установлению;\n" "предельное количество этажей или предельная высота зданий, строений, сооружений не " "подлежат установлению;\n" "максимальный процент застройки в границах земельного участка, определяемый как отношение " "суммарной площади земельного участка, которая может быть застроена, ко всей площади " "земельного участка – 100%; Иные предельные параметры:\n" "коэффициент строительного использования земельного участка – 4;\n" "процент застройки подземного пространства – 100%;\n" "минимальная площадь земельного участка - 3000 кв. м;\n" "максимальный процент застройки в границах земельного участка ... для многоэтажной жилой " "застройки – 100%;\n" ) # Зона ЦС-1: КСИ 2,4 / 2,5 / 2 (десятичная запятая), этажность 5, застройка 80%, площадь 5000. _CS1 = ( "максимальный процент застройки в границах земельного участка, определяемый как отношение " "суммарной площади земельного участка ... ко всей площади земельного участка – 80%;\n" "коэффициент строительного использования земельного участка (за исключением ...) – 2,4;\n" "коэффициент строительного использования земельного участка (только в отношении ...) - 2,5;\n" "коэффициент строительного использования земельного участка (только в отношении ...) – 2;\n" 'предельная этажность (только в отношении ... "гостиничное обслуживание") – 5 этажей;\n' "максимальная общая площадь ... – 5 000 кв.м;\n" ) # Зона ЦС-6: всё «не подлежат установлению» — числовой экстракт пуст. _CS6 = ( "предельные (минимальные и (или) максимальные) размеры земельных участков, в том числе их " "площадь, не подлежат установлению;\n" "максимальный процент застройки в границах земельного участка ... не подлежит установлению;\n" "Иные предельные параметры разрешенного строительства ... не подлежат установлению\n" ) def test_c1_min_area_per_vri() -> None: """Обе min-площади (среднеэтажная 1500 + многоэтажная 3000) тянутся как список.""" p = parse_limit_params([_C1]) assert p["min_parcel_area_m2"] == [1500.0, 3000.0] def test_c1_far_and_building_and_underground() -> None: p = parse_limit_params([_C1]) assert p["max_far"] == [4.0] assert p["max_building_pct"] == [100.0, 100.0] # per-ВРИ повтор assert p["underground_pct"] == [100.0] def test_c1_no_floors_or_height_when_not_established() -> None: """«не подлежат установлению» для этажей/высоты → ключи отсутствуют.""" p = parse_limit_params([_C1]) assert "max_floors" not in p assert "max_height_m" not in p def test_cs1_decimal_comma_far() -> None: """КСИ с десятичной запятой (2,4 / 2,5 / 2) парсятся в float.""" p = parse_limit_params([_CS1]) assert p["max_far"] == [2.4, 2.5, 2.0] def test_cs1_floors_and_thousand_separated_area() -> None: p = parse_limit_params([_CS1]) assert p["max_floors"] == [5.0] assert p["max_building_pct"] == [80.0] def test_cs6_all_not_established_is_empty() -> None: assert parse_limit_params([_CS6]) == {} def test_empty_inputs() -> None: assert parse_limit_params(None) == {} assert parse_limit_params([]) == {} assert parse_limit_params(["", None]) == {} # type: ignore[list-item] def test_headline_scalars_most_permissive() -> None: """min_parcel_area = наименьший минимум; прочие = max; max_floors = int.""" p = parse_limit_params([_C1, _CS1]) s = _headline_scalars(p) assert s["min_parcel_area_m2"] == 1500.0 # min из [1500, 3000] assert s["max_far"] == 4.0 # max из [4, 2.4, 2.5, 2] assert s["max_building_pct"] == 100.0 assert s["max_floors"] == 5 assert isinstance(s["max_floors"], int) assert s["max_height_m"] is None # нет ни одного → None def test_headline_scalars_empty() -> None: s = _headline_scalars({}) assert all(v is None for v in s.values()) assert set(s) == { "max_far", "max_building_pct", "underground_pct", "min_parcel_area_m2", "max_floors", "max_height_m", } def test_upsert_skips_when_no_zone_index() -> None: """Регламент без zone_index не пишется (None), БД не трогается.""" reg = ZoneRegulation( zone_index=None, zone_full_name=None, main_vri=[], conditional_vri=[], auxiliary_vri=[], limit_params=[], ) class _Boom: def begin_nested(self): # pragma: no cover - не должен вызываться raise AssertionError("db не должна трогаться при отсутствии zone_index") assert upsert_zone_regulation(_Boom(), reg) is None # type: ignore[arg-type]