"""Smoke-proof для parity-harness'а (tests/support/parity.py) — issue #2304. Доказывает, что harness реально ловит эквивалентность legacy ↔ scraper_kit на РЕАЛЬНОЙ паре функций, а не на синтетике: `parse_detail_html` — parsing-ядро, которое `app/api/v1/admin.py::scrape_avito_detail` (debug-роут ручного запуска парсера) вызывает косвенно через `fetch_detail()` (fetch_detail = HTTP GET + `parse_detail_html(html, url)`; сам parse — чистая функция, без сети/БД, что и позволяет сравнить её offline на фиксированной HTML-фикстуре). `app/services/scrapers/avito_detail.py::parse_detail_html` (legacy) и `scraper_kit/providers/avito/detail.py::parse_detail_html` (kit) на момент написания теста ИДЕНТИЧНЫ по телу функции (см. audit `Scraper_Kit_Legacy_Dependency_Audit_0703` в vault) — различаются только import-пути модулей, из которых берутся зависимости (`app.services.scrapers.*` vs `scraper_kit.*`) и Strangler-инжекция `ScraperConfig` в соседних функциях (`fetch_detail`, `_build_detail_session`), которую сам `parse_detail_html` не использует. Это НЕ миграция admin.py (issue #2305+) — только доказательство инструмента. HTML-фикстура — тот же minimal-marker набор, что и в `tests/test_avito_detail_parse.py::test_minimal_parse` (независимая копия литерала, чтобы этот тест не зависел от другого test-модуля). """ # ruff: noqa: E501 # HTML-фикстура содержит длинные строки разметки from __future__ import annotations import dataclasses import os import pytest # app.services.scrapers.avito_detail импортирует app.core.config.settings=Settings(), # которому нужен DATABASE_URL. Офлайн-парсинг БД не трогает — фиктивный DSN достаточен # (mirror tests/test_scraper_kit_avito_golden_parity.py — тест должен жить и без # CI-уровневого DATABASE_URL из ci-tradein.yml/deploy-tradein.yml). os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db") from scraper_kit.providers.avito.detail import ( parse_detail_html as kit_parse_detail_html, ) from app.services.scrapers.avito_detail import ( parse_detail_html as legacy_parse_detail_html, ) from tests.support.parity import ParityMismatchError, assert_parity _DETAIL_HTML = """
№ 7986882804 · 18 мая в 13:40 · 1899 просмотров (+ 12 сегодня)
11 990 000 ₽
Дом

1 собственник

Последняя смена собственника 5 мая 2020

Не найдены ограничения и обременения

Совпадают площадь, адрес и этаж

Отличная квартира в новом ЖК. Чкаловская 11-15 мин пешком.
""" _SOURCE_URL = "https://www.avito.ru/ekaterinburg/kvartiry/test_7986882804" def test_legacy_and_kit_parse_detail_html_are_class_distinct() -> None: """Sanity-check ДО harness'а: подтверждает, зачем harness вообще нужен. legacy DetailEnrichment и kit DetailEnrichment — РАЗНЫЕ классы (разные модули) → обычный `==` даёт False, хотя данные идентичны. Именно это harness должен обходить через структурное сравнение (dataclasses.fields). """ legacy_result = legacy_parse_detail_html(_DETAIL_HTML, _SOURCE_URL) kit_result = kit_parse_detail_html(_DETAIL_HTML, _SOURCE_URL) assert type(legacy_result) is not type(kit_result) assert legacy_result != kit_result # dataclass __eq__ проверяет class identity первым assert dataclasses.asdict(legacy_result) == dataclasses.asdict(kit_result) def test_parity_harness_confirms_avito_detail_parse_equivalence() -> None: """Реальное proof-of-concept использование harness'а (issues #2305-#2310 template).""" assert_parity( legacy_fn=legacy_parse_detail_html, kit_fn=kit_parse_detail_html, fixtures=[(_DETAIL_HTML, _SOURCE_URL)], ) def test_parity_harness_catches_real_field_divergence() -> None: """Доказывает, что harness реально ЛОВИТ расхождение на этой же реальной 30+-полевой dataclass-форме — а не только сообщает "нет разницы" на двух genuinely-identical инстансах, как предыдущий тест. Мутируем один field (``price_rub``) в kit-результате через ``dataclasses.replace`` и убеждаемся, что harness падает с ``ParityMismatchError``, называющим именно этот field. """ legacy_result = legacy_parse_detail_html(_DETAIL_HTML, _SOURCE_URL) kit_result = kit_parse_detail_html(_DETAIL_HTML, _SOURCE_URL) assert kit_result.price_rub is not None # sanity: поле реально распарсилось из фикстуры mutated_kit_result = dataclasses.replace(kit_result, price_rub=kit_result.price_rub + 1) with pytest.raises(ParityMismatchError) as exc_info: assert_parity( legacy_fn=lambda: legacy_result, kit_fn=lambda: mutated_kit_result, fixtures=[()], ) assert "price_rub" in str(exc_info.value)