"""Индексная выборка значений колонок должна давать то же, что _pick_value. _pick_value / _pick_structured_value — эталонная (медленная) семантика: на каждую строку отчёта перебираются все кандидаты каждого правила. Быстрый путь строит обратный индекс «путь -> правила» один раз и за строку проходит только по полям самой записи. Тест сверяет оба пути на случайных и реальных payload по всем правилам и всем источникам (operation / participant / merged). """ from __future__ import annotations import random from app.pipeline import mapping from app.pipeline.column_constants import BLOCK_EIO from app.pipeline.mapping import ( REPORT_MAPPING_RULES, _build_payload_lookup, _merge_fields, _partition_participant_lookup, _pick_structured_value, _pick_value, _rule_lookup_source, pick_rule_values_indexed, ) from app.pipeline.parser import parse_xml_content _PREFIXES = ("", "УчастникОп.", "СообщОперКО.ИнформЧасть.СведКО.Операция.") def _candidate_pool() -> list[str]: pool: set[str] = set() for rule in REPORT_MAPPING_RULES: for candidate in rule.compiled_candidates: pool.add(candidate.raw) pool.add(candidate.short) parts = candidate.raw.split(".") for start in range(len(parts)): pool.add(".".join(parts[start:])) return sorted(pool) def _random_payload(rng: random.Random, pool: list[str]) -> dict[str, str]: payload: dict[str, str] = {} for _ in range(rng.randint(1, 40)): key = rng.choice(pool) prefix = rng.choice(_PREFIXES) if rng.random() < 0.25: parts = key.split(".") position = rng.randrange(len(parts)) parts[position] = f"{parts[position]}[{rng.randint(0, 2)}]" key = ".".join(parts) key = f"{prefix}{key}" value = "" if rng.random() < 0.1 else f"v{rng.randint(1, 5)}" payload[key] = value if rng.random() < 0.3: payload[f"Шум{rng.randint(1, 9)}.Поле"] = "шум" return payload def _reference_values(lookup, source: str) -> dict[int, str]: expected: dict[int, str] = {} for position, rule in enumerate(REPORT_MAPPING_RULES): if not rule.allow_direct_mapping or _rule_lookup_source(rule) != source: continue allow_short_lookup = rule.allow_short_lookup or rule.column_index in BLOCK_EIO if rule.structured_value: expected[position] = _pick_structured_value( lookup=lookup, candidates=rule.compiled_candidates, join_with_space="фио" in rule.column_name.lower(), allow_short_lookup=allow_short_lookup, ) else: expected[position] = _pick_value( lookup, rule.compiled_candidates, allow_short_lookup=allow_short_lookup, ) return expected def _lookups_for(operation_fields, participant_fields): operation_lookup = _build_payload_lookup(operation_fields) participant_lookup = _build_payload_lookup(participant_fields) base_lookup, eio_lookup = _partition_participant_lookup(participant_lookup) merged_lookup = _build_payload_lookup( _merge_fields(operation_fields, participant_fields) ) return { "operation": operation_lookup, "participant_base": base_lookup, "participant_eio": eio_lookup, "merged": merged_lookup, } def _assert_same_values(operation_fields, participant_fields, label: str) -> None: for source, lookup in _lookups_for(operation_fields, participant_fields).items(): expected = _reference_values(lookup, source) actual = pick_rule_values_indexed(lookup, source) for position, expected_value in expected.items(): rule = REPORT_MAPPING_RULES[position] assert actual.get(position, "") == expected_value, ( f"{label}: source={source}, колонка {rule.column_index} " f"({rule.column_id}): ожидалось {expected_value!r}, " f"получено {actual.get(position, '')!r}" ) extra = set(actual) - set(expected) assert not extra, f"{label}: лишние правила в индексной выборке: {extra}" def test_indexed_values_match_pick_value_on_random_payloads() -> None: rng = random.Random(20261009) pool = _candidate_pool() for sample in range(400): operation_fields = _random_payload(rng, pool) participant_fields = _random_payload(rng, pool) _assert_same_values(operation_fields, participant_fields, f"sample {sample}") def test_indexed_values_match_pick_value_on_parsed_xml() -> None: xml = """ <СообщОперКО xmlns="urn:test"> <СлужЧасть><ИдФайл>ID1<ДатаФайла>2026-06-16 <ИнформЧасть> <ИнфБанк><БИК>044525111<НаимКО>АО Банк <СведКО> <ИнфФилиал><НомерФилиала>01 <Операция> <ИдентификаторЗаписи>OP1 <КодОперации>6001 <ДатаОперации>2026-06-16 <СуммаОперации>12345.67 <ВалютаОперации>643 <НазначениеПлатежа>Оплата по договору <ОснованиеОп><КодДок>21<НомДок>1<ДатаДок>2026-06-01 <ОснованиеОп><КодДок>22<НомДок>2<ДатаДок>2026-06-02 <СведПереводДС> <ВидПеревода>1 <СведБанкПлательщик><БИККО>044525111<НаимБанк>АО Банк<НомерСчета>40702810000000000001 <СведБанкПолучатель><БИККО>044525222<НаимБанк>ПАО Банк2<НомерСчета>40702810000000000002 <УчастникОп> <ТипУчастника>2 <УчастникФЛИП> <СведФЛИП> <ФИО><Фамилия>Иванов<Имя>Иван<Отчество>Иванович <ИННФЛИП>123456789012 <ДатаРождения>1980-01-01 <КодОКСМ>643 <СведДокУдЛичн><КодДок>21<СерияДок>4510<НомДок>123456<ДатаДок>2010-01-01<КемВыдан>ОУФМС <АдрРег Индекс="674500" КодОКСМ="643" КодСубъектаПоОКАТО="45" Пункт="Москва г" Улица="Ленина ул" Дом="1" Корпус="2" Квартира="3"/> <УчастникОп> <ТипУчастника>1 <УчастникЮЛ> <СведЮЛ> <НаимЮЛ>ООО Ромашка<ИННЮЛ>7701234567<КППЮЛ>770101001<ОГРН>1027700000001 <АдрРегЮЛ Индекс="675520" КодОКСМ="643" КодСубъектаПоОКАТО="10" Район="Благовещенский р-н" Пункт="Чигири с" Улица="Зеленая ул" Дом="1"/> <СведЕИО><ФЛЕИО><ФИО><Фамилия>Петров<Имя>Петр<ИННФЛИП>111111111111 <БенефициарЮЛ><ФЛЕИО><ФИО><Фамилия>Сидоров<Имя>Сидор<ИННФЛИП>222222222222 """ result = parse_xml_content("SKO115FZ_01_044525111_20260616_000001.xml", xml) assert not result.is_fatal assert result.rows for row in result.rows: _assert_same_values( row.operation_fields, row.participant_fields, f"row {row.operation_index}/{row.is_continuation}", ) def test_every_direct_rule_has_exactly_one_lookup_source() -> None: for rule in REPORT_MAPPING_RULES: source = _rule_lookup_source(rule) assert source in mapping.LOOKUP_SOURCES, (rule.column_id, source)