Files
Aislo/resources/tester/test_m02_table_fill.py
T
eomsangdonandClaude Sonnet 5.5 2878b7c0de test(M02): 집계표 시험 넷 낡은 견본 · 기대값 맞춤(87-2)
- 자동 맞춤 견본 = 옵션 그룹 · 컨테이너 복사 건(복사본 …__그룹) 빼고 본체만 — 지정 항목 모드 몫이라 자동 맞춤에서 두 번 셈(46-3 · 54-6)
- 관 유출 흙막이 열 id 「rv|돌쌓기(찰)|1.5」(기본 높이 54-36) · 세월교 열 「fb|1000|2」(련수 기본 2 · 65-1)
- 템플릿 층 채운 표 알림 = 배관 한 개 컨테이너 셋 → 미지정 4건

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
2026-10-08 13:12:10 +09:00

230 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M02 구조물 집계표 채우기 — 펼침 열 · 측점 줄 · 관 줄 나눔 · 손 값 지킴 (임시 프로젝트)."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from M02_MasterTemplete import M02_Table_Fill as fill
from resources.tester.helper_m02_master_sample import sample_table
def _master() -> dict[str, Any]:
return sample_table() # 시험 안 견본(PLAN 26-5a) — 실제 마스터 안 읽음
@pytest.fixture
def project(tmp_path: Path) -> Path:
root = tmp_path / "proj"
route = root / "B05_Profile/route"
route.mkdir(parents=True)
structures = [
{
"type_id": "masonry_dry",
"chainage_m": 85,
"start_m": 80,
"end_m": 90,
"options": {"height_m": 2.0, "length_m": 10},
},
{
"type_id": "masonry_wet",
"chainage_m": 80,
"start_m": 80,
"end_m": 90,
"options": {"height_m": 2.5, "length_m": 10},
},
{
"type_id": "pavement_concrete",
"chainage_m": 205,
"start_m": 200,
"end_m": 230,
"options": {"length_m": 30, "width_m": 3, "thickness_cm": 20, "joint_spacing_m": 5},
},
{"type_id": "position_sign", "chainage_m": 300},
{
"type_id": "boulder_masonry",
"chainage_m": 325,
"start_m": 320,
"end_m": 330,
"options": {"height_m": 2.5, "length_m": 10, "bond": "메쌓기", "stone_cm": "40~60"},
},
]
for index, item in enumerate(structures):
item.setdefault("structure_id", f"st{index}")
item.setdefault("placement", "interval" if "start_m" in item else "point")
(route / "structures.json").write_text(
json.dumps({"revision": 1, "structures": structures}, ensure_ascii=False), encoding="utf-8"
)
edits = root / "B04_PreProcess/drainage/edits"
edits.mkdir(parents=True)
points = [
{"chainage_m": 85.05, "source": "user", "options": {"pipe_diameter_mm": "1000"}},
{"chainage_m": 85.05, "source": "user", "options": {"pipe_kind": "흄관"}},
{"chainage_m": 173.09, "source": "user", "facility": "ford_bridge", "options": {}},
]
(edits / "pipe_points.json").write_text(json.dumps({"points": points}), encoding="utf-8")
return root
def _auto(project: Path, document: dict[str, Any], lengths: dict[float, float]) -> dict[str, Any]:
"""바인딩 자동 맞춤(`fill_document` 기본) — 프로젝트 `fill_table` 은 지정 항목 모드(PLAN 21-3)라
펼침 · 묶음 · 손 값 규칙은 이 길로 봄."""
# 옵션 그룹 · 컨테이너마다 더 서는 건(복사본 `…__그룹`)은 지정 항목 모드 몫 — 자동 맞춤은 본체만 셈
items = [i for i in fill.collect_items(project, lengths) if "__" not in i["복사본"]]
return fill.fill_document(document, items)
def _filled(project: Path, document: dict[str, Any] | None = None) -> dict[str, Any]:
return _auto(project, document or _master(), {85.0: 14.0})
def _col(doc: dict[str, Any], col_id: str) -> dict[str, Any]:
return next(column for column in doc["열"] if column["id"] == col_id)
def test_펼침_열은_설계에_쓰인_값만_차례대로(project: Path) -> None:
doc = _filled(project)
ids = [column["id"] for column in doc["열"]]
assert [i for i in ids if i.startswith("ms|")] == ["ms|찰쌓기|2.5", "ms|메쌓기|2.0"]
assert _col(doc, "ms|메쌓기|2.0")["머리"] == ["돌쌓기", "메쌓기", "H=2.0"]
assert [i for i in ids if i.startswith("pp_len|")] == [
"pp_len|흄관|1000",
"pp_len|파형강관|1000",
]
assert not any(i.startswith("rw|") for i in ids) # 옹벽 없음 → 열 없음
assert "rv" not in ids and "ps" in ids # 펼침 마스터 칸은 빠지고 고정 열은 남음
# 관 유출 기슭막이는 레지스트리 기본값(찰 · 흙막이 기본 높이 1.5 — 54-36)으로 열이 섬 · id 는 설계 값
assert [i for i in ids if i.startswith("rv|")] == ["rv|돌쌓기(찰)|1.5"]
assert _col(doc, "rv|돌쌓기(찰)|1.5")["머리"] == ["돌기슭막이", "찰쌓기", "H=1.5"]
# 세월교 기본값(65-1) — 련수 칸 기본 2
assert _col(doc, "fb|1000|2")["머리"] == ["세월교", "Φ1000×2련", None]
assert not any(i.startswith("pg_in|") for i in ids) # 날개벽·집수정 형식 없음 → 관보호공 없음
def test_묶음_열은_값마다_한데(project: Path) -> None:
ids = [column["id"] for column in _filled(project)["열"]]
assert [i for i in ids if i.startswith("pp_")] == [
"pp_len|흄관|1000",
"pp_cp|흄관|1000",
"pp_len|파형강관|1000",
"pp_cp|파형강관|1000",
]
assert [i for i in ids if i.startswith("pv_")] == [
f"pv_{k}|20" for k in ("b", "l", "w", "a", "jt")
]
def test_계산_열은_설계값_딱지_없음(project: Path) -> None:
master = _master()
assert not [c["id"] for c in master["열"] if c.get("식") and c.get("바인딩")]
doc = _filled(project)
for col_id in ("pv_a|20", "pv_jt|20", "pp_cp|파형강관|1000", "bm_area|메쌓기|40~60|2.5"):
column = _col(doc, col_id)
assert "바인딩" not in column and column["펼침틀"]["원열"]["id"] == col_id.split("|")[0]
assert _col(doc, "pp_cp|파형강관|1000")["머리"] == ["관공", "파형강관 Φ1000", "커플링밴드"]
# 다시 저장 · 채워도 같은 열(계산 열도 원열로 되돌림)
again = _auto(project, fill.strip_design(doc), {85.0: 14.0})
assert [c["id"] for c in again["열"]] == [c["id"] for c in doc["열"]]
def test_계산_열_식은_같은_묶음_열로_바뀌고_식_없으면_빈_값(project: Path) -> None:
doc = _filled(project)
# 식을 지운 계산 열(pv_a · pv_jt · pp_cp) = 빈 값 — 값은 면적산출 알약 연결로 받음(PLAN 13장)
for col_id in ("pv_a|20", "pv_jt|20", "pp_cp|파형강관|1000", "pp_cp|흄관|1000"):
assert "식" not in _col(doc, col_id), col_id
assert all(
col_id not in row["값"] and col_id not in (row.get("식") or {}) for row in doc["줄"]
)
assert _col(doc, "bm_area|메쌓기|40~60|2.5")["식"] == (
'IF([bm_len|메쌓기|40~60|2.5]>0,[bm_len|메쌓기|40~60|2.5]*2.5,"")'
)
def test_측점_줄_같은_측점은_한_줄_관은_나눔(project: Path) -> None:
doc = _filled(project)
rows = {row["id"]: row for row in doc["줄"]}
assert doc["줄"][0]["고정"] == "전구간"
interval = rows["s80.00~90.00"]["값"]
assert interval["sta"] == "NO.4~NO.4+10"
assert interval["ms|찰쌓기|2.5"] == 10 and interval["ms|메쌓기|2.0"] == 10
first, second = rows["s85.05"]["값"], rows["s85.05#1"]["값"]
assert first["sta"] == second["sta"] == "NO.4+5.05"
assert first["pp_len|파형강관|1000"] == 14.0 and "pp_len|흄관|1000" not in first
assert second["pp_len|흄관|1000"] == 14.0 # 한 측점 횡단 관 연장은 하나 — 두 관이 같이 씀
assert first["rv|돌쌓기(찰)|1.5"] == second["rv|돌쌓기(찰)|1.5"] == 10
assert rows["s300.00"]["값"]["ps"] == 1
assert [row["값"].get("no") for row in doc["줄"][1:]] == [str(i) for i in range(1, 8)]
assert "식" not in rows["s200.00~230.00"] # 줄눈 간격이 달라도 열 식이 없으면 줄 식 없음
assert doc["알림"] == []
bare = _auto(project, _master(), {})
assert bare["알림"] == ["관 연장 없음 2곳 — B06 횡단 설계 전이면 빈칸"]
assert not any("pp_len" in key for row in bare["줄"] for key in row["값"])
def test_손_값과_전구간_줄은_다시_채워도_지킴(project: Path) -> None:
doc = _filled(project)
doc["줄"][0]["값"]["ps"] = 3
next(row for row in doc["줄"] if row["id"] == "s300.00")["값"]["h_intake"] = 2
doc["줄"].append({"id": "u1", "값": {"sta": "NO.9", "h_rockfall": 4}, "손": True})
doc["줄"].append({"id": "s999.00", "값": {"sta": "NO.49+19", "h_waste": 1.5}})
again = _filled(project, doc)
rows = {row["id"]: row for row in again["줄"]}
assert rows["all"]["값"]["ps"] == 3
assert rows["s300.00"]["값"]["h_intake"] == 2
assert rows["u1"]["값"]["h_rockfall"] == 4
assert rows["s999.00"]["손"] is True and rows["s999.00"]["값"]["h_waste"] == 1.5
assert [c["id"] for c in again["열"]] == [c["id"] for c in doc["열"]] # 두 번 펼치지 않음
def test_측점_표기() -> None:
assert fill.station_label(0) == "NO.0"
assert fill.station_label(38) == "NO.1+18"
assert fill.station_label(59.999) == "NO.3"
assert fill.station_label(2413.5) == "NO.120+13.5"
def test_저장은_양식과_손_값만_다시_채우면_제자리(project: Path) -> None:
master = _master()
doc = _filled(project)
rows = {row["id"]: row for row in doc["줄"]}
rows["s300.00"]["값"]["h_intake"] = 2
rows["s85.05#1"]["값"]["memo"] = "흄관 확인"
doc["줄"][0]["값"]["ps"] = 3
saved = fill.strip_design({**doc, "결과": {"계산": {}}})
assert [c["id"] for c in saved["열"]] == [
c["id"] for c in master["열"]
] # 펼침 규칙 열로 되돌림
assert "알림" not in saved and "결과" not in saved
assert saved["줄"] == [
{"id": "all", "값": {"sta": "전구간", "ps": 3}, "고정": "전구간"},
{"id": "s85.05#1", "값": {"sta": "NO.4+5.05", "memo": "흄관 확인"}},
{"id": "s300.00", "값": {"sta": "NO.15", "h_intake": 2}},
]
again = {row["id"]: row for row in _filled(project, saved)["줄"]}
assert again["s300.00"]["값"]["h_intake"] == 2 and again["s300.00"]["값"]["ps"] == 1
assert again["s85.05#1"]["값"]["memo"] == "흄관 확인"
assert again["s85.05#1"]["값"]["pp_len|흄관|1000"] == 14.0
def test_측점이_없어진_손_값은_맨_아래_비고_줄로(project: Path) -> None:
saved = fill.strip_design(_filled(project))
saved["줄"].append({"id": "s300.00", "값": {"sta": "NO.15", "h_intake": 2}})
saved["줄"].append({"id": "s555.00", "값": {"sta": "NO.27+15", "h_intake": 1}})
doc = _filled(project, saved)
last = doc["줄"][-1]
assert last["id"] == "s555.00" and last["손"] is True
assert last["값"] == {"sta": "NO.27+15", "h_intake": 1, "memo": "설계에서 측점 없어짐"}
kept = fill.strip_design(doc)
assert kept["줄"][-1]["값"]["h_intake"] == 1 # 다음 저장에도 남음
def test_집계_길이는_구간형이면_시작_종료가_먼저(project: Path) -> None:
# 노면공 87-2 — 범위를 고쳐도 옛 length_m 이 안 앞섬
moved = {"start_m": 200, "end_m": 245, "options": {"length_m": 30}}
assert fill._length(moved) == 45
assert fill._length({"options": {"length_m": 12}}) == 12 # 점형 · 구간 없음 = 입력 길이
assert fill._length({"options": {}}) is None