gendesign/tradein-mvp/backend/tests/test_scraper_kit_yandex_golden_parity.py
bot-backend 575c233e51
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m2s
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
refactor(tradein/scrapers): удалить 6 осиротевших legacy scraper-модулей + тест-хирургия (#2397 Part E2)
app.services.scrapers.{cian,cian_detail,yandex_realty,domclick,domclick_exceptions,
avito_houses} имели 0 runtime-импортёров после E1 (scrape_pipeline.py удалён,
#2405) — admin.py/cian_history_backfill.py уже на scraper_kit.providers.*.

Удаление:
- 6 модулей целиком (avito_houses.py, cian.py, cian_detail.py, domclick.py,
  domclick_exceptions.py, yandex_realty.py)
- 3 осиротевших dev-sweep скрипта (local-sweep-ekb{,-cian,-yandex}.py) —
  эксклюзивно драйвили удалённые легаси-скраперы, kit-путь их заменяет

Тесты (18 файлов DELETE — эксклюзивно тестировали удалённые модули или были
чистым legacy-vs-kit parity, чей легаси-эталон исчез):
test_cian_detail.py, test_cian_serp_scraper.py, tests/scrapers/test_cian_exhaustive.py,
test_yandex_realty_serp.py, tests/scrapers/test_domclick.py,
test_avito_houses_dedup.py, test_avito_houses_parse.py,
test_houses_rating_filter_entries_1789.py, test_scraper_kit_domclick_golden_parity.py,
tests/scrapers/test_admin_{avito,cian,yandex}_kit_parity.py.

CONVERT (файл покрывает СМЕШАННЫЙ live+legacy набор — вырезаны только
секции удалённых модулей, live-покрытие сохранено):
- test_scraper_kit_cian_golden_parity.py — SERP/`_format_address`/detail
  (cian.py/cian_detail.py) убраны; extract_state (cian_state_parser, live)/
  valuation (cian_valuation, live)/newbuilding (cian_newbuilding, live)/
  no-app-imports guard — остались
- test_scraper_kit_yandex_golden_parity.py — SERP `_entity_to_lot`/
  `_parse_gate_json`/`_build_url` (yandex_realty) убраны; normalize_house_type
  (live)/newbuilding parse (yandex_newbuilding, live)/no-app-imports guard —
  остались
- test_scraper_kit_bisection_parity.py — cian/domclick/yandex-ветки убраны;
  avito-ветка (live, единственная оставшаяся легаси-бисекция) — осталась
  полностью, включая full-seed-grid
- test_scraper_kit_group_c_backfill_kit_parity.py — cian fetch_detail
  browser_fetcher parity-тест убран; avito/house_imv_backfill/
  cian_history_backfill/ekb_geoportal тесты — остались
- test_scraper_kit_pricehistory_session_parity.py — fetch_detail
  config-injection/proxy-wiring тесты (cian_detail) убраны; extract_state
  (cian_state_parser, live)/ScrapedLot (base, live) parity — остались
- test_yandex_scrapers_delay_wiring.py — yandex_realty delay-тест убран
  (kit-эквивалент уже покрыт test_kit_serp_proxy_pool.py); yandex_newbuilding
  (live)/yandex_detail/yandex_valuation (kit) тесты — остались
- test_scraper_proxy.py — yandex_realty BrowserFetcher-тест убран (redundant
  с test_kit_serp_proxy_pool.py::test_yandex_serp_no_pool_by_default)

RETARGET на kit (символ живёт, только источник поменялся):
- test_domclick_sweep.py — `_map_item` → scraper_kit.providers.domclick.serp
- test_yandex_city_sweep.py — DEFAULT_PRICE_RANGES/ROOM_PATH →
  scraper_kit.providers.yandex.serp
- test_pipeline_browser_routing.py — fetch_house_catalog →
  scraper_kit.providers.avito.houses (+ KitAvitoBlockedError для firewall-теста)
- test_snapshot_writer.py — DetailEnrichment/save_detail_enrichment →
  scraper_kit.providers.cian.detail (byte-identical strangler-копия)
- test_backfill_wave2.py — DetailEnrichment → scraper_kit.providers.cian.detail
- tests/scrapers/test_avito_unix_date_tz_consistency.py — avito_houses →
  scraper_kit.providers.avito.houses (houses-ветка _unix_to_date/RUS_MONTHS
  consolidation-проверки)

Stale-комментарии поправлены в cian_price_history.py, tests/scrapers/
test_domclick_detail.py (описывали ещё-живой domclick.py/domclick_exceptions.py).

grep -rn scrapers\.\(cian\|cian_detail\|yandex_realty\|domclick\|domclick_exceptions\|
avito_houses\) app/ scripts/ packages/ — только historical footnotes
("strangler-копия app.services.scrapers.X", past-tense "удалён/deleted").

Backend suite: 2811 passed, 6 skipped, 1 failed (known flake test_search_cache_hit
#2208, не регрессия). ruff check чист на всех изменённых файлах.
2026-07-04 14:37:19 +03:00

123 lines
5.8 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.

"""Golden-parity: `scraper_kit.providers.yandex.*` парсинг ≡ ещё живым
`app.services.scrapers.yandex_*`.
Strangler-инвариант (#2133): новая scraper_kit-копия yandex-провайдера должна давать
БАЙТ-ИДЕНТИЧНЫЙ результат парсинга старому боевому коду на одинаковом входе.
Развязка (settings→ScraperConfig, get_scraper_delay→delay_provider,
house_type_normalizer→providers.yandex.shared, base/browser_fetcher/helpers→scraper_kit.*)
НЕ меняет распарсенные данные.
`app.services.scrapers.yandex_realty` удалён (#2397 Part E2, 0 runtime-импортёров).
SERP `_entity_to_lot`/`_parse_gate_json`/`_build_url` parity-тесты (сравнение с
`YandexRealtyScraper`) удалены вместе с ним (легаси-эталон исчез).
Оставшееся сравнивает kit ≡ ЕЩЁ ЖИВЫЕ легаси-модули:
- `normalize_house_type` — `app.services.scrapers.house_type_normalizer` (живой);
- newbuilding `parse` (ЖК-лендинг → YandexNewbuildingInfo) —
`app.services.scrapers.yandex_newbuilding` (живой);
- strangler-guard: kit-провайдер не импортирует `app.*`.
Легаси `app.services.scrapers.yandex_detail` / `yandex_valuation` удалены (#2277
финальный шаг scraper_kit-миграции, 0 runtime importers) — detail/valuation
parity-проверки убраны отсюда ранее. Kit-side coverage для detail/valuation
`.parse()` сохранена без изменений в `tests/test_yandex_detail.py` /
`tests/test_yandex_detail_structural.py` / `tests/test_yandex_valuation.py` /
`tests/test_yandex_valuation_save.py` (переведены на kit-эквиваленты тем же PR).
Идентичность результата = kit-парсер верно скопирован (развязка не задела логику).
"""
from __future__ import annotations
import inspect
import os
# Старый app.services.scrapers.house_type_normalizer/yandex_newbuilding импортирует
# app.core.config.settings=Settings(), которому нужен DATABASE_URL. Офлайн-парсинг
# БД не трогает — фиктивный DSN достаточен.
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
# НОВЫЙ (scraper_kit) и СТАРЫЙ (app.services.scrapers) yandex-парсеры — оба в одном
# тесте; ruff сортирует scraper_kit (first-party) выше app-импортов.
from scraper_kit.providers.yandex.newbuilding import (
YandexNewbuildingScraper as NewNewbuildingScraper,
)
from scraper_kit.providers.yandex.shared import normalize_house_type as new_normalize_house_type
from app.services.scrapers.house_type_normalizer import (
normalize_house_type as old_normalize_house_type,
)
from app.services.scrapers.yandex_newbuilding import (
YandexNewbuildingScraper as OldNewbuildingScraper,
)
# ── normalize_house_type parity ──────────────────────────────────────────────
def test_normalize_house_type_parity() -> None:
"""SCREAMING / camelCase / канон / мусор → идентичный результат."""
raws = [
None,
"",
"MONOLIT",
"BRICK",
"PANEL",
"MONOLIT_BRICK",
"monolithBrick",
"gasSilicateBlock",
"monolith_brick",
"monolith",
"stalin",
"OTHER",
" MONOLIT ",
]
for raw in raws:
assert old_normalize_house_type(raw) == new_normalize_house_type(raw)
# ── newbuilding parse parity (синтетика) ─────────────────────────────────────
_NEWBUILDING_HTML = """
<html><body>
<h1>ЖК «Татлин»</h1>
Екатеринбург, ул. Черепанова 4.3 из 5 1505 оценок Смотреть все 353 отзыва
56.855312 60.576668
<h2>О комплексе</h2>
<div>Дом комфорт-класса, монолитный. Введён в эксплуатацию в июнь 2023.
35-этажные башни на участке 1,5 га. Три корпуса.</div>
</body></html>
"""
def test_newbuilding_parse_parity() -> None:
"""ЖК-лендинг → YandexNewbuildingInfo идентичен."""
old_scraper = OldNewbuildingScraper()
new_scraper = NewNewbuildingScraper()
old_res = old_scraper.parse(
_NEWBUILDING_HTML,
jk_slug="tatlin",
jk_id="1592987",
source_url="https://realty.yandex.ru/ekaterinburg/kupit/novostrojka/tatlin-1592987/",
)
new_res = new_scraper.parse(
_NEWBUILDING_HTML,
jk_slug="tatlin",
jk_id="1592987",
source_url="https://realty.yandex.ru/ekaterinburg/kupit/novostrojka/tatlin-1592987/",
)
assert old_res.model_dump() == new_res.model_dump()
# ── strangler-guard: kit-провайдер не импортирует app.* ──────────────────────
def test_kit_yandex_has_no_app_imports() -> None:
"""Ни один модуль scraper_kit.providers.yandex.* не импортирует app.* (развязка)."""
from scraper_kit.providers.yandex import detail, newbuilding, serp, shared, valuation
for mod in (serp, detail, valuation, newbuilding, shared):
source = inspect.getsource(mod)
for line in source.splitlines():
stripped = line.strip()
assert not stripped.startswith("from app"), f"{mod.__name__}: {line!r}"
assert not stripped.startswith("import app"), f"{mod.__name__}: {line!r}"