⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt` 가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는 시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음. - `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침** - `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시) - 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q` 옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0 ⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
169 lines
6.6 KiB
Python
169 lines
6.6 KiB
Python
"""B05 횡단배수 최소고 강제 스위치(2026-09-01 사용자 지시).
|
|
|
|
배수 자리가 종단 계획고를 잡아당겨 제어가 어렵다는 지적에 따라, 최소고 적용을
|
|
`enforce_pipe_clearance` 하나로 여닫게 했다. **기본은 해제**다. 여기서는
|
|
① 기본이 해제인지
|
|
② 요청·저장값이 스위치를 켜는지
|
|
③ 켜고 끔에 따라 계획선 표고가 실제로 달라지는지 (산식은 그대로)
|
|
④ 화면(TS) 편집 가드도 같은 스위치를 보는지
|
|
를 확인한다. 산식 자체(관경+토피 등)는 `test_b05_min_cover_rules.py`가 잠근다.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from B05_Profile.B05_Profile_Engine_Grade import resolve_grade_options # noqa: E402
|
|
from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402
|
|
design_pipe_anchored_profile,
|
|
)
|
|
from B05_Profile.B05_Profile_Schema import RouteSolveRequest # noqa: E402
|
|
|
|
|
|
def _options(**kwargs):
|
|
return resolve_grade_options("trunk", **kwargs)
|
|
|
|
|
|
# ── ① 기본 해제 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_default_is_released():
|
|
"""기본은 해제 — 자동 전처리가 배수 자리마다 계획고를 들어 올리지 않는다."""
|
|
assert _options().enforce_pipe_clearance is False
|
|
|
|
|
|
def test_as_dict_carries_the_switch():
|
|
"""저장·복원 경로(criteria/grade_options)에 스위치가 실린다."""
|
|
assert _options().as_dict()["enforce_pipe_clearance"] is False
|
|
assert (
|
|
_options(requested={"enforce_pipe_clearance": True}).as_dict()["enforce_pipe_clearance"]
|
|
is True
|
|
)
|
|
|
|
|
|
# ── ② 요청 → 저장 → 기본 순서 ────────────────────────────────────────────────
|
|
|
|
|
|
def test_requested_turns_it_on():
|
|
assert _options(requested={"enforce_pipe_clearance": True}).enforce_pipe_clearance is True
|
|
|
|
|
|
def test_stored_is_used_when_request_is_silent():
|
|
"""재생성처럼 요청이 비어 오는 경로는 확정 당시 저장값을 따른다."""
|
|
assert _options(stored={"enforce_pipe_clearance": True}).enforce_pipe_clearance is True
|
|
|
|
|
|
def test_request_wins_over_stored():
|
|
"""사용자가 방금 끈 것이 옛 저장값을 이긴다."""
|
|
resolved = _options(
|
|
requested={"enforce_pipe_clearance": False},
|
|
stored={"enforce_pipe_clearance": True},
|
|
)
|
|
assert resolved.enforce_pipe_clearance is False
|
|
|
|
|
|
def test_schema_defaults_to_none_and_ships_the_flag():
|
|
"""스키마 기본은 None(미지정) — 저장값을 덮어쓰지 않는다."""
|
|
request = RouteSolveRequest(
|
|
filter_key="f",
|
|
bp={"x": 0.0, "y": 0.0},
|
|
ep={"x": 100.0, "y": 0.0},
|
|
)
|
|
assert request.enforce_pipe_clearance is None
|
|
assert "enforce_pipe_clearance" in request.grade_options()
|
|
|
|
|
|
# ── ③ 계획선 표고가 실제로 달라진다 ──────────────────────────────────────────
|
|
|
|
|
|
def _flat_longitudinal(length_m: float = 400.0, step: float = 20.0) -> dict:
|
|
"""지반고가 일정한 종단 — 최소고 적용 여부만 표고 차이로 드러난다."""
|
|
chainages = np.arange(0.0, length_m + step, step)
|
|
return {
|
|
"length_m": float(length_m),
|
|
"samples": [
|
|
{"chainage_m": float(c), "elevation_m": 100.0, "valid": True} for c in chainages
|
|
],
|
|
"stations": [
|
|
{"chainage_m": float(c), "kind": "regular", "elevation_m": 100.0} for c in chainages
|
|
],
|
|
}
|
|
|
|
|
|
def _elevation_at(profile: dict, chainage_m: float) -> float:
|
|
samples = profile["samples"]
|
|
xs = [float(s["chainage_m"]) for s in samples]
|
|
ys = [float(s["elevation_m"]) for s in samples]
|
|
return float(np.interp(chainage_m, xs, ys))
|
|
|
|
|
|
@pytest.mark.parametrize("anchor_m", [200.0])
|
|
def test_released_profile_stays_on_the_ground(anchor_m):
|
|
"""해제(기본): 배관 자리 계획고가 지반고를 그대로 통과한다."""
|
|
_, entry = design_pipe_anchored_profile(
|
|
_flat_longitudinal(),
|
|
_options(),
|
|
[anchor_m],
|
|
station_interval_m=20.0,
|
|
pipe_clearances=None,
|
|
)
|
|
assert _elevation_at(entry, anchor_m) == pytest.approx(100.0, abs=0.05)
|
|
|
|
|
|
@pytest.mark.parametrize("anchor_m,clearance_m", [(200.0, 1.5)])
|
|
def test_enforced_profile_is_lifted_by_the_clearance(anchor_m, clearance_m):
|
|
"""강제: 배관 자리 계획고가 시설 여유만큼 들린다(Ø1000 → +1.5m)."""
|
|
_, entry = design_pipe_anchored_profile(
|
|
_flat_longitudinal(),
|
|
_options(requested={"enforce_pipe_clearance": True}),
|
|
[anchor_m],
|
|
station_interval_m=20.0,
|
|
pipe_clearances={anchor_m: clearance_m},
|
|
)
|
|
assert _elevation_at(entry, anchor_m) == pytest.approx(100.0 + clearance_m, abs=0.05)
|
|
|
|
|
|
def test_criteria_round_trip_keeps_the_switch():
|
|
"""계획선에 기록된 criteria로 스위치가 되살아난다(재편집 경로)."""
|
|
_, entry = design_pipe_anchored_profile(
|
|
_flat_longitudinal(),
|
|
_options(requested={"enforce_pipe_clearance": True}),
|
|
[200.0],
|
|
station_interval_m=20.0,
|
|
pipe_clearances={200.0: 1.5},
|
|
)
|
|
assert entry["criteria"]["enforce_pipe_clearance"] is True
|
|
|
|
|
|
# ── ④ 화면 가드도 같은 스위치를 본다 ─────────────────────────────────────────
|
|
|
|
RENDER_SOURCE = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Render.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
SECTIONS_SOURCE = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_Engine_Sections.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
|
|
def test_edit_guard_is_conditional():
|
|
"""편집 차단 가드가 스위치를 먼저 본다 — 해제면 막지 않는다.
|
|
|
|
가드는 2026-09-02 에 `_Profile_MinCover.blocksMinCover` 로 옮겼다(모든 편집이
|
|
지나는 `applyEdits` 한 곳에서 부른다). 스위치는 그 인자 `enforced` 로 들어온다.
|
|
"""
|
|
guard = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_MinCover.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "if (!base || !alignment || !enforced || !targets.length) return false;" in guard
|
|
|
|
|
|
def test_section_generation_passes_clearances_only_when_enabled():
|
|
"""자동설계는 켜졌을 때만 여유값을 넘긴다."""
|
|
assert "grade_options.enforce_pipe_clearance" in SECTIONS_SOURCE
|