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>
287 lines
11 KiB
Python
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)
|