test(tradein): reusable legacy/scraper_kit parity harness (#2304) #2313

Merged
lekss361 merged 2 commits from feat/tradein-parity-test-harness into main 2026-07-03 20:52:26 +00:00
Owner

Closes #2304 (Stage 0 эпика #2277 — scraper_kit migration).

Что сделано

  • tests/support/parity.py — переиспользуемая parity-test утилита: assert_parity()/compare_outputs(), нормализует dataclass/pydantic перед сравнением (решает проблему "legacy_result == kit_result всегда False, т.к. это разные классы"), поддерживает ignore_fields и глобальный numeric tolerance, при расхождении кидает ParityMismatchError с полным field-path и обоими значениями.
  • tests/support/test_parity.py — 7 unit-тестов харнесса (идентичные/разные входы, tolerance, ignore_fields, bool/int type-mismatch — см. hardening ниже).
  • tests/support/README.md — как следующим PR'ам (#2305-#2310) подключить харнесс.
  • tests/scrapers/test_avito_detail_kit_parity.py — smoke-доказательство на реальном коде: legacy avito_detail.parse_detail_html vs kit providers/avito/detail.py на фиксированной HTML-фикстуре + тест, что харнесс реально ЛОВИТ расхождение (мутация price_rubParityMismatchError с упоминанием поля).

Hardening после code-review (2-й коммит)

Ревьюер (verdict: ⚠️ MINOR, safe to merge) нашёл: exact-equality путь не отличал True/1 (bool — подкласс int в Python, True == 1). Для будущей высокорисковой миграции estimator.py (#2308) такая тихая маскировка типа была бы опасна. Пофикшено + добавлен тест. Per-field tolerance (dict вместо float) оставлен как заметка на будущее для #2308 — не нужен сейчас (default tolerance=0.0, opt-in).

Проверено

  • pytest tests/support tests/scrapers/test_avito_detail_kit_parity.py — 10 passed (после rebase на актуальный main)
  • ruff check + format — чисто
  • Не трогает app/services/scrapers/* (legacy), не трогает scraper_kit provider-логику, не мигрирует ни один из 12 importers (это #2305-#2310)

Дальше

Блокирует #2305, #2306, #2307, #2310 — все используют этот харнесс для parity-гейта.

Closes #2304 (Stage 0 эпика #2277 — scraper_kit migration). ## Что сделано - `tests/support/parity.py` — переиспользуемая parity-test утилита: `assert_parity()`/`compare_outputs()`, нормализует dataclass/pydantic перед сравнением (решает проблему "`legacy_result == kit_result` всегда `False`, т.к. это разные классы"), поддерживает `ignore_fields` и глобальный numeric `tolerance`, при расхождении кидает `ParityMismatchError` с полным field-path и обоими значениями. - `tests/support/test_parity.py` — 7 unit-тестов харнесса (идентичные/разные входы, tolerance, ignore_fields, bool/int type-mismatch — см. hardening ниже). - `tests/support/README.md` — как следующим PR'ам (#2305-#2310) подключить харнесс. - `tests/scrapers/test_avito_detail_kit_parity.py` — smoke-доказательство на реальном коде: legacy `avito_detail.parse_detail_html` vs kit `providers/avito/detail.py` на фиксированной HTML-фикстуре + тест, что харнесс реально ЛОВИТ расхождение (мутация `price_rub` → `ParityMismatchError` с упоминанием поля). ## Hardening после code-review (2-й коммит) Ревьюер (verdict: ⚠️ MINOR, safe to merge) нашёл: exact-equality путь не отличал `True`/`1` (bool — подкласс int в Python, `True == 1`). Для будущей высокорисковой миграции `estimator.py` (#2308) такая тихая маскировка типа была бы опасна. Пофикшено + добавлен тест. Per-field tolerance (dict вместо float) оставлен как заметка на будущее для #2308 — не нужен сейчас (default `tolerance=0.0`, opt-in). ## Проверено - `pytest tests/support tests/scrapers/test_avito_detail_kit_parity.py` — 10 passed (после rebase на актуальный main) - ruff check + format — чисто - Не трогает `app/services/scrapers/*` (legacy), не трогает `scraper_kit` provider-логику, не мигрирует ни один из 12 importers (это #2305-#2310) ## Дальше Блокирует #2305, #2306, #2307, #2310 — все используют этот харнесс для parity-гейта.
lekss361 added 2 commits 2026-07-03 20:46:38 +00:00
Stage 0 of the scraper_kit migration epic (#2277): shared test tool for
issues #2305-#2310, which each need to prove their kit-path importer
produces the same output as the legacy path on the same input.

- tests/support/parity.py: assert_parity()/compare_outputs() normalize
  dataclass/pydantic outputs to dict/list/scalar before comparing, since
  legacy vs kit dataclasses (e.g. DetailEnrichment) are different classes
  and dataclass __eq__ always returns False across classes even when all
  field values match. Supports ignore_fields (drop non-deterministic
  fields like latency_ms/fetched_at) and numeric tolerance (math.isclose)
  for float fields, with an assertion listing every differing field
  (path + legacy value + kit value) on mismatch.
- tests/support/test_parity.py: unit tests for the harness itself
  (identical outputs pass, differing outputs raise with informative
  diff, tolerance/ignore_fields options, cross-class dataclass parity).
- tests/scrapers/test_avito_detail_kit_parity.py: end-to-end smoke proof
  against real code — app.services.scrapers.avito_detail.parse_detail_html
  (legacy, reached via admin.py's scrape_avito_detail debug endpoint
  through fetch_detail) vs scraper_kit.providers.avito.detail's copy, on
  a fixed HTML fixture.
- tests/support/README.md: usage note for #2305-#2310 migration PRs.

Found while implementing: tests/test_scraper_kit_*_parity.py (9 files,
~3400 lines) already do ad-hoc `dataclasses.asdict(old) == asdict(new)`
parity checks for the already-migrated SERP scraper modules (avito/cian/
domclick/yandex/base/scheduler/pipeline) — this harness generalizes that
repeated pattern for the remaining 12 non-scraper importers, adding
ignore_fields/tolerance which those ad-hoc checks don't have.
fix(tradein/tests): harden parity harness — bool-type mismatch detection + divergence-catch proof (#2304)
All checks were successful
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m34s
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
d95c9e6e55
Code-reviewer follow-up on the parity harness (aaaf8179):

- parity.py: plain `==` in the scalar-equality branch let `True == 1` /
  `False == 0` silently pass (Python bool is an int subclass). A future
  migration bug turning a `bool | None` field into a raw 0/1 would slip
  through undetected. Now any type mismatch where exactly one side is a
  bool is reported as a diff, regardless of numeric equality.
- test_parity.py: unit test for the new bool-vs-int branch.
- test_avito_detail_kit_parity.py: the existing smoke test only proved the
  harness reports "no diff" on two genuinely-identical real dataclass
  instances — it never proved the harness catches a real divergence on
  this same 30+-field shape (only the toy fixtures in test_parity.py did).
  Added a test that mutates `price_rub` via dataclasses.replace() on the
  real kit DetailEnrichment and asserts assert_parity raises
  ParityMismatchError naming that field.
lekss361 merged commit 5db6a2bec3 into main 2026-07-03 20:52:26 +00:00
lekss361 deleted branch feat/tradein-parity-test-harness 2026-07-03 20:52:26 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2313
No description provided.