"""Unit-тесты для parity-harness'а самого по себе (tests/support/parity.py). Проверяет три сценария из issue #2304: - legacy/kit выводы идентичны → assert_parity проходит без исключения; - выводы различаются → ParityMismatchError с информативным диффом (не просто "not equal"); - tolerance-опция гасит незначащие float-расхождения (напр. latency_ms). Также покрывает ignore_fields (второй способ игнорировать недетерминированные поля) и кейс, ради которого harness вообще нужен: dataclass-инстансы РАЗНЫХ классов (имитация legacy vs kit module) с одинаковыми полями должны сравниваться структурно, а не через identity классов. """ from __future__ import annotations from dataclasses import dataclass import pytest from tests.support.parity import ParityMismatchError, assert_parity, compare_outputs def test_assert_parity_passes_when_outputs_identical() -> None: def legacy_fn(x: int) -> dict[str, int]: return {"value": x * 2} def kit_fn(x: int) -> dict[str, int]: return {"value": x * 2} assert_parity(legacy_fn, kit_fn, fixtures=[1, 2, 3]) def test_assert_parity_raises_with_informative_diff_when_outputs_differ() -> None: def legacy_fn(x: int) -> dict[str, int]: return {"value": x, "count": 10} def kit_fn(x: int) -> dict[str, int]: return {"value": x, "count": 999} # намеренное расхождение with pytest.raises(ParityMismatchError) as exc_info: assert_parity(legacy_fn, kit_fn, fixtures=[1]) message = str(exc_info.value) # Диагностика должна называть КОНКРЕТНОЕ поле и оба значения, не просто "not equal". assert "$.count" in message assert "10" in message assert "999" in message # value совпало у обеих функций → не должно попасть в список различий. assert "$.value" not in message def test_assert_parity_tolerance_ignores_small_float_drift() -> None: def legacy_fn(x: int) -> dict[str, float]: return {"latency_ms": 100.001, "score": 0.5} def kit_fn(x: int) -> dict[str, float]: return {"latency_ms": 100.004, "score": 0.5} # Без tolerance — расхождение 0.003 ловится. with pytest.raises(ParityMismatchError): assert_parity(legacy_fn, kit_fn, fixtures=[1]) # С tolerance >= drift — проходит. assert_parity(legacy_fn, kit_fn, fixtures=[1], tolerance=0.01) def test_ignore_fields_skips_named_field_entirely() -> None: def legacy_fn(x: int) -> dict[str, object]: return {"fetched_at": "2026-01-01T00:00:00Z", "value": x} def kit_fn(x: int) -> dict[str, object]: return {"fetched_at": "2026-07-03T12:00:00Z", "value": x} # timestamp всегда разный with pytest.raises(ParityMismatchError): assert_parity(legacy_fn, kit_fn, fixtures=[1]) assert_parity(legacy_fn, kit_fn, fixtures=[1], ignore_fields={"fetched_at"}) def test_dataclass_instances_of_different_classes_compared_structurally() -> None: # Имитация legacy vs kit: одинаковые поля, РАЗНЫЕ классы (разные модули). @dataclass class LegacyResult: item_id: str price: int @dataclass class KitResult: item_id: str price: int legacy = LegacyResult(item_id="42", price=100) kit = KitResult(item_id="42", price=100) # Прямое == было бы False (dataclass __eq__ проверяет class identity первым). assert legacy != kit # compare_outputs сравнивает по полям, а не по классу → различий нет. 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: item_id: str price: int @dataclass class KitResult: item_id: str price: int legacy = LegacyResult(item_id="42", price=100) kit = KitResult(item_id="42", price=200) diffs = compare_outputs(legacy, kit) assert len(diffs) == 1 assert "price" in diffs[0] assert "100" in diffs[0] assert "200" in diffs[0]