Files
Aislo/resources/tester/test_b08_preparation.py
T
eomsangdonandClaude Opus 5 d7cb14f416 test(B08): tmp/tests 중 tester 에 없던 47 개를 resources/tester 로 옮김
tmp/ 가 창끼리 안 건너가는 것이 확정돼(랩탑이 시간 두고 두 번 확인) 시험·예외가
저절로 건너가도록 git 안으로 옮김. 사용자 확정.

- 내용은 하나도 안 고침 — 자리만 옮김. tmp/tests 는 남겨 둠.
- 같은 이름이 이미 있던 64 개는 랩탑 것을 그대로 두고 건너뜀.
- helper_b05_*.js 둘은 랩탑이 .cjs 로 이미 올린 것과 **줄바꿈만 다른 같은 내용**이라
  복사본을 도로 뺌(시험이 .cjs 를 부름).
- resources/tester/ 에서 전체 1176 통과 · 29 건너뜀 · 실패 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:15:57 +09:00

233 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.
"""준비공·사방공 검사 — PLAN 8-3 의 ❌ 둘 (2026-09-07 ⑪).
여기서 하는 일은 **줄을 세우고 못 서는 줄의 사유를 적는 것**이다. 빈 표를 내면
「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다.
⚠⚠ 벌목은 이미 토공집계의 「지장목제거」로 선다 — 여기서 또 세우면 같은 나무를 두 번 벤다.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_Preparation import ( # noqa: E402
EROSION_CONTROL_TYPES,
STATUS_COUNTED_ELSEWHERE,
STATUS_NOT_APPLICABLE,
STATUS_PENDING,
build_table,
erosion_rows,
)
def (table: dict, item: str) -> dict:
return next(row for row in table["rows"] if row["item"] == item)
# ── 이중계상 방어 ───────────────────────────────────────────────────
def test_벌목은_값을_내지_않고_가리키기만_함() -> None:
"""⚠ 이 파일의 핵심 — 토공집계의 「지장목제거」와 겹치면 같은 나무를 두 번 벤다."""
table = build_table({"tree_removal_fill": 1000.0, "tree_removal_cut": 500.0})
row = (table, "벌목·지장목제거")
assert row["amount"] is None # 값은 안 낸다
assert row["status"] == STATUS_COUNTED_ELSEWHERE
assert "이중계상" in row["reason"]
# 참고 면적은 보인다 — 어느 값과 겹치는지 사람이 알아야 한다.
assert row["reference_amount"] == pytest.approx(1500.0)
# ── 못 서는 줄은 사유를 적는다 ─────────────────────────────────────
def test_못_서는_줄이_목록에_남을것() -> None:
"""빈 표를 내면 「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다."""
table = build_table()
items = [row["item"] for row in table["rows"]]
assert "표토제거" in items and "제근·뿌리다듬기" in items
# 규준틀은 둘로 갈렸다 — 비탈(11-2)과 수평(11-3)의 설치 기준이 다르다.
assert "비탈 규준틀" in items and "수평 규준틀" in items
def test_사유가_비어_있지_않을것() -> None:
for row in build_table()["rows"]:
assert row["reason"], f"{row['item']} 에 사유가 없다"
def test_값이_없어도_공종코드는_알려줄것() -> None:
"""무엇으로 서게 될 줄인지 알아야 나중에 잇는다."""
assert (build_table(), "표토제거")["work_item_code"] == "FP-09-15"
# ── 사방공 ──────────────────────────────────────────────────────────
def test_사방_시설이_없으면_해당_없음() -> None:
"""있는 것처럼 0 을 적지 않는다."""
row = (build_table(structures=[{"type_id": "masonry_wet"}]), "사방 시설")
assert row["status"] == STATUS_NOT_APPLICABLE
assert row["amount"] is None
def test_사방_시설이_있으면_줄이_섬() -> None:
table = build_table(structures=[{"type_id": "erosion_check"}, {"type_id": "revetment"}])
사방 = [row for row in table["rows"] if row["group"] == "사방공"]
# 키는 `type_id` 칸으로 오가고, 화면에 뜨는 `item` 은 사람 이름이다.
assert [row["type_id"] for row in 사방] == ["erosion_check", "revetment"]
assert [row["item"] for row in 사방] == ["골막이", "기슭막이"]
assert all(row["status"] == STATUS_PENDING for row in table["rows"] if row["group"] == "사방공")
def test_사방_종류_이름이_레지스트리와_맞을것() -> None:
"""⚠ 이름을 지어내면 영영 안 걸린다 — 레지스트리의 실제 `type_id` 여야 한다."""
import json
registry = json.loads(
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
)
types = registry if isinstance(registry, list) else (registry.get("types") or [])
known = {t["type_id"] for t in types}
assert EROSION_CONTROL_TYPES <= known, EROSION_CONTROL_TYPES - known
def test_셈이_맞을것() -> None:
table = build_table()
assert table["row_count"] == len(table["rows"])
assert table["pending_count"] == sum(1 for r in table["rows"] if r["status"] == STATUS_PENDING)
# ── 화면에 안 보이던 확정 항목을 드러내기 (2026-09-07) ─────────────
#
# ⚠ 목록에만 있고 화면에 없으면 **사용자는 그것이 잠정인 줄도 모른다.**
# 「확인 필요」만 있어도 안 된다 — **지금 무슨 값으로 돌고 있는지**를 함께 적어야
# 사용자가 무엇을 정할지 안다(원단위 미확보에서 「표에 있는 규격을 함께 알린」 그 방식).
def test_지장목제거_줄에_공종_미확정_사유가_적힐것() -> None:
row = (build_table(), "벌목·지장목제거")
assert "공종 미확정" in row["reason"]
assert "수확베기" in row["reason"] # 후보가 무엇인지도 함께 보인다
# ── 규준틀 개소 — 원문이 기준을 정해 둠 (2026-09-07 ㉒) ────────────
#
# 11-2 [주]① 「비탈길이 10m 이상 20m마다 설치한다」
# 11-3 [주]① 「중심점에서 성토 높이 5m 이상에 설치한다」
# ⚠ 재료량은 [주]④ 「설계수량에 따른다」 — **개소만 내고 재료는 미확보**.
def 사면줄(*pairs: tuple[float, float]) -> list[dict]:
"""(그 측점의 사면길이, 앞 측점까지 거리) 목록."""
return [
{"lengths": {"face_dressing_fill": length}, "distance_m": dist} for length, dist in pairs
]
def test_비탈길이_10m_이상_구간만_셀것() -> None:
"""⚠ 「10m 이상」은 **비탈길이** 조건이고 「20m마다」는 **노선 거리** 간격이다.
둘을 섞으면 짧은 사면 구간까지 세어 개소가 부푼다."""
from B08_Quantity.B08_Quantity_Engine_Preparation import batter_frame_count
# 사면길이 12m 인 구간 40m + 사면길이 3m 인 구간 100m → 40m 만 센다.
count, notes = batter_frame_count(사면줄((12.0, 20.0), (12.0, 20.0), (3.0, 100.0)))
assert count == 3 # 40 ÷ 20 + 1
assert "11-2" in notes[0]
def test_긴_사면이_없으면_안_설것() -> None:
from B08_Quantity.B08_Quantity_Engine_Preparation import batter_frame_count
count, notes = batter_frame_count(사면줄((4.0, 100.0)))
assert count == 0
assert notes
def test_비탈_규준틀이_표에_값으로_설것() -> None:
# ⚠ 실제 사면표는 **첫 측점의 distance_m 이 0** 이다(앞 측점이 없다). 그 모양으로 넣는다.
table = build_table(slope_rows=사면줄((12.0, 0.0), (12.0, 20.0)))
row = (table, "비탈 규준틀")
assert row["amount"] == 2.0 # 20 ÷ 20 + 1
assert row["status"] == "값 있음"
# 재료는 여전히 미확보임을 같은 줄에 적는다.
assert "설계수량" in row["reason"]
def test_수평_규준틀은_성토고_칸이_없으면_못_셈() -> None:
"""⚠ 0 으로 때우지 않는다 — 「없음」과 「못 셈」은 다르다."""
row = (build_table(slope_rows=[{"lengths": {}, "distance_m": 20.0}]), "수평 규준틀")
assert row["amount"] is None
assert "성토고" in row["reason"] and "11-3" in row["reason"]
def test_수평_규준틀은_성토고_5m_이상_측점마다() -> None:
"""⚠ 비탈규준틀(「10m 이상 20m마다」)과 **기준이 다르다** — 이쪽은 **지점 조건**이라
간격이 없다. 두 기준을 같은 식으로 쓰면 조용히 틀린다."""
rows = [
{"fill_height_m": 6.0, "distance_m": 20.0},
{"fill_height_m": 2.0, "distance_m": 20.0},
{"fill_height_m": 5.0, "distance_m": 20.0},
]
row = (build_table(slope_rows=rows), "수평 규준틀")
assert row["amount"] == 2.0 # 6.0 · 5.0 두 곳(5.0 은 「이상」이라 든다)
assert row["status"] == "값 있음"
def test_성토고가_다_낮으면_0개소() -> None:
"""0 이 **값**인 자리다 — 「못 셈」과 달리 「없다」가 맞는 답이다."""
row = (build_table(slope_rows=[{"fill_height_m": 1.0, "distance_m": 20.0}]), "수평 규준틀")
assert row["amount"] == 0.0
assert row["status"] == "값 있음"
# ── 표토제거 두께 — 품셈이 아니라 설계가 정함 (2026-09-07 ㉓) ───────
#
# 9-15 [주]② 「Q=Q1/T … T : 표토두께(m)」 — **공식의 입력 변수**다.
# ⚠ 안 넣으면 0 으로 때우지 않고 물량을 안 낸다.
def test_두께가_없으면_물량을_안_낼것() -> None:
row = (build_table({"face_dressing_fill": 1000.0, "face_dressing_cut": 500.0}), "표토제거")
assert row["amount"] is None
# ⚠ 사유가 정정됐다(2026-09-09) — 종전에 「품셈이 정하는 값이 아닙니다」라고만 적었는데
# 품셈 9-15-2(답외구간) 원문에 **T=0.2m · L=20m 가 적용값으로 박혀 있다.** 두께를
# 자동으로 넣지는 않되 **원문에 있는 값을 없다고 말하지도 않는다.**
assert "두께는 설계가 정하는 값" in row["reason"]
assert "T=0.2m" in row["reason"] and "9-15-2" in row["reason"]
# 대상 면적은 참고로 보인다 — 무엇에 곱할지 사용자가 알아야 한다.
assert row["reference_amount"] == pytest.approx(1500.0)
def test_두께를_넣으면_물량이_설것() -> None:
table = build_table(
{"face_dressing_fill": 1000.0, "face_dressing_cut": 500.0}, topsoil_thickness_m=0.15
)
row = (table, "표토제거")
assert row["amount"] == pytest.approx(225.0) # 1,500㎡ × 0.15m
assert row["status"] == "값 있음"
def test_두께가_0이면_안_낼것() -> None:
"""0 은 「안 넣음」과 같게 본다 — 0㎥ 를 내면 「없다」로 오해된다."""
row = (build_table({"face_dressing_fill": 1000.0}, topsoil_thickness_m=0), "표토제거")
assert row["amount"] is None
def test_erosion_row_uses_registry_name_not_type_id():
"""사방공 줄 이름에 개발자 키가 새지 않는다 (2026-09-08 ㉕ 통과에서 `soil_guard` 로 뜬 자리)."""
rows = erosion_rows([{"type_id": "soil_guard"}], {"soil_guard": "흙막이"})
assert [row["item"] for row in rows] == ["흙막이"]
assert rows[0]["type_id"] == "soil_guard"
def test_erosion_row_falls_back_to_wording_table_without_registry():
"""이름표가 없어도 문구표가 받아 준다 — 없으면 키를 보이되 지어내지 않는다."""
assert erosion_rows([{"type_id": "soil_guard"}])[0]["item"] == "흙막이"
assert erosion_rows([{"type_id": "erosion_check"}])[0]["item"] == "골막이"