"""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