"""원천 벌 뽑기(`_build_pum.py`) 시험 — 2026-09-18 브레인 일감(랩탑 메인). 못박는 것 - 원천 벌이 **지금 md 와 같음** — md 가 고쳐졌는데 안 뽑았으면 빨강(뽑기를 돌리라는 뜻). - ⚠ **줄이 밀려도 표 번호·절이 그대로** — 이번 막힘(밑수 238 → 105)의 원인 자리. - 표가 끼어들어도 **뒤 번호가 안 밀림** · 글자만 고쳐진 표는 **제 번호** · 사라진 표는 **알림**. - ⚠ 머리글이 같은 이웃 표(12-17-1 펌프카 타설 일곱 표)로 **건너뛰지 않음** — 서브가 겪은 사고. 금액 불변 — 뽑은 뒤 공종 마스터를 다시 짓고 `test_work_item_key_gate.py` 원가계산서 시험 (직접공사비 5,983,724)이 잼. 이 파일은 그 앞 단계(원천 벌 표 번호·절·칸 글자)가 안 흔들림을 잼. """ from __future__ import annotations import copy import json import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "resources" / "data_cost_input_value")) import _build_pum as build # noqa: E402 FOREST = build.CATALOGS["pum_forest"] @pytest.fixture(scope="module") def forest() -> dict: """지금 md 에서 뽑은 산림 벌 — 아래 시험은 이것에 줄을 보태고·표를 끼우고·고치고·뺌.""" committed = json.loads(FOREST.read_text(encoding="utf-8")) return build.extract(committed, build._read, "pum_forest")[0] def _lines_of(doc: dict) -> list[str]: return (ROOT / doc["sources"][0]["path"]).read_text(encoding="utf-8").splitlines() def _reader(lines: list[str]): return lambda path: "\n".join(lines).encode("utf-8") def _tables(doc: dict) -> list[dict]: return doc["variables"]["pum"]["tables"] @pytest.mark.parametrize("dataset", list(build.CATALOGS)) def test_원천_벌이_지금_md_와_같음(dataset: str) -> None: """md 가 고쳐졌으면 `_build_pum.py` 를 돌릴 것. sha 는 줄끝(CRLF) 따라 PC 마다 갈려 안 봄.""" old = json.loads(build.CATALOGS[dataset].read_text(encoding="utf-8")) doc, report = build.extract(old, build._read, dataset) stale = {k: v for k, v in report.items() if k != "same" and v} assert _tables(doc) == _tables(old), ( f"{dataset} 원천 벌이 md 와 다름 — _build_pum.py 돌릴 것: {stale}" ) def test_줄이_밀려도_표_번호와_절이_그대로(forest: dict) -> None: lines = _lines_of(forest) at = next(i for i, x in enumerate(lines) if x.startswith("### 5-1.")) shifted = lines[:at] + ["보탠 줄"] * 37 + lines[at:] doc, report = build.extract(forest, _reader(shifted), "pum_forest") before, after = _tables(forest), _tables(doc) assert [(t["table_id"], t["section"]) for t in after] == [(t["table_id"], t["section"]) for t in before] # fmt: skip assert [t["line"] - b["line"] for t, b in zip(after, before)] == [37 if b["line"] > at else 0 for b in before] # fmt: skip assert report["changed"] == report["new"] == report["removed"] == [] def test_표가_끼어들어도_뒤_번호가_안_밀리고_새_번호를_받음(forest: dict) -> None: lines = _lines_of(forest) at = next(i for i, x in enumerate(lines) if x.startswith("### 9-2.")) inserted = lines[: at + 1] + ["", "| 새 | 표 |", "|---|---|", "| 1 | 2 |", ""] + lines[at + 1 :] doc, report = build.extract(forest, _reader(inserted), "pum_forest") ids = [t["table_id"] for t in _tables(doc)] assert [t["table_id"] for t in _tables(forest)] == [i for i in ids if i != "F0477"] assert [(n["table_id"], n["section"]) for n in report["new"]] == [("F0477", "9-2. 노선 굴진 보조원")] # fmt: skip assert report["removed"] == [] and report["changed"] == [] def test_글자만_고쳐진_표는_제_번호_머리글_같은_이웃으로_안_건너뜀(forest: dict) -> None: """12-17-1 펌프카 타설 — 같은 머리글 표가 줄지어 있음. 가운데 표 한 칸을 고쳐도 번호가 제자리.""" lines = _lines_of(forest) target = next(t for t in _tables(forest) if t["table_id"] == "F0359") row = target["line"] + 1 # 첫 몸 줄(0부터 셈: 머리 line-1 · 구분줄 line · 몸 line+1) edited = copy.copy(lines) edited[row] = edited[row].replace("|", "| 고침", 1) # 첫 칸 글자만 바꿈 doc, report = build.extract(forest, _reader(edited), "pum_forest") assert [t["table_id"] for t in _tables(doc)] == [t["table_id"] for t in _tables(forest)] assert report["changed"] == [{"table_id": "F0359", "line": target["line"]}] def test_표가_빠지면_번호를_버리고_알림(forest: dict) -> None: lines = _lines_of(forest) target = next(t for t in _tables(forest) if t["table_id"] == "F0358") end = target["line"] + 1 + len(target["rows"]) # 머리·구분줄·몸 removed = lines[: target["line"] - 1] + lines[end:] doc, report = build.extract(forest, _reader(removed), "pum_forest") ids = [t["table_id"] for t in _tables(doc)] assert "F0358" not in ids and ids == [t["table_id"] for t in _tables(forest) if t["table_id"] != "F0358"] # fmt: skip assert [r["table_id"] for r in report["removed"]] == ["F0358"] and report["new"] == [] def test_2단_머리를_펼치지_않음() -> None: """윗머리 줄은 표 머리(`headers`)로, 아랫머리는 첫 몸 줄로 **글자 그대로** 남음.""" lines = ["### 8-3-1 손료", "", "| 분류 번호 | 시 간 당(10-7) | |", "|---|---|---|", "| | 상각비 계수 | 계 |", "| 0101-0007 | 1,811 | 2,000 |"] # fmt: skip (table,) = build.md_tables(lines) assert table["headers"] == ["분류 번호", "시 간 당(10-7)", ""] assert table["rows"][0] == ["", "상각비 계수", "계"]