Files
Aislo/resources/tester/test_b05_pipe_clearance_switch.py
T
eomsangdonandClaude Opus 5 0ef32b5279 chore(tester): 시험을 resources/tester/ 로 옮김 — 창끼리 건너가게
⚠ **뿌리** — `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>
2026-09-09 17:12:30 +09:00

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