"""Проверки §7 ядра (spec statement-checks; design D-4, D-5): перенос проверок tabular-parser.""" from decimal import Decimal import pytest from src.domain.statement_checks import ( ChecksReport, FailedCheck, cross_form_check, load_check_rules, run_checks, ) D = Decimal RULES = load_check_rules() def _lines(*items: tuple[str, dict[int, str | None]]) -> list[tuple[str, dict[int, Decimal | None]]]: return [(code, {i: (D(v) if v is not None else None) for i, v in values.items()}) for code, values in items] def test_rules_loaded_from_copy() -> None: assert RULES.tolerance == D("1") assert RULES.rounding_tolerance_percent == D("0.001") relations = [rule.relation for rule in RULES.relations_for("BALANCE")] assert "1600 = 1100 + 1200" in relations and "1600 = 1700" in relations income = {rule.relation: rule for rule in RULES.relations_for("INCOME")} assert income["2100 = 2110 - 2120"].abs_terms == frozenset({"2120"}) assert income["2400 = 2300 - 2410 + 2420"].strict is False def test_hard_violation() -> None: report = run_checks( "BALANCE", _lines(("1100", {0: "300"}), ("1200", {0: "600"}), ("1600", {0: "1000"})), [0], RULES ) assert FailedCheck(0, "1600 = 1100 + 1200", D("1000"), D("900")) in report.failed def test_relation_skipped_without_target() -> None: report = run_checks("BALANCE", _lines(("1110", {0: "5"}), ("1150", {0: "7"})), [0], RULES) assert report == ChecksReport(failed=(), soft_failed=()) def test_tolerance_one_unit() -> None: report = run_checks("BALANCE", _lines(("1100", {0: "300"}), ("1200", {0: "600"}), ("1600", {0: "901"})), [0], RULES) assert all(item.relation != "1600 = 1100 + 1200" for item in report.failed) def test_abs_terms_minus_sign() -> None: report = run_checks("INCOME", _lines(("2110", {0: "100"}), ("2120", {0: "-60"}), ("2100", {0: "40"})), [0], RULES) assert all(item.relation != "2100 = 2110 - 2120" for item in report.failed) def test_rounding_goes_soft() -> None: # расхождение 5 при числах ~1 000 000: 5 ≤ 1 000 000 × 0,001 % = 10 → мягкое «в пределах округления» report = run_checks( "BALANCE", _lines(("1100", {0: "500000"}), ("1200", {0: "500000"}), ("1600", {0: "1000005"})), [0], RULES ) assert all(item.relation != "1600 = 1100 + 1200" for item in report.failed) assert any(item.relation == "1600 = 1100 + 1200" for item in report.soft_failed) def test_soft_relation_not_hard() -> None: report = run_checks("INCOME", _lines(("2300", {0: "100"}), ("2410", {0: "20"}), ("2400", {0: "50"})), [0], RULES) assert all(item.relation != "2400 = 2300 - 2410 + 2420" for item in report.failed) assert any(item.relation == "2400 = 2300 - 2410 + 2420" for item in report.soft_failed) def test_sublines_soft() -> None: report = run_checks("BALANCE", _lines(("1150", {0: "10"}), ("11501", {0: "6"}), ("11502", {0: "2"})), [0], RULES) assert any(item.relation == "1150 = 11501 + 11502" for item in report.soft_failed) assert report.failed == () def test_target_present_no_terms_hard_failure() -> None: # Спека: отсутствующий код = 0; если цель присутствует но членов нет, это нарушение # Правило 1600 = 1100 + 1200, в колонке только 1600=1000 → 1600 ≠ 0, жёсткое нарушение report = run_checks("BALANCE", _lines(("1600", {0: "1000"})), [0], RULES) assert any( item.relation == "1600 = 1100 + 1200" and item.expected == D("1000") and item.actual == D("0") for item in report.failed ) def test_target_without_self_sum_hard_failure() -> None: # Спека: пропуск только если цель отсутствует; если цель есть, правило проверяется # Правило 1600 = 1700, в колонке только 1600=1000 (нет 1300, 1400, 1500) → нарушение report = run_checks("BALANCE", _lines(("1600", {0: "1000"})), [0], RULES) assert any( item.relation == "1600 = 1700" and item.expected == D("1000") and item.actual == D("0") for item in report.failed ) @pytest.mark.parametrize( ("b1370", "i2400", "expected"), [ ((D("500"), D("300")), D("200"), None), ( (D("500"), D("300")), D("150"), FailedCheck(-1, "1370 − 1370(начало года) = 2400", D("200"), D("150")), ), ((None, D("300")), D("200"), None), ((D("500"), D("300")), None, None), ], ) def test_cross_form( b1370: tuple[Decimal | None, Decimal | None], i2400: Decimal | None, expected: FailedCheck | None ) -> None: assert cross_form_check(b1370, i2400, D("1")) == expected def test_broken_rules_file(tmp_path) -> None: bad = tmp_path / "statement_checks.yaml" bad.write_text("tolerance: 1\nrelations:\n - {form: BALANCE, relation: '16A0 = 1100', strict: true}\n", "utf-8") with pytest.raises(ValueError, match="statement_checks.yaml"): load_check_rules(bad)