Files
Aislo/resources/tester/test_390_5_snap_step_ops.py
T

199 lines
7.8 KiB
Python

"""390-5 스냅 간격 = 조작 단위(요구 243) — 스냅 켬이면 방향키 한 번 = 고른 간격 · 끄는 동안 MC 가
간격 배수에 맞은 자리로 보임 · MC 측점 값 = 간격 자릿수(0.1 = 소수 한 자리 · 그 밖 = 정수) ·
간격을 저장 노선(곡선표 `station_snap_step`)에 같이 → 서버 측점 셈(파이썬 짝)이 같은 값.
TS 셈은 `helper_390_5_snap_step_ops.cjs`(Node) · 파이썬 짝 거울 · 저장 왕복 · 편집기 이음은 여기.
./venv/Scripts/python.exe -m pytest resources/tester/test_390_5_snap_step_ops.py -q
"""
import json
import random
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B05_Profile import B05_Profile_Engine_Sections_Core as core # noqa: E402
from B05_Profile.B05_Profile_Engine_Sections import load_station_snap_step # noqa: E402
from B05_Profile.B05_Profile_Route_Save import RouteDraftInput, RouteSaveRequest # noqa: E402
from B05_Profile.B05_Profile_Router_Replan import _write_planned_polyline # noqa: E402
from common_util.common_util_route_geometry import planned_route_working_path # noqa: E402
B05 = ROOT / "B05_Profile"
HELPER = Path(__file__).with_name("helper_390_5_snap_step_ops.cjs")
GRID = [float(v) for v in range(0, 1001, 20)]
STEPS = {"0.1": 0.1, "1": 1.0, "10": 10.0, "station": None}
def _spans() -> list[list[float]]:
rng = random.Random(3905)
spans, at = [], 15.0
while at < 950:
start = round(at + rng.uniform(0, 30), 3)
end = round(start + rng.uniform(18, 60), 3)
spans.append([start, end])
at = end + rng.uniform(5, 25)
return spans
SPANS = _spans()
@pytest.fixture(scope="module")
def got() -> dict:
run = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HELPER), json.dumps({"grid": GRID, "spans": SPANS})],
capture_output=True,
text=True,
encoding="utf-8",
timeout=120,
)
assert run.returncode == 0, run.stderr
return json.loads(run.stdout)
# ── ① 방향키 ──
def test_스냅_끔_방향키는_0_1_Shift_1(got) -> None:
assert got["keys"]["off"] == pytest.approx(0.1)
assert got["keys"]["offShift"] == pytest.approx(1.0)
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
def test_스냅_켬_방향키_한_번_고른_간격_Shift_무시(got, step) -> None:
key = got["keys"]["snap"][step]
assert key["plain"] == pytest.approx(float(step))
assert key["shift"] == pytest.approx(float(step))
# ── ② 끌기 미리보기 ──
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
def test_끄는_동안_MC_가_간격_배수(got, step) -> None:
drag = got["drag"][step]
assert drag["off"] == 0
assert max(drag["residues"]) <= drag["tol"]
assert drag["dragMoved"] <= float(step) / 2 + 1e-6 # 끈 IP 는 접선 따라 ±간격/2 안만
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
def test_미리보기는_쌓이지_않고_놓을_때와_같음(got, step) -> None:
drag = got["drag"][step]
assert drag["stale"] == 0
assert drag["release"] == 0
assert drag["before"] == 0 # 끈 IP 앞앞 IP 는 그대로
def test_끌기_방향키_편집기_이음() -> None:
pointer = (B05 / "B05_Profile_UI_RouteEdit_Pointer.ts").read_text(encoding="utf-8")
keys = (B05 / "B05_Profile_UI_RouteEdit_Keys.ts").read_text(encoding="utf-8")
edit = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
assert "params.fitDrag(dragNode)" in pointer
assert "params.fitDrag(now.index)" in keys and "params.snapStepM()" in keys
assert "fit.fitDrag(snapshotNow(), base, index, fitLimits())" in edit
# 하한 검사는 맞추기 전 노선으로(맞춤이 IP 를 ±간격/2 밀어 10 m · 측점 No. 에서 헛걸림)
assert "dragRaw.forEach((vertex, index) => (state.planned[index] = vertex))" in pointer
assert "now.raw.forEach((vertex, index) => (state.planned[index] = vertex))" in keys
assert edit.count("\n") <= 695
# ── ③ MC 측점 값 · 파이썬 짝 ──
def test_자릿수_배수(got) -> None:
assert got["scale"] == [10, 1, 1, 1]
assert [core._mc_scale(v) for v in (0.1, 1.0, 10.0, None)] == [10, 1, 1, 1]
@pytest.mark.parametrize("key", list(STEPS))
def test_MC_측점_값_자릿수(got, key) -> None:
values = got["snapped"][key]
moved = [v for v in values if v not in GRID]
assert moved, "호 안 측점이 MC 로 옮겨져야 함"
scale = 10 if key == "0.1" else 1
assert all(abs(v * scale - round(v * scale)) < 1e-9 for v in values)
if key == "0.1":
assert any(abs(v - round(v)) > 1e-9 for v in moved) # 소수 한 자리 MC 가 실제로 생김
@pytest.mark.parametrize("key", list(STEPS))
def test_파이썬_짝_거울(got, key) -> None:
py = [float(v) for v in core.snap_curve_stations(GRID, SPANS, STEPS[key])]
assert py == got["snapped"][key]
def test_옛_부르기는_정수_그대로() -> None:
old = [float(v) for v in core.snap_curve_stations(GRID, SPANS)]
assert old == [float(v) for v in core.snap_curve_stations(GRID, SPANS, None)]
assert all(v == round(v) for v in old)
def test_눈금_글_0_1_때만_소수_한_자리(got) -> None:
labels = got["labels"]
assert labels["int"] == "5+20" and labels["oneDefault"] == "5+20" # 다른 화면 = 정수(09-29)
assert labels["one"] == "5+19.8" and labels["oneSmall"] == "5+0.1"
assert labels["oneWhole"] == "6+0"
overlay = (ROOT / "B04_PreProcess" / "B04_PreProcess_UI_MapOverlays.ts").read_text(
encoding="utf-8"
)
assert "stationLabel(chainage, interval, options.chainages ? 1 : 0)" in overlay
# ── 저장 왕복 ──
def test_초안_간격이_요청으로_펴짐() -> None:
nodes = [(0.0, 0.0), (50.0, 10.0), (100.0, 0.0)]
draft = RouteDraftInput(nodes=nodes, stationSnap=True, stationSnapStep=0.1)
resolved = RouteSaveRequest(draft=draft).resolved()
assert resolved.station_snap is True and resolved.station_snap_step == 0.1
old = RouteSaveRequest(draft=RouteDraftInput(nodes=nodes, stationSnap=True))
assert old.resolved().station_snap_step is None
@pytest.mark.parametrize(
("snap", "step", "want"),
[
(True, 0.1, 0.1),
(True, 1.0, 1.0),
(True, 10.0, 10.0),
(True, None, None),
(False, 0.1, None),
],
)
def test_곡선표_저장_왕복(tmp_path, snap, step, want) -> None:
path = planned_route_working_path(tmp_path)
path.parent.mkdir(parents=True, exist_ok=True)
points = [(0.0, 0.0), (120.0, 15.0), (240.0, -10.0), (360.0, 30.0)]
_write_planned_polyline(path, points, 30.0, station_snap=snap, station_snap_step=step)
assert load_station_snap_step(tmp_path) == want
def test_옛_곡선표는_측점_No(tmp_path) -> None:
path = planned_route_working_path(tmp_path)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("x,y\n0,0\n10,0\n", encoding="utf-8")
path.with_name(f"{path.stem}_curves.json").write_text(
json.dumps({"curves": [], "station_snap": True}), encoding="utf-8"
)
assert load_station_snap_step(tmp_path) is None
def test_서버_측점_셈이_저장_간격을_씀() -> None:
sections = (B05 / "B05_Profile_Engine_Sections.py").read_text(encoding="utf-8")
assert sections.count("station_snap_step=load_station_snap_step(project_root)") == 2
design = ROOT / "A00_Common" / "design"
page = (design / "design_pipeline_page.ts").read_text(encoding="utf-8")
assert "stationSnapStep: routePlan.station_snap_step" in page
stage = (design / "logic" / "design_pipeline_stations.ts").read_text(encoding="utf-8")
assert "stationSnapStep: ctx.inputs.route?.stationSnapStep" in stage
apply = (B05 / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
assert "stationSnap: true, stationSnapStep: null" in apply # 추천 = 측점 No.