"""B08 근거 사전 — 사전이 **엔진과 어긋나지 않는지** 지키는 시험 (PLAN 8-36 ④). 이 시험의 값어치는 마지막 것 하나에 있다: **사전에 적은 열 이름이 실제 토적표 줄에 있는가.** 엔진이 열을 바꾸거나 이름을 갈면 사전만 옛것으로 남아, 맞는 값 옆에 틀린 근거가 붙는다. 그 어긋남은 화면에서 눈에 안 띄므로(카드가 그냥 안 뜬다) 여기서 잡는다. """ from __future__ import annotations import dataclasses import pytest from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import SummaryRow from B08_Quantity.B08_Quantity_Engine_EarthworkTable import EarthworkRow from B08_Quantity.B08_Quantity_Engine_HaulSummary import HaulSummaryRow from B08_Quantity.B08_Quantity_Provenance import ( earthwork_sheet, haul_sheet, quantity_provenance, summary_sheet, ) from common_util import common_util_provenance as provenance_module from common_util.common_util_provenance import ( TIERS, ColumnProvenance, provenance_payload, sheet_provenance, ) def test_토적표_사전이_엔진_열과_같은_이름을_쓴다(): """사전 열 키가 전부 `EarthworkRow` 에 있어야 한다 — **이 시험이 사전의 존재 이유다.**""" row_fields = {field.name for field in dataclasses.fields(EarthworkRow)} dictionary = earthwork_sheet()["columns"] 낯선_키 = sorted(set(dictionary) - row_fields) assert not 낯선_키, f"사전에 있는데 토적표 줄에 없는 열: {낯선_키}" def _모든_열(): """개발환경에서 실리는 여섯 장을 한 줄로 펜다 — (장 이름, 열 키, 몸통).""" payload = quantity_provenance() assert payload is not None for sheet_name, sheet in payload["sheets"].items(): for key, body in sheet["columns"].items(): yield sheet_name, key, body def test_여섯_장이_다_실린다(): payload = quantity_provenance() assert payload is not None assert set(payload["sheets"]) == { "earthwork", "summary", "haul", "preparation", "material", "unit_quantity", } def test_사전_등급이_전부_아는_값이다(): for sheet_name, key, body in _모든_열(): assert body["tier"] in TIERS, f"{sheet_name}.{key} 등급이 모르는 값: {body['tier']}" def test_사전_열마다_이름과_식이_비어_있지_않다(): """빈 카드는 「설명이 있다」는 거짓만 남긴다 — 적을 것이 없으면 열을 아예 안 넣는다.""" for sheet_name, key, body in _모든_열(): assert body.get("label"), f"{sheet_name}.{key} 에 이름이 없음" assert body.get("formula"), f"{sheet_name}.{key} 에 식이 없음" def test_같은_열을_두_번_적으면_막는다(): with pytest.raises(ValueError): sheet_provenance( [ ColumnProvenance(key="a", label="가", tier="calc", formula="x"), ColumnProvenance(key="a", label="나", tier="calc", formula="y"), ] ) def test_모르는_등급을_적으면_막는다(): with pytest.raises(ValueError): sheet_provenance([ColumnProvenance(key="a", label="가", tier="없는등급")]) def test_배포환경에서는_사전을_아예_안_보낸다(monkeypatch): """⚠ 로직 보안 — 화면에서 숨기는 것이 아니라 **응답에 안 싣는 것**이 문이다.""" monkeypatch.setattr(provenance_module, "is_dev_environment", lambda: False) assert provenance_payload({"earthwork": earthwork_sheet()}) is None assert quantity_provenance() is None def test_개발환경에서는_시트가_실린다(monkeypatch): monkeypatch.setattr(provenance_module, "is_dev_environment", lambda: True) payload = quantity_provenance() assert payload is not None assert "earthwork" in payload["sheets"] def test_고르는_자리의_채택_규칙은_적었을_때만_실린다(): """`rule` 은 안전관리비처럼 **값 안에 선택이 숨은** 열에만 붙는다(B09 조사 ㉯).""" 없는_것 = ColumnProvenance(key="a", label="가", tier="calc", formula="x").as_dict() assert "rule" not in 없는_것 있는_것 = ColumnProvenance( key="b", label="나", tier="calc", formula="x", rule="A·B 중 작은 쪽" ).as_dict() assert 있는_것["rule"] == "A·B 중 작은 쪽" def test_집계표_사전이_엔진_열과_같은_이름을_쓴다(): row_fields = {field.name for field in dataclasses.fields(SummaryRow)} 낯선_키 = sorted(set(summary_sheet()["columns"]) - row_fields) assert not 낯선_키, f"사전에 있는데 집계표 줄에 없는 열: {낯선_키}" def test_운반표_사전이_엔진_열과_같은_이름을_쓴다(): """⚠ `average_distance_m` 은 필드가 아니라 property 라 필드 목록만 보면 놓친다.""" names = {field.name for field in dataclasses.fields(HaulSummaryRow)} names |= {n for n in dir(HaulSummaryRow) if not n.startswith("_")} 낯선_키 = sorted(set(haul_sheet()["columns"]) - names) assert not 낯선_키, f"사전에 있는데 운반표 줄에 없는 열: {낯선_키}" def test_집계표에_최종이_서고_토적표에는_없다(): """등급 여섯은 한 장이 아니라 **두 장을 합쳐야** 다 쓰인다 — 그 갈림을 시험으로 박는다.""" 토적표 = {body["tier"] for body in earthwork_sheet()["columns"].values()} 집계표 = {body["tier"] for body in summary_sheet()["columns"].values()} assert "final" not in 토적표, "토적표는 중간 장부라 최종 열이 없어야 함" assert "final" in 집계표, "내역서로 나가는 값은 집계표 「계」에서 서야 함"