"""PLAN 483-1(요구 280-4) — 스냅이 일반측점을 MC 로 옮기지 않음 · MC 특별측점 더함. 서버 `_station_layout`(`generate_sections`) ↔ TS `buildStations` 거울 — 분석 D 재현 1 다섯 경우 (BP–EP 200 m 직선 · 곡선 하나 · 측점 간격 20 m). 짝 = `helper_280_4_stations.cjs`. """ from __future__ import annotations import json import subprocess import sys import tempfile from pathlib import Path import numpy as np import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B05_Profile import B05_Profile_Engine_Sections_Core as core # noqa: E402 pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "typescript").is_dir(), reason="프론트엔드 의존성 미설치", ) LINE = [[0.0, 0.0], [200.0, 0.0]] def _curve(bc: float, ec: float, radius: float = 30.0) -> list[dict]: return [ {"start": [bc, 0.0], "apex": [(bc + ec) / 2, 3.0], "end": [ec, 0.0], "radius_m": radius} ] CASES = { "A_10m_긴호_MC50": {"curves": _curve(34.3, 65.7), "snap": True, "step": 10.0}, "B_10m_짧은호_MC50": {"curves": _curve(45.6, 54.4, 8.5), "snap": True, "step": 10.0}, "C_10m_MC60": {"curves": _curve(44.3, 75.7), "snap": True, "step": 10.0}, "D_측점No_MC47": {"curves": _curve(31.3, 62.7), "snap": True, "step": None}, "E_스냅끔_MC50": {"curves": _curve(34.3, 65.7), "snap": False, "step": None}, } GRID = [float(v) for v in range(0, 201, 20)] class _Flat: def sample_xy(self, xy): count = len(np.asarray(xy)) return np.zeros(count), np.ones(count, dtype=bool) def _server(case: dict) -> list[dict]: data = core.generate_sections( [[x, y, 0.0] for x, y in LINE], _Flat(), core.SectionGenerationOptions(), design_curves=case["curves"], station_snap=case["snap"], station_snap_step=case["step"], ) return data["longitudinal"]["stations"] @pytest.fixture(scope="module") def ts() -> dict: jobs = [{"line": LINE, **case} for case in CASES.values()] with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "job.json" path.write_text(json.dumps(jobs), encoding="utf-8") proc = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(HERE / "helper_280_4_stations.cjs"), str(path)], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return dict(zip(CASES, json.loads(proc.stdout), strict=True)) _KIND = {"bp": "end", "ep": "end", "regular": "grid", "curve": "curve"} EXPECT_CURVE = { "A_10m_긴호_MC50": 50.0, "B_10m_짧은호_MC50": 50.0, "C_10m_MC60": None, "D_측점No_MC47": 47.0, "E_스냅끔_MC50": 50.0, } @pytest.mark.parametrize("name", list(CASES)) def test_일반측점_그대로_MC_특별측점(name, ts): server = _server(CASES[name]) regular = [s["chainage_m"] for s in server if s["kind"] in ("bp", "regular", "ep")] assert regular == GRID # 20 m 일반측점 하나도 안 사라짐 curves = [s for s in server if s["kind"] == "curve"] mc = EXPECT_CURVE[name] if mc is None: # N+0 = 일반측점 위 MC → 특별측점 없음 assert curves == [] else: assert [s["chainage_m"] for s in curves] == [mc] assert curves[0]["label"].endswith(" (MC)") # 거울 — 누가거리 · 종류 ours = ts[name] assert [s["m"] for s in ours] == pytest.approx([s["chainage_m"] for s in server], abs=1e-6) assert [s["kind"] for s in ours] == [_KIND[s["kind"]] for s in server] def test_A_열쇠(ts): keys = {s["key"]: s["m"] for s in ts["A_10m_긴호_MC50"]} assert keys["grid:2"] == 40.0 and keys["curve:50.00"] == 50.0 for s in ts["A_10m_긴호_MC50"]: if s["kind"] == "grid": assert s["key"] == f"grid:{round(s['m'] / 20)}" def test_스냅_켬_끔_같은_목록(): on = _server(CASES["A_10m_긴호_MC50"]) off = _server(CASES["E_스냅끔_MC50"]) assert on == off