Files
Aislo/resources/tester/test_120_b08_drain_summary.py
T
eomsangdonandClaude Opus 5.5 ba6d7bfa31 Merge remote-tracking branch 'origin/sub_desktop_1' into sub_laptop_4
충돌 = test_120 물넘이 500 m 줄 — 양쪽 살림(ford_chute_1_* 셋 + summary_item) · 143-1 사본 규칙대로 gfordpavechute1 잇기 추가(브레인 승인)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
2026-10-09 15:12:20 +09:00

301 lines
12 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.
# -*- coding: utf-8 -*-
"""B08 구조물 집계표 「횡단배수」 관 밖 구조물 · 열 폭 — PLAN 120.
견본:
300 관 Φ800 L18 + BOX 2.0×2.0(같은 측점 · 키 겹침 막음) 본체 9.6 m · 유입 옛 「있음」 = 날개벽 ·
유출 흙막이 메 · 높이 빈칸 → 박스 높이 2.0
340 BOX 사용자 입력 2.5×1.5 · 본체 길이 없음(B06 [저장] 전) · 유출 날개벽
400 세월교 Φ1000×2련 월류 폭 12 · 유입 날개벽 · 유출 흙막이 찰(높이 빈칸 → 미입력) 3 + 4
500 물넘이 포장 월류 폭 8 · H=1.0
600 개거 + 노출형 횡단수로 · 700 맹암거(그룹 조합)
143-1 — 칸은 그 칸을 낸 조합 사본이 있을 때만 셈: 견본은 칸을 내는 잇기를 모두 두고 연결된 사본 =
지정 전부 · 사본 없으면 없음은 test_143_1
"""
from __future__ import annotations
import copy
import json
import re
import subprocess
from pathlib import Path
import pytest
from B06_Section.B06_Section_Schema import CrossSectionPatch
from B08_Quantity import B08_Quantity_Router_Summary as summary
from M02_MasterTemplete import M02_Item_Snapshot as snapshots
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
ROOT = Path(__file__).resolve().parents[2]
FIXTURES = Path(__file__).parent / "fixtures"
needs_node = pytest.mark.skipif(
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
)
def _item(key: str) -> dict:
return {"layer": "system", "owner": None, "key": key, "name": key}
POINTS = [
{"chainage_m": 300.0, "options": {"pipe_diameter_mm": 800}, "summary_item": _item("SS000100")},
{
"chainage_m": 300.0,
"facility": "box_culvert",
"options": {
"body_width_m": 2,
"body_height_m": 2,
"wing_in": "있음",
"wing_out": "흙막이",
"outlet_revet_form": "돌쌓기(메)",
"outlet_revet_before_m": 5,
"outlet_revet_after_m": 5,
},
"summary_item": _item("SS000101"),
"group_items": {
"gboxbody": _item("SS000102"),
"gboxin": _item("SS000106"),
"gboxout": _item("SS000107"),
},
},
{
"chainage_m": 340.0,
"facility": "box_culvert",
"options": {
"body_width_m": 2.5,
"body_height_m": 1.5,
"wing_in": "없음",
"wing_out": "날개벽",
},
"summary_item": _item("SS000101"),
"group_items": {"gboxout": _item("SS000107")},
},
{
"chainage_m": 400.0,
"facility": "ford_bridge",
"options": {
"pipe_diameter_mm": 1000,
"pipe_count": 2,
"ford_width_m": 12,
"inlet_type": "날개벽",
"wing_in": "있음",
"outlet_type": "흙막이",
"wing_out": "없음",
"outlet_revet_form": "돌쌓기(찰)",
"outlet_revet_before_m": 3,
"outlet_revet_after_m": 4,
},
"group_items": {
"gford": _item("SS000103"),
"gfordin": _item("SS000104"),
"gfordout": _item("SS000108"),
},
},
{
"chainage_m": 500.0,
"facility": "ford_pavement",
"options": {
"ford_width_m": 8,
"ford_height_m": 1.0,
# 물넘이 포장 도수로/돌붙임(136-2) — 관 · 박스 · 세월교와 같은 열
"ford_chute_1_structure": "돌붙임",
"ford_chute_1_bond": "찰붙임",
"ford_chute_1_area": 12,
},
"summary_item": _item("SS000109"),
"group_items": {"gfordpavechute1": _item("SS000112")},
},
]
STRUCTURES = [
{
"structure_id": "s-ditch",
"type_id": "open_ditch",
"placement": "point",
"chainage_m": 600,
"summary_item": _item("SS000110"),
},
{
"structure_id": "s-exposed",
"type_id": "cross_drain_exposed",
"placement": "point",
"chainage_m": 600,
"summary_item": _item("SS000111"),
},
{
"structure_id": "s-ud",
"type_id": "cross_underdrain",
"placement": "point",
"chainage_m": 700,
"group_items": {"gcrossud": _item("SS000105")},
},
]
DESIGNS = [
{"chainage_m": 300.0, "design": {"pipe_length_m": 18, "box_length_m": 9.6, "inlet_basin": ""}},
{"chainage_m": 340.0, "design": {}},
]
@pytest.fixture
def out(tmp_path: Path) -> dict:
root = tmp_path / "proj"
edits = root / "B04_PreProcess/drainage/edits"
edits.mkdir(parents=True)
points = [{**p, "source": "user"} for p in POINTS]
(edits / "pipe_points.json").write_text(
json.dumps({"points": points}, ensure_ascii=False), encoding="utf-8"
)
route = root / "B05_Profile/route"
route.mkdir(parents=True)
(route / "structures.json").write_text(
json.dumps({"revision": 1, "structures": STRUCTURES}, ensure_ascii=False),
encoding="utf-8",
)
names = definition.read()["종류"]["pipe"]["이름"]
linked = {owner: {} for owner in snapshots.assignments(root)} # 지정 전부 사본 있음
return summary.pipe_summary(root, DESIGNS, names, 6.0, linked=linked)
def _cell(out: dict, at: float, cid: str):
row = next(r for r in out["rows"] if abs(r["chainage_m"] - at) < 0.01)
return row["values"].get(cid)
def _col(out: dict, cid: str) -> dict:
return next(c for c in out["columns"] if c["id"] == cid)
def test_같은_측점_관_박스_따로(out):
assert _cell(out, 300, "pipe:len|800") == 18
assert _cell(out, 300, "pipe:box|body|2.0×2.0") == 9.6
def test_박스_본체_날개벽_흙막이(out):
body = _col(out, "pipe:box|body|2.0×2.0")
assert body["group"] == ["BOX암거", "2.0 × 2.0", "본체"] and body["label"] == "m"
assert body["combos"] == ["system||SS000101", "system||SS000102"]
assert _cell(out, 300, "pipe:box_culvert|wing|inlet") == 1 # 옛 「있음」 = 날개벽
assert _cell(out, 300, "pipe:box_culvert|wing|outlet") is None # 유출은 흙막이
wing = _col(out, "pipe:box_culvert|wing|inlet")
assert wing["group"] == ["BOX암거", "날개벽", "유입"] and wing["label"] == "개소"
# 흙막이 빈 높이 = 박스 높이 · 공용 흙막이 열
assert _cell(out, 300, "pipe:revet|돌쌓기(메)|2.0") == 10
# 사용자 입력 규격 · 본체 길이 없음 = 빈칸 + 알림
assert _col(out, "pipe:box|body|2.5×1.5")["group"][1] == "2.5×1.5"
assert _cell(out, 340, "pipe:box|body|2.5×1.5") is None
assert _cell(out, 340, "pipe:box_culvert|wing|outlet") == 1
assert _cell(out, 340, "pipe:box_culvert|wing|inlet") is None
assert any("BOX 본체 길이 없음 1곳" in n for n in out["notes"])
def test_세월교_월류폭_개소_날개벽_흙막이_미입력(out):
width = _col(out, "pipe:ford|1000|2|width")
assert width["group"] == ["세월교", "Φ1000×2련", "월류 폭"] and width["label"] == "m"
assert width["combos"] == ["system||SS000103"] # 정의 그룹 밖 잇기 = 저장된 잇기에서
assert _cell(out, 400, "pipe:ford|1000|2|width") == 12
assert _cell(out, 400, "pipe:ford|1000|2|count") == 1
assert _col(out, "pipe:ford|1000|2|count")["label"] == "개소"
assert _cell(out, 400, "pipe:ford_bridge|wing|inlet") == 1
assert _col(out, "pipe:ford_bridge|wing|inlet")["combos"] == ["system||SS000104"]
assert _cell(out, 400, "pipe:ford_bridge|wing|outlet") is None
soil = _col(out, "pipe:revet|돌쌓기(찰)|-")
assert soil["group"] == ["흙막이", "돌 쌓기(찰)", "미입력"]
assert _cell(out, 400, "pipe:revet|돌쌓기(찰)|-") == 7
def test_물넘이_H별_월류폭(out):
pave = _col(out, "pipe:pave|1.0")
assert pave["group"] == ["물넘이포장", "월류 폭", "H=1.0"] and pave["label"] == "m"
assert _cell(out, 500, "pipe:pave|1.0") == 8
assert _cell(out, 500, "pipe:chute|찰붙임") == 12
def test_개거_노출형_맹암거_개소(out):
assert _cell(out, 600, "pipe:open_ditch") == 1
assert _cell(out, 600, "pipe:cross_drain_exposed") == 1
assert _cell(out, 700, "pipe:cross_underdrain") == 1
assert _col(out, "pipe:open_ditch")["group"] == ["개거(겉도랑)"]
assert _col(out, "pipe:cross_underdrain")["combos"] == ["system||SS000105"]
def test_차례_계_종류(out):
ids = [c["id"] for c in out["columns"]]
order = [
"pipe:revet|돌쌓기(찰)|-",
"pipe:revet|돌쌓기(메)|2.0",
"pipe:len|800",
"pipe:box|body|2.0×2.0",
"pipe:box|body|2.5×1.5",
"pipe:box_culvert|wing|inlet",
"pipe:ford|1000|2|width",
"pipe:ford|1000|2|count",
"pipe:ford_bridge|wing|inlet",
"pipe:pave|1.0",
"pipe:cross_drain_exposed",
"pipe:open_ditch",
"pipe:cross_underdrain",
]
assert [ids.index(i) for i in order] == sorted(ids.index(i) for i in order)
assert out["total"]["pipe:box_culvert|wing|outlet"] == 1
assert set(out["types"]) == {
"pipe",
"box_culvert",
"ford_bridge",
"ford_pavement",
"cross_drain_exposed",
"open_ditch",
"cross_underdrain",
}
def _width(*texts: str) -> int:
source = (ROOT / "B08_Quantity/B08_Quantity_Summary_Data.ts").read_text(encoding="utf-8")
source = re.sub(r"^import[^;]*;\n", "", source, flags=re.MULTILINE)
script = f"{source}\nconsole.log(widthOf(...{json.dumps(texts, ensure_ascii=False)}));"
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return int(result.stdout)
def test_열_폭_맨_아래_단_글만_44_100():
assert _width("m") == 44 # 최소
assert _width("관매설") == 52 # 한글 12 × 3 + 16
assert _width("H=2.0") == 51 # 7 × 5 + 16
assert _width("m", "1,234.56") == 72 # 칸 글이 길면 칸 글
assert _width("흙막이 돌쌓기(메) 아주 긴 조합 이름") == 100 # 최대 · 머리는 줄바꿈
def test_화면_조합_열_빼기_폭_머리_줄바꿈():
ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
assert "served.has(point.facility" in ui and "served.has(item.type_id)" in ui
assert "widthOf(column.label, ...texts)" in ui and "widthOf(column.name)" in ui
assert "widthOf(stationHead, ...labels)" in ui
css = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.css").read_text(encoding="utf-8")
assert '[data-b08-summary="table"] thead th' in css and "white-space: normal" in css
def test_패치가_박스_길이를_받는다():
assert CrossSectionPatch(chainage_m=1, box_length_m=9.6).box_length_m == 9.6
with pytest.raises(ValueError):
CrossSectionPatch(chainage_m=1, box_length_m=-1)
@needs_node
def test_서버가_박스_본체_길이를_주인_측점에만():
from B06_Section.B06_Section_Server_Calc_Prebuild import _NPM_SCRIPT, BUNDLE
from common_util.common_util_node_bundle import run_bundle_json
data = json.loads((FIXTURES / "b06_73_4_body_guard_sections.json").read_text("utf-8"))
sections = data["cross_sections"]
box_at = next(s for s in sections if s.get("box"))
neighbour = next(s for s in sections if s["chainage_m"] == 340)
neighbour["box"] = copy.deepcopy(box_at["box"])
output = run_bundle_json(BUNDLE, _NPM_SCRIPT, {"detail": {"cross_sections": sections}})
rows = {round(r["chainage_m"], 3): r for r in output["areas"]}
assert rows[352.14]["box_length_m"] > 0
assert "box_length_m" not in rows[340]