test(tradein): reusable legacy/scraper_kit parity harness (#2304) #2313
3 changed files with 58 additions and 1 deletions
|
|
@ -29,6 +29,8 @@ 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 — тест должен жить и без
|
||||
|
|
@ -42,7 +44,7 @@ from scraper_kit.providers.avito.detail import (
|
|||
from app.services.scrapers.avito_detail import (
|
||||
parse_detail_html as legacy_parse_detail_html,
|
||||
)
|
||||
from tests.support.parity import assert_parity
|
||||
from tests.support.parity import ParityMismatchError, assert_parity
|
||||
|
||||
_DETAIL_HTML = """
|
||||
<html><body>
|
||||
|
|
@ -100,3 +102,28 @@ def test_parity_harness_confirms_avito_detail_parse_equivalence() -> None:
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -111,6 +111,15 @@ def _diff(
|
|||
_diff(f"{path}[{i}]", lv, kv, ignore_fields, tolerance, diffs)
|
||||
return
|
||||
|
||||
if isinstance(legacy_val, bool) != isinstance(kit_val, bool):
|
||||
# bool — подкласс int в Python, поэтому `True == 1` и `False == 0` дают True
|
||||
# при обычном `==` (проверять надо isinstance(x, bool), НЕ isinstance(x, int)).
|
||||
# Миграция, случайно превратившая `bool | None` поле в сырой 0/1 (или наоборот),
|
||||
# должна считаться расхождением, даже если числовое значение совпадает.
|
||||
diffs.append(
|
||||
f"{path}: type mismatch (bool vs non-bool) — legacy={legacy_val!r}, kit={kit_val!r}"
|
||||
)
|
||||
return
|
||||
if legacy_val == kit_val:
|
||||
return
|
||||
if _within_tolerance(legacy_val, kit_val, tolerance):
|
||||
|
|
|
|||
|
|
@ -98,6 +98,27 @@ def test_dataclass_instances_of_different_classes_compared_structurally() -> Non
|
|||
assert compare_outputs(legacy, kit) == []
|
||||
|
||||
|
||||
def test_bool_vs_int_type_mismatch_is_not_silently_equal() -> None:
|
||||
# Python bool — подкласс int: `True == 1` и `False == 0` дают True при обычном
|
||||
# `==`. Миграция, случайно превратившая bool-поле в сырой 0/1, должна ловиться.
|
||||
def legacy_fn(x: int) -> dict[str, object]:
|
||||
return {"mortgage_available": True}
|
||||
|
||||
def kit_fn(x: int) -> dict[str, object]:
|
||||
return {"mortgage_available": 1} # намеренно int вместо bool
|
||||
|
||||
with pytest.raises(ParityMismatchError) as exc_info:
|
||||
assert_parity(legacy_fn, kit_fn, fixtures=[1])
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert "mortgage_available" in message
|
||||
assert "bool" in message
|
||||
|
||||
# Обратный случай (False vs 0) и совпадающие типы (bool vs bool) — симметрично.
|
||||
assert compare_outputs({"v": False}, {"v": 0}) != []
|
||||
assert compare_outputs({"v": True}, {"v": True}) == []
|
||||
|
||||
|
||||
def test_dataclass_field_mismatch_reported_by_name() -> None:
|
||||
@dataclass
|
||||
class LegacyResult:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue