"""PLAN 511-B B-2 (요구 320-4) — 서버 측점 생성 = 화면과 같은 TS(Node `stations.sections`). 못박는 것 - 고정 자료(`fixtures/golden_472`) 노선 · 관 · 구조물 · 추가 측점 — `build_sections` Node 길 = 파이썬 `generate_sections` 길 0 차(칸 · 차례 · 값 · 측점 열쇠 `key` 만 더함) — 곡선표 있음 · 없음 · 스냅 10 m · 측점 간격 10 m · 끝 측점 뺌. - DTM 아닌 지표면 = 파이썬 길(Node 안 부름 · 열쇠 없음). - 열쇠 = Node `stations.list`(화면 묶음 stations 단계 `buildStations`) 열쇠. - 창구(`/calc`)는 `stations.sections`(서버 파일 경로 입력)를 안 받음. - 등록부 R5 · R6 · R7 = node:stations.list · G4 = node:stations.sections(472-4 until 없음). - 공유 DB · 실제 storage 안 씀(고정 자료 사본). ./venv/Scripts/python.exe -m pytest resources/tester/test_511_b2_stations_node.py -q """ from __future__ import annotations import sys from dataclasses import replace from pathlib import Path import numpy as np import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] for path in (ROOT, HERE): if str(path) not in sys.path: sys.path.insert(0, str(path)) import helper_472_1_paths as paths # noqa: E402 from B05_Profile import B05_Profile_Engine_Sections as sections # noqa: E402 from B05_Profile import B05_Profile_Engine_Sections_Node as node # noqa: E402 from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions # noqa: E402 from common_util import common_util_calc_runner as runner # noqa: E402 from common_util.common_util_surface_sampler import ( # noqa: E402 CallableSurfaceSampler, build_surface_sampler, ) pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "vite").is_dir(), reason="프론트엔드 의존성 미설치" ) @pytest.fixture(scope="module") def project(tmp_path_factory): root = paths.stage_project(tmp_path_factory.mktemp("b2") / "project") summary = paths.read("summary.json")["longitudinal"]["data"] options = SectionGenerationOptions( **{k: v for k, v in summary["options"].items() if k != "extra_stations"} ) polyline = sections._load_route_polyline(root, paths.ROUTE) pipes = sections._load_pipe_points(root, polyline) facilities = sections.station_facilities(root, pipes, polyline) extras = sections.resolve_extra_stations( root, facilities[0], structures=facilities[1], plus=facilities[2] ) return { "root": root, "options": options, "polyline": polyline, "pipes": pipes, "facilities": facilities, "extras": extras, "sampler": build_surface_sampler(root / "B04_PreProcess/models", *paths.SURFACE), "curves": sections._load_design_curves(root), } def _both(project, sampler=None, **change): kw = { "options": project["options"], "grade_options": None, "source_snapshot": {"filter": "classification"}, "crs": "EPSG:5186", "design_curves": project["curves"], "station_snap": sections.load_station_snap(project["root"]), "station_snap_step": sections.load_station_snap_step(project["root"]), **change, } args = (project["polyline"], sampler or project["sampler"], project["pipes"], project["extras"]) python, _ = sections.build_sections(*args, **kw) noded, _ = sections.build_sections(*args, facilities=project["facilities"], **kw) return python, noded def _without_key(value): if isinstance(value, dict): return {k: _without_key(v) for k, v in value.items() if k != "key"} if isinstance(value, list): return [_without_key(v) for v in value] return value def _first_diff(a, b, path=""): """첫 다른 자리(값 · 칸 차례) — 큰 글 비교를 pytest 에 안 넘김. 수 = 같은 값(0 ↔ 0.0).""" if isinstance(a, dict) and isinstance(b, dict): if list(a) != list(b): return f"{path}: 칸 {list(a)} != {list(b)}" return next((d for k in a if (d := _first_diff(a[k], b[k], f"{path}.{k}"))), None) if isinstance(a, list) and isinstance(b, list): if len(a) != len(b): return f"{path}: 길이 {len(a)} != {len(b)}" return next( (d for i, (x, y) in enumerate(zip(a, b)) if (d := _first_diff(x, y, f"{path}[{i}]"))), None, ) return None if a == b else f"{path}: {a!r} != {b!r}" def _same(python, noded): hit = _first_diff(python, _without_key(noded)) # 칸 차례 · 자릿수까지 assert hit is None, hit keys = [s.get("key") for s in noded["longitudinal"]["stations"]] assert all(keys) and keys == [s["key"] for s in noded["cross_sections"]] return keys @pytest.mark.parametrize( "case", ["as_saved", "no_curves", "snap10", "interval10", "no_endpoint"], ) def test_Node_길_파이썬_길_0차(project, case): change = { "as_saved": {}, "no_curves": {"design_curves": []}, "snap10": {"station_snap": True, "station_snap_step": 10.0}, "interval10": {"options": replace(project["options"], station_interval_m=10.0)}, "no_endpoint": {"options": replace(project["options"], include_endpoint=False)}, }[case] python, noded = _both(project, **change) keys = _same(python, noded) assert len(keys) == len(set(keys)) > 10 if case == "as_saved": assert len(keys) == 57 and keys[0] == "end:BP" and keys[-1] == "end:EP" kinds = {s["kind"] for s in python["longitudinal"]["stations"]} assert {"bp", "ep", "regular", "irregular"} <= kinds assert any(k.startswith("pipe:") for k in keys) def test_열쇠_Node_측점_목록과_같음(project): _, noded = _both(project) opts = project["options"] stations = node.stations_input( [[p[0], p[1]] for p in project["polyline"]], *project["facilities"], opts, project["curves"], sections.load_station_snap(project["root"]), sections.load_station_snap_step(project["root"]), ) listed = node.node_stations(stations) assert [s["key"] for s in listed] == [s["key"] for s in noded["longitudinal"]["stations"]] assert [s["chainageM"] for s in listed] == [ s["chainage_m"] for s in noded["longitudinal"]["stations"] ] def test_DTM_아닌_지표면_파이썬_길(project, monkeypatch): called = [] monkeypatch.setattr(runner, "run_calc_served", lambda *a: called.append(a)) flat = CallableSurfaceSampler(lambda xy: np.full(len(xy), 100.0)) python, noded = _both(project, sampler=flat) assert called == [] and python == noded assert "key" not in noded["longitudinal"]["stations"][0] def test_Node_못_돌림_파이썬_길(project, monkeypatch): monkeypatch.setattr(runner, "run_calc_served", lambda *a: None) python, noded = _both(project) assert python == noded def test_창구는_서버_측점_생성_안_받음(): from fastapi import FastAPI from fastapi.testclient import TestClient from B06_Section import B06_Section_Router_Calc as calc_router app = FastAPI() app.include_router(calc_router.router) http = TestClient(app) url = "/api/projects/11111111-2222-3333-4444-555555555555/calc" body = {"run": ["stations.sections"], "inputs": {"stations": {"grid": "C:/x"}}} bad = http.post(url, json=body) assert bad.status_code == 400 and "창구 밖" in bad.json()["message"] assert runner.SERVER_ONLY_IDS <= runner.NODE_IDS def test_등록부_측점_줄_Node(): src = (ROOT / "A00_Common/design/logic/design_calc_registry.ts").read_text(encoding="utf-8") for no, run in ( ("R5", "stations.list"), ("R6", "stations.list"), ("R7", "stations.list"), ("G4", "stations.sections"), ): row = src.split(f'no: "{no}"')[1].split("\n },")[0] assert f'runner: "node:{run}"' in row and "until" not in row, no