"""810-1 지킴 — STmate 원파일 파서(`M02_San_Stc`) · 실무 .STC 일곱(읽기만). (a) 거울 — `tools/stmate-test/smt_parser.py`(parse_dbf_bytes · extract_costn_memos)와 COSTN · BOQ11 행 · 칸 · 산근 원문 1:1(다름 0) · 줄 풀이 = `M02_San_Stmate.parse_line` (b) 블록 다시 셈(`M02_San_Eval.stmate_block` · 차수 1 단가 · 낙찰 · 결과 조정 · 끝전 갈래 = 옵션 SAN_TYP 1 비목버림 / 0 합버림_큰나머지) = COSTN 저장 금액(TAMT1 · LAMT1 · MAMT1 · SAMT1) 100 % (c) 엑셀 짝 대조는 812-1 엑셀 읽기기 골든(`test_812_1_xlsx_golden` · 옛 엑셀 엔진 걷음) 원파일 없으면 skip(다른 PC). """ from __future__ import annotations import sys from functools import lru_cache from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] for p in (ROOT, ROOT / "tools" / "stmate-test", ROOT / "resources" / "tester"): if str(p) not in sys.path: sys.path.insert(0, str(p)) from M02_MasterTemplete import M02_San_Eval as ev # noqa: E402 from M02_MasterTemplete import M02_San_Stc as S # noqa: E402 from M02_MasterTemplete import M02_San_Stmate as stm # noqa: E402 K = ROOT / "resources" / "knowledge" / "original" / "실무문서" SO = K / "25년 산불진화임도(기번8)(울진소광)" FILES = { "봉화": K / "04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC", # noqa: E501 "동부청영월": K / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC", # noqa: E501 "울진대흥": K / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC", # noqa: E501 "영덕계류": K / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC", # noqa: E501 "울진소광": SO / "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", # noqa: E501 "소광22": SO / "1. 설계원본/03-내역/22-2025년 산불진화임도사업(울진 금강송 소광).STC", # noqa: E501 "소광변경1": SO / "변경 1차분-2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", # noqa: E501 } # fmt: skip #: 파일 → (산근 블록, 일위 블록 · 구성 못 읽음, SAN_TYP) WANT = { "봉화": (65, 45, "1"), "동부청영월": (69, 23, "0"), "울진대흥": (46, 34, "1"), "영덕계류": (35, 13, "1"), "울진소광": (62, 37, "1"), "소광22": (40, 25, "1"), "소광변경1": (54, 39, "1"), } RULE = {"1": "비목버림", "0": "합버림_큰나머지"} @lru_cache(maxsize=2) def raw(tag: str) -> bytes: path = FILES[tag] if not path.exists(): pytest.skip(f"원파일 없음 {path.name}") return path.read_bytes() @lru_cache(maxsize=2) def parsed(tag: str) -> dict: return S.read(raw(tag)) def _members(tag: str) -> dict[str, bytes]: z, names = S._zip(raw(tag)) return {k: z.read(v) for k, v in names.items()} @pytest.mark.parametrize("tag", list(FILES)) def test_mirror_smt_parser(tag: str) -> None: import smt_parser as sp m = _members(tag) for name in ("COSTN.DBF", "BOQ11.DBF"): _, theirs, _ = sp.parse_dbf_bytes(m[name]) mine = S.dbf(m[name]) assert len(mine) == len(theirs), name for a, b in zip(mine, theirs, strict=True): b = {k: v for k, v in b.items() if k != "_deleted"} if "IFORM" in a: assert a.pop("IFORM").hex() == b.pop("IFORM_HEX") b.pop("IFORM") assert a == b memos = sp.extract_costn_memos(m["COSTN.DBF"], m["COSTN.SMT"]) d = parsed(tag) blocks = {b["코드"]: b for b in d["산근"]} assert sorted(blocks) == sorted(memos) for code, mm in memos.items(): assert [ln["글"] for ln in blocks[code]["줄"]] == mm["text"].splitlines() assert all(ln["풀이"] == stm.parse_line(ln["글"]) for ln in blocks[code]["줄"]) @pytest.mark.parametrize("tag", list(FILES)) def test_saved_amounts(tag: str) -> None: d = parsed(tag) n_cost, n_head, san = WANT[tag] assert (len(d["산근"]), len(d["일위"]), d["옵션"]["SAN_TYP"]) == (n_cost, n_head, san) assert all(b["표지"] == [S.UNREAD[0]] and not b["줄"] for b in d["일위"]) bad = [] for b in d["산근"]: got = ev.stmate_block(b, [ln["풀이"] for ln in b["줄"]], d["단가"], RULE[san])["금액"] if any(abs(got[c] - b["저장"][c]) >= 0.5 for c in "TLMS"): bad.append((b["코드"], got, b["저장"])) assert bad == [] assert all(not b["표지"] for b in d["산근"]) def test_structure_line() -> None: """봉화 D00014 — 참조 줄 자원 · 비목 · 계 · 산근 블록 머리.""" b = next(b for b in parsed("봉화")["산근"] if b["코드"] == "D00014") assert (b["이름"], b["단위"], b["저장"]["T"]) == ("수평규준틀(변곡점)", "개소", 26144) refs = [ln for ln in b["줄"] if ln["자원"]] assert [ln["자원"]["코드"] for ln in refs] == ["M00080", "M00002", "M00001", "L00001", "L00007"] assert refs[0]["자원"]["종류"] == "자재" and refs[3]["자원"]["종류"] == "노임" assert [ln["단"] for ln in b["줄"] if ln["합계"]] == [1, 1, 2]