Files
Aislo/resources/tester/test_511_b2_stations_node.py
T

216 lines
8.6 KiB
Python

"""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
def test_이월_열쇠_Node_출력_없으면_파이썬(project):
from B05_Profile.B05_Profile_Route_Save_Parts import keyed_stations, station_keys
_, noded = _both(project)
stations = noded["longitudinal"]["stations"]
keyed = keyed_stations(stations)
pipes, structures, _ = project["facilities"]
assert keyed == station_keys(stations, pipes, structures, 20.0) # 고정 자료 = 같은 열쇠
assert keyed_stations([{k: v for k, v in s.items() if k != "key"} for s in stations]) is None
assert keyed_stations([*stations, {"chainage_m": 1.0}]) is None # 섞이면(구조물 측점 덧) 파이썬
route_save = (ROOT / "B05_Profile/B05_Profile_Route_Save.py").read_text(encoding="utf-8")
assert route_save.count("keyed_stations(") == 2