"""Stage 1c tests — DXF and PDF exporters. DXF is asserted by a binary round-trip (re-read with ezdxf, check layers/entities). The PDF render is skipped when WeasyPrint's native libraries are unavailable (dev boxes без libgobject); the HTML-build step is always exercised. """ from __future__ import annotations import importlib.util import os import tempfile from collections import Counter import ezdxf import pytest from app.schemas.concept import ConceptInput from app.services.generative import geometry from app.services.generative.exporters import dxf, pdf _PARCEL_COORDS = [ [60.60, 56.830], [60.6045, 56.830], [60.6045, 56.8328], [60.60, 56.8328], [60.60, 56.830], ] def _payload() -> ConceptInput: return ConceptInput( parcel_geojson={"type": "Polygon", "coordinates": [_PARCEL_COORDS]}, housing_class="comfort", target_floors=9, development_type="mid_rise", land_cost_rub=150_000_000.0, ) def _weasyprint_available() -> bool: """WeasyPrint импортируется только с нативными библиотеками (libgobject и т.д.).""" if importlib.util.find_spec("weasyprint") is None: return False try: import weasyprint # noqa: F401 except OSError: return False return True def test_dxf_export_round_trips_with_layers() -> None: payload = _payload() parcel = geometry.parse_parcel(payload) variant = geometry.generate(payload)[0] data = dxf.export_concept_dxf(parcel, variant) assert data.startswith(b"AutoCAD Binary DXF") with tempfile.NamedTemporaryFile(suffix=".dxf", delete=False) as fh: fh.write(data) path = fh.name try: doc = ezdxf.readfile(path) finally: os.unlink(path) msp = doc.modelspace() by_layer = Counter(e.dxf.layer for e in msp) # Участок и пятно застройки нарисованы. assert by_layer["PARCEL"] == 1 assert by_layer["BUILDABLE"] == 1 # Секции нарисованы (по одному polyline на секцию + подписи). n_sections = len(variant.buildings_geojson["features"]) assert n_sections > 0 assert by_layer["BUILDINGS"] >= n_sections def test_dxf_building_footprints_have_metric_area() -> None: from shapely.geometry import Polygon payload = _payload() parcel = geometry.parse_parcel(payload) variant = geometry.generate(payload)[0] data = dxf.export_concept_dxf(parcel, variant) with tempfile.NamedTemporaryFile(suffix=".dxf", delete=False) as fh: fh.write(data) path = fh.name try: doc = ezdxf.readfile(path) finally: os.unlink(path) areas = [] for e in doc.modelspace(): if e.dxftype() == "LWPOLYLINE" and e.dxf.layer == "BUILDINGS": pts = [(p[0], p[1]) for p in e.get_points()] areas.append(Polygon(pts).area) assert areas, "no building polylines found" # Площади секций — десятки/сотни кв.м (метры), не доли (градусы). for area in areas: assert 50.0 < area < 2000.0 def test_pdf_html_build_contains_tables() -> None: variants = geometry.generate(_payload()) html = pdf._build_html(variants) assert "Технико-экономические показатели" in html assert "Финансовая модель" in html # PR-3: DCF-строки заменили IRR-proxy в таблице финмодели. assert "NPV (DCF), млн руб" in html assert "IRR (DCF, годовой)" in html assert "Окупаемость (PBP)" in html def test_pdf_html_build_graceful_on_empty() -> None: html = pdf._build_html([]) assert "нет вариантов" in html def test_pdf_caveat_does_not_contradict_office_revenue_row() -> None: """Эпик #2445 C1: каскад включает нежилую выручку/НДС — caveat не должен это отрицать. Payload — ``comfort`` (office_share=0.05 в teap.py), поэтому строка "Выручка — нежилое (1-й этаж)" в таблице ненулевая: caveat ниже НЕ может утверждать, что коммерция/офисы "не учитываются" или что НДС — "только машиноместа". """ variants = geometry.generate(_payload()) html = pdf._build_html(variants) assert "Выручка — нежилое (1-й этаж), млн руб" in html assert "НДС (нежилое: паркинг + коммерция), млн руб" in html # Старые (стейл) формулировки, отрицающие модель нежилого, — не должны вернуться. assert "не учитываются" not in html assert "только на нежилую часть" not in html assert "только машиноместа" not in html # Caveat честно описывает нежилое как учтённое (не «нет в ТЭП»). assert "нет в ТЭП" not in html @pytest.mark.skipif( not _weasyprint_available(), reason="WeasyPrint native libs unavailable on this host", ) def test_pdf_export_produces_pdf_bytes() -> None: variants = geometry.generate(_payload()) data = pdf.export_concept_pdf(variants) assert data.startswith(b"%PDF-") assert len(data) > 1000