"""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 assert "IRR-proxy" in html def test_pdf_html_build_graceful_on_empty() -> None: html = pdf._build_html([]) assert "нет вариантов" 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