Files
Aislo/resources/tester/test_b08_earthwork_summary.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

287 lines
11 KiB
Python

"""토공집계표·프로젝트 설정 검사 — PLAN 8-11·8-13·8-7.
여기서 못 박는 것 셋
· 암 갈래 **개수를 코드에 안 박음** — 공사마다 다르다(울진 2 · 거창 5 · BOM 1).
· 반영률 **기본 100 %** — 실무 관측 80/50/80 은 기본값이 아니다(★법대로).
· 무대는 **집계에는 오르되 내역 줄이 아니다** — 품셈 1-2-7.
"""
from __future__ import annotations
import json
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_EarthworkSummary import ( # noqa: E402
SummaryInput,
build_rows,
build_table,
haul_check,
totals_by_unit,
)
from common_util.common_util_project_settings import ( # noqa: E402
APPLICATION_RATIO_KEYS,
ROCK_CLASS_SETS,
application_ratio,
default_settings,
load_settings,
quantity_settings,
rock_classes,
save_section,
)
def 토적표합계() -> dict[str, float]:
return {
"cut_soil_volume_m3": 2526.99,
"cut_rock_volume_m3": 3512.09,
"ditch_soil_volume_m3": 90.51,
"ditch_rock_volume_m3": 101.22,
"adjusted_total_m3": 6511.06,
"fill_volume_m3": 16836.47,
"diverted_m3": 5801.21,
}
def 사면합계() -> dict[str, float]:
return {
"face_dressing_fill": 13518.6,
"face_dressing_cut": 5433.7,
"tree_removal_fill": 13518.6,
"tree_removal_cut": 5433.7,
"bench_cut_fill": 13518.6,
}
# ── 암 갈래 — 개수를 코드에 박지 않는다 ─────────────────────────────
def test_비율이_없으면_암을_쪼개지_않음() -> None:
"""지어낸 비율로 나누지 않는다 — 「암」 한 줄로 낸다."""
rows = build_rows(
SummaryInput(
earthwork_totals=토적표합계(),
rock_classes=list(ROCK_CLASS_SETS["geochang5"]),
rock_ratios_pct={},
)
)
cut = [r for r in rows if r.group == "흙깎기"]
assert [r.item for r in cut] == ["토사", "암"]
assert cut[1].amount == pytest.approx(3512.09)
def test_울진2갈래() -> None:
rows = build_rows(
SummaryInput(
earthwork_totals=토적표합계(),
rock_classes=list(ROCK_CLASS_SETS["uljin2"]),
rock_ratios_pct={"연암": 20, "발파암": 80},
)
)
cut = [r for r in rows if r.group == "흙깎기"]
assert [r.item for r in cut] == ["토사", "연암", "발파암"]
assert cut[1].amount == pytest.approx(3512.09 * 0.2)
assert cut[2].amount == pytest.approx(3512.09 * 0.8)
def test_거창5갈래() -> None:
"""같은 코드가 갈래 수만 바꿔 선다 — 개수를 박지 않았다는 증거."""
rows = build_rows(
SummaryInput(
earthwork_totals=토적표합계(),
rock_classes=list(ROCK_CLASS_SETS["geochang5"]),
rock_ratios_pct={"풍화암": 10, "연암": 60, "보통암": 25, "경암": 5},
)
)
cut = [r for r in rows if r.group == "흙깎기"]
assert [r.item for r in cut] == ["토사", "풍화암", "연암", "보통암", "경암"]
assert sum(r.amount for r in cut[1:]) == pytest.approx(3512.09)
def test_비율_합이_100이_아니어도_총량은_보존() -> None:
"""설계자가 60/30 만 넣어도 암 총량이 새면 안 된다 — 준 비율끼리 안분한다."""
rows = build_rows(
SummaryInput(
earthwork_totals=토적표합계(),
rock_classes=list(ROCK_CLASS_SETS["uljin2"]),
rock_ratios_pct={"연암": 60, "발파암": 30},
)
)
cut = [r for r in rows if r.group == "흙깎기"]
assert sum(r.amount for r in cut[1:]) == pytest.approx(3512.09)
# 60/30 이 실제로는 66.7/33.3 으로 돈다 — 값이 말없이 바뀌므로 비고에 드러낸다.
assert cut[1].amount == pytest.approx(3512.09 * 60 / 90)
assert "90" in cut[1].note and "안분" in cut[1].note
def test_비율_합이_100이면_비고가_비어_있음() -> None:
"""제대로 넣었는데 안내가 뜨면 잡음이 된다."""
rows = build_rows(
SummaryInput(
earthwork_totals=토적표합계(),
rock_classes=list(ROCK_CLASS_SETS["uljin2"]),
rock_ratios_pct={"연암": 20, "발파암": 80},
)
)
cut = [r for r in rows if r.group == "흙깎기"]
assert all(r.note == "" for r in cut[1:])
# ── 반영률 — 기본 100 % ─────────────────────────────────────────
def test_반영률_기본은_100퍼센트() -> None:
rows = build_rows(SummaryInput(earthwork_totals=토적표합계(), slope_totals=사면합계()))
compaction = next(r for r in rows if r.group == "성토면다짐")
assert compaction.amount == pytest.approx(13518.6)
assert compaction.note == "" # 기본이면 비고에 아무것도 안 적는다
def test_반영률을_주면_곱해지고_비고에_남음() -> None:
"""실무 시트가 비고란에 적던 그 자리다 — 누가 정한 값인지 보이게 한다."""
rows = build_rows(
SummaryInput(
earthwork_totals=토적표합계(),
slope_totals=사면합계(),
application_ratios={"fill_slope_compaction": 0.8},
)
)
compaction = next(r for r in rows if r.group == "성토면다짐")
assert compaction.amount == pytest.approx(13518.6 * 0.8)
assert "80" in compaction.note
def test_초류종자살포는_성토_절토_따로() -> None:
rows = build_rows(
SummaryInput(
earthwork_totals=토적표합계(),
slope_totals=사면합계(),
application_ratios={"seed_spray_fill": 0.5, "seed_spray_cut": 1.0},
)
)
seed = next(r for r in rows if r.group == "초류종자살포")
assert seed.amount == pytest.approx(13518.6 * 0.5 + 5433.7)
# ── 무대 — 집계에는 오르되 내역 줄이 아니다 ──────────────────────
def test_무대는_내역줄이_아님() -> None:
"""품셈 1-2-7 — 소운반 20m 는 품에 포함이라 붙일 단가가 없다."""
source = SummaryInput(
earthwork_totals=토적표합계(),
haul_rows=[
{"equipment": "free_haul", "ground": "토사", "volume_m3": 871, "average_distance_m": 11.94},
{"equipment": "dozer", "ground": "토사", "volume_m3": 1170, "average_distance_m": 43.66},
{"equipment": "dump_truck", "ground": "암", "volume_m3": 1714, "average_distance_m": 318.6},
],
)
rows = build_rows(source)
free = next(r for r in rows if r.group.startswith("무대"))
assert free.in_bill is False
assert "내역 제외" in free.note
assert free.amount == pytest.approx(871)
assert all(r.in_bill for r in rows if r.group in ("도자운반", "덤프운반"))
def test_무대를_내되_검산에_씀() -> None:
"""무대를 아예 안 내면 `무대+도자+덤프 = 총 운반토량` 검산이 죽는다."""
source = SummaryInput(
earthwork_totals=토적표합계(),
haul_rows=[
{"equipment": "free_haul", "volume_m3": 1000},
{"equipment": "dozer", "volume_m3": 2000},
{"equipment": "dump_truck", "volume_m3": 2801.21},
],
)
check = haul_check(source, 토적표합계())
assert check["hauled_total_m3"] == pytest.approx(5801.21)
assert check["difference_m3"] == pytest.approx(0.0, abs=0.01)
def test_평균운반거리가_비고에_남음() -> None:
rows = build_rows(
SummaryInput(
earthwork_totals=토적표합계(),
haul_rows=[{"equipment": "dozer", "ground": "토사", "volume_m3": 1170, "average_distance_m": 43.66}],
)
)
dozer = next(r for r in rows if r.group == "도자운반")
assert "43.66" in dozer.note
# ── 표 모양 ─────────────────────────────────────────────────────
def test_열_구성은_실무_시트_그대로() -> None:
table = build_table(SummaryInput(earthwork_totals=토적표합계()))
assert table["columns"] == ["구분", "공종", "규격", "단위", "계", "비고"]
def test_단위를_섞어_더하지_않음() -> None:
rows = build_rows(SummaryInput(earthwork_totals=토적표합계(), slope_totals=사면합계()))
totals = totals_by_unit(rows)
assert set(totals) == {"㎥", "㎡"}
# ── 프로젝트 설정 ────────────────────────────────────────────────
def test_기본설정_모양(tmp_path: Path) -> None:
settings = default_settings()
assert settings["schema_version"] == 1
assert set(settings) == {"schema_version", "quantity", "estimation"}
quantity = settings["quantity"]
# override 는 기본이 None — 「안 정했으면 config 정본을 쓴다」는 뜻.
assert quantity["conversion_factors_override"] is None
assert quantity["haul_limits_m_override"] is None
# 반영률 기본 100. 실무 관측 80/50/80 을 넣지 않는다.
assert quantity["application_ratios_pct"] == {key: 100 for key in APPLICATION_RATIO_KEYS}
# estimation 은 자리만 — 채우는 것은 B09 몫.
# ⚠ 「연도」가 아니라 **판**을 가리킨다 — 제비율은 연중에도 개정된다(현행판 2026-04-13).
assert settings["estimation"]["rate_dataset"] is None
assert "rate_year" not in settings["estimation"]
def test_파일이_없으면_기본값(tmp_path: Path) -> None:
assert load_settings(tmp_path) == default_settings()
def test_깨진_파일이어도_화면은_서야_함(tmp_path: Path) -> None:
(tmp_path / "project_settings.json").write_text("{ 망가짐", encoding="utf-8")
assert load_settings(tmp_path) == default_settings()
def test_한_구획만_갈아끼움(tmp_path: Path) -> None:
"""두 페이지가 같은 파일을 쓴다 — 통째로 덮으면 상대 값이 사라진다."""
save_section(tmp_path, "estimation", {"rate_dataset": {"dataset_id": "rates", "effective_date": "2026-04-13"}})
save_section(tmp_path, "quantity", {"rock_class_set": "uljin2"})
stored = json.loads((tmp_path / "project_settings.json").read_text(encoding="utf-8"))
# 남의 구획이 살아 있다 — B08 이 저장해도 B09 값이 안 지워진다.
assert stored["estimation"]["rate_dataset"]["effective_date"] == "2026-04-13"
assert stored["quantity"]["rock_class_set"] == "uljin2"
def test_모르는_구획은_거부(tmp_path: Path) -> None:
with pytest.raises(ValueError):
save_section(tmp_path, "hacked", {})
def test_암갈래는_세트에서(tmp_path: Path) -> None:
save_section(tmp_path, "quantity", {"rock_class_set": "uljin2", "rock_classes": None})
settings = quantity_settings(tmp_path)
settings["rock_classes"] = None
assert rock_classes(settings) == list(ROCK_CLASS_SETS["uljin2"])
def test_반영률_읽기() -> None:
settings = default_settings()["quantity"]
assert application_ratio(settings, "obstacle_removal") == pytest.approx(1.0)
settings["application_ratios_pct"]["obstacle_removal"] = 80
assert application_ratio(settings, "obstacle_removal") == pytest.approx(0.8)