gendesign/backend/tests/services/scrapers/test_domrf_kn_normalize.py
Light1YT a49ee2ccfc
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m27s
CI / backend-tests (pull_request) Successful in 6m27s
fix(domrf_kn): детерминированный sha256-fallback для flat_id (#1208)
Fallback flat_id в _norm_flat использовал abs(hash(elem)) % (2**63 - 1).
В CPython hash(str) РАНДОМИЗИРОВАН per-process (PYTHONHASHSEED нигде в
репо не зафиксирован — uvicorn/celery не выставляют его). Эффект:

- При resume упавшего sweep (resume_kn_run в новом процессе воркера) до
  10 объектов после checkpoint перечитываются. Квартиры без flatId
  получают ДРУГОЙ hash-id → ON CONFLICT (id, snapshot_date) не
  срабатывает → дубли строк одной квартиры в одном snapshot.
- То же при повторном прогоне за ту же дату — каждый раз новый id.
- Дубли инфлируют все агрегации (units_sold/price медианы, supply_layers).

Patch:
- abs(hash(elem)) → int.from_bytes(sha256(elem)[:8], "big") % (2**63 - 1).
- sha256 стабилен между процессами/перезапусками. 8 байт → BIGINT-fit.

6 новых юнит-тестов (test_domrf_kn_normalize.py): formula matches sha256,
stable across calls, distinct elems→distinct ids, flatId wins over
fallback, no-id→None, BIGINT-fit. ruff clean.

Closes #1208
2026-06-13 11:01:41 +05:00

57 lines
2.6 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.

"""Unit-тесты для нормализаторов scrapers/domrf_kn (#1208).
Главный сценарий: детерминированный fallback flat_id через sha256 — НЕ
рандомизированный hash(). Раньше при resume упавшего sweep (новый процесс
воркера) та же квартира получала другой id → ON CONFLICT не срабатывал →
дубли в одном snapshot.
"""
from __future__ import annotations
import hashlib
from app.services.scrapers.domrf_kn import _norm_flat
def _expected_sha_fallback(elem: str) -> int:
"""Зеркало формулы из _norm_flat — для прямого сравнения в тестах."""
digest = hashlib.sha256(str(elem).encode("utf-8")).digest()[:8]
return int.from_bytes(digest, "big") % (2**63 - 1)
class TestFlatIdFallbackDeterministic:
"""flat_id fallback ОБЯЗАН быть детерминированным между процессами/перезапусками."""
def test_fallback_matches_sha256_formula(self) -> None:
row = {"elemId": "abc123"}
out = _norm_flat(row, region_cd=66)
assert out["id"] == _expected_sha_fallback("abc123")
def test_fallback_stable_across_calls(self) -> None:
# Тот же elemId → тот же id. (Раньше hash() мог дать другой id в новом процессе.)
row = {"elemId": "stable-elem-id"}
a = _norm_flat(row, region_cd=66)["id"]
b = _norm_flat(row, region_cd=66)["id"]
assert a == b
assert a == _expected_sha_fallback("stable-elem-id")
def test_different_elems_give_different_ids(self) -> None:
a = _norm_flat({"elemId": "elem-A"}, region_cd=66)["id"]
b = _norm_flat({"elemId": "elem-B"}, region_cd=66)["id"]
assert a != b
def test_flat_id_present_wins_over_fallback(self) -> None:
# Если flatId есть — используем его, fallback не активируется.
row = {"flatId": 12345, "elemId": "should-be-ignored"}
out = _norm_flat(row, region_cd=66)
assert out["id"] == 12345
def test_no_flat_id_no_elem_id_returns_none(self) -> None:
out = _norm_flat({}, region_cd=66)
assert out["id"] is None
def test_fallback_fits_bigint(self) -> None:
# 8-байтовый digest усечён в pos-int, всегда < 2**63.
out = _norm_flat({"elemId": "x" * 1000}, region_cd=66)
assert out["id"] is not None
assert 0 <= out["id"] < 2**63