gendesign/backend/tests/test_layout_tz_pdf.py
Light1YT 6a554b559e
Some checks failed
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Successful in 1m21s
CI / openapi-codegen-check (pull_request) Failing after 2m6s
CI / backend-tests (pull_request) Successful in 14m35s
fix(best-layouts): знаменатель покрытия в комплексах, не в сырых obj_id (#2177)
domrf_kn_objects дробит один ЖК на несколько obj_id (очереди, дубли
snapshot-строк, безымянные корпуса) — §4.2 показывала «покрытие 17%
(2 из 12 ЖК)» вместо честного счёта комплексов.

- group_radius_objects: именованные группируются по (застройщик, ядро
  имени) — нормализация ёлочек/«N очередь»/generic-префиксов/повторов
  токенов («Квартал Депо» сохраняется); безымянные — single-linkage
  гео-кластеры ≤300 м per застройщик (кластер в 600 м от именованного —
  отдельная честная группа)
- coverage по группам: группа с данными = хоть один её obj_id с velocity;
  прод-кейс 66:41:0204016:10: 12 obj_id → 6 комплексов → 33% вместо 17%
- raw_objects_total в LayoutDataQuality для прозрачности; подпись
  «N из M ЖК» → «N из M комплексов» (бейдж + PDF)

Шаг 1 из 3 issue #2177 (mapping-расширение и lots-fallback — следом).
Follow-up: npm run codegen после деплоя (нужен живой OpenAPI).
2026-07-02 23:59:00 +05:00

160 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests для layout_tz_pdf renderer (Issue #113 PR D).
WeasyPrint requires native GTK/Pango/GObject shared libraries. These are present
in the Docker container (Linux) but absent on Windows dev machines. All tests in
this module are skipped automatically when the native libs are unavailable.
"""
import datetime as dt
import pytest
def _weasyprint_native_libs_available() -> tuple[bool, str]:
"""Probe whether WeasyPrint can actually render (native GTK/Pango/GObject libs).
WeasyPrint is lazy-imported inside render_layout_tz_pdf, so a plain module
import no longer surfaces the missing-native-lib OSError. The native libs
(libgobject/libpango) are present in the Docker/CI image (Linux) but absent
on macOS/Windows dev machines — the OSError fires only at write_pdf() time.
We probe by rendering a trivial document; failure ⇒ skip on dev, but the
suite still runs the PDF path on CI where the libs exist.
"""
try:
from weasyprint import HTML
HTML(string="<p>x</p>").write_pdf()
except (OSError, ImportError) as e:
return False, str(e)
return True, ""
_WP_OK, _WP_ERR = _weasyprint_native_libs_available()
if not _WP_OK:
pytest.skip(f"WeasyPrint native libs missing: {_WP_ERR}", allow_module_level=True)
# Импорты app-модулей ПОСЛЕ module-level skip guard выше — иначе на macOS без
# native libs импорт схемы не нужен (модуль skip'ается). E402 здесь намеренный.
from app.schemas.parcel import ( # noqa: E402
BestLayoutsResponse,
LayoutDataQuality,
LayoutTzMixRow,
LayoutTzRecommendation,
TopLayoutRow,
)
from app.services.exporters.layout_tz_pdf import render_layout_tz_pdf # noqa: E402
def _sample_response() -> BestLayoutsResponse:
return BestLayoutsResponse(
top_layouts=[
TopLayoutRow(
rank=1,
room_bucket="1",
area_bin="25-40",
signature="1__25-40",
competitor_obj_ids=[1234, 5678],
competitor_count=2,
total_sold_in_window=67,
velocity_per_month=8.4,
avg_price_per_m2_rub=148000.0,
avg_area_m2=38.5,
supply_units_in_radius=312,
sold_pct_of_supply=21.5,
is_oversold=False,
),
TopLayoutRow(
rank=2,
room_bucket="studio",
area_bin="<25",
signature="studio__<25",
competitor_obj_ids=[1234],
competitor_count=1,
total_sold_in_window=40,
velocity_per_month=5.0,
avg_price_per_m2_rub=160000.0,
avg_area_m2=22.0,
supply_units_in_radius=100,
sold_pct_of_supply=40.0,
is_oversold=False,
),
],
recommendation_for_tz=LayoutTzRecommendation(
rationale_text="Test rationale текст с кириллицей",
mix=[
LayoutTzMixRow(room_bucket="studio", pct=10, abs_units=30, avg_target_area_m2=22.0),
LayoutTzMixRow(room_bucket="1", pct=60, abs_units=180, avg_target_area_m2=38.5),
LayoutTzMixRow(room_bucket="2", pct=30, abs_units=90, avg_target_area_m2=55.0),
],
weighted_avg_price_per_m2_rub=152000.0,
based_on_obj_count=5,
based_on_total_deals=107,
data_window_start=dt.date(2026, 2, 1),
data_window_end=dt.date(2026, 5, 1),
),
data_quality=LayoutDataQuality(
objects_with_velocity_data=5,
objects_total_in_radius=8,
raw_objects_total=14,
velocity_coverage_pct=62.5,
confidence="medium",
),
)
def test_pdf_renders_non_empty_bytes() -> None:
pdf = render_layout_tz_pdf(
_sample_response(),
cad_num="66:41:0204016:10",
radius_km=1.0,
time_window="last_quarter",
)
assert len(pdf) > 1000 # PDF минимум ~1KB
def test_pdf_starts_with_pdf_magic() -> None:
pdf = render_layout_tz_pdf(
_sample_response(),
cad_num="66:41:0204016:10",
radius_km=1.0,
time_window="last_quarter",
)
assert pdf[:4] == b"%PDF"
def test_pdf_renders_cyrillic_correctly() -> None:
"""Smoke — WeasyPrint должен handle кириллический rationale_text без UnicodeEncodeError."""
response = _sample_response()
pdf = render_layout_tz_pdf(
response,
cad_num="66:41:0303161:42",
radius_km=1.5,
time_window="last_year",
)
# Embedded text может быть compressed, но без exception = OK
assert len(pdf) > 1000
def test_pdf_handles_empty_top_layouts() -> None:
response = _sample_response()
response.top_layouts = []
pdf = render_layout_tz_pdf(
response,
cad_num="66:41:0204016:10",
radius_km=1.0,
time_window="last_quarter",
)
assert pdf[:4] == b"%PDF"
def test_pdf_handles_null_avg_price() -> None:
"""avg_price_per_m2_rub=None (ЖК не покрыт Objective) → должно рендериться как ''."""
response = _sample_response()
response.top_layouts[0].avg_price_per_m2_rub = None
pdf = render_layout_tz_pdf(
response,
cad_num="66:41:0204016:10",
radius_km=1.0,
time_window="last_quarter",
)
assert pdf[:4] == b"%PDF"