- logic 새 파일 다섯 — Cross_Wall_Geom(벽 1매) · Cross_Wall_Hatch_Geom(무늬 띠 · 물빼기 · 도수로 띠 · 원지반선) · Cross_Excavation_Geom(벽 터파기) · Cross_Design_Surface_Geom(암선 · 포장층) · Cross_Revetment_Geom(독립 기슭막이 배치) - logic 으로 옮김 — Body_Guard → Cross_Body_Guard · Structure_Layouts · Route_Carry_Patch(내용 그대로 · import 경로만) - inferStationInterval → common_util_station(글자표 없는 자리 · common_util_svg 는 다시 내보냄) — Node 묶음 ui_template_locale 끌림 끊음 - 부르는 쪽 import 줄 · 등록부 ts 경로 · 시험 · 도우미 경로 - test_472_3 GEOMETRY_PULLS 다섯 줄 걷음 · Wall_Hatch · Excavation · Design_Surface 세 줄은 Culvert · Box · Ford 그림 파일(518-2)이 그리기 함수로 부르는 동안 남김 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
8.3 KiB
Python
214 lines
8.3 KiB
Python
"""PLAN 511-A (요구 320-4) — 묶음마다 서버 창구 답 = 브라우저 자리 TS 답(거울).
|
|
|
|
브라우저 쪽 = `helper_511_a_mirror.cjs` 가 그 자리 함수를 바로 부름 · 서버 쪽 = 늘 떠 있는 Node
|
|
(`run_calc_served` · 창구와 같은 본문 꼴 — `design_calc_server.ts` `areasRequest` · `bermRequest` ·
|
|
시험 길 args).
|
|
고정 자료 = golden_472 · 272 소단 사례 · 손 자료(공유 DB · 실제 마스터 안 씀).
|
|
|
|
./venv/Scripts/python.exe -m pytest resources/tester/test_511_a_calc_mirror.py -q
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
ROOT = HERE.parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
if str(HERE) not in sys.path:
|
|
sys.path.insert(0, str(HERE))
|
|
|
|
from common_util import common_util_calc_runner as runner # noqa: E402
|
|
|
|
FIXTURE = HERE / "fixtures" / "golden_472"
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
shutil.which("node") is None or not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
|
reason="node · 프론트엔드 의존성 없음",
|
|
)
|
|
|
|
|
|
def _fixture(name: str):
|
|
return json.loads((FIXTURE / name).read_text(encoding="utf-8"))
|
|
|
|
|
|
def _browser(tmp_path: Path, calls: list[dict]) -> list:
|
|
job = tmp_path / "job.json"
|
|
job.write_text(json.dumps(calls, ensure_ascii=False), encoding="utf-8")
|
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
["node", str(HERE / "helper_511_a_mirror.cjs"), str(job)],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=120,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def _served(run: list[str], inputs: dict) -> dict:
|
|
answer = runner.run_calc_served(run, inputs)
|
|
assert answer and "results" in answer, answer
|
|
return answer["results"]
|
|
|
|
|
|
def _trial(id_: str, args: list):
|
|
return _served([id_], {"args": {id_: args}})[id_]
|
|
|
|
|
|
# ── ③ 토량 · 구조물 셈(E1 · F7 · C4 · F8 = 브라우저 부름 없음) ───────────────────────
|
|
|
|
|
|
def test_quantity_F7_C4_E1_서버_브라우저_같음(tmp_path):
|
|
from test_272_berm_source import STRUCTURE_CASES
|
|
|
|
detail = _fixture("detail.json")
|
|
sections = detail["cross_sections"]
|
|
conv = _fixture("context.json")["earthwork_conversion"]
|
|
long_ = detail["longitudinal"]
|
|
calls = [
|
|
{"mod": "B06_Section/logic/B06_Section_Structure_Layouts.ts", "fn": "structureAreaRows",
|
|
"args": [sections]},
|
|
{"mod": "common_util/common_util_mass_haul.ts", "fn": "computeMassHaulSeries",
|
|
"args": [long_, sections, conv, None, ["cross"]]},
|
|
] + [
|
|
{"mod": "common_util/common_util_berm_spans.ts", "fn": "bermSpansFromStructures",
|
|
"args": [case]}
|
|
for case in STRUCTURE_CASES
|
|
] # fmt: skip
|
|
browser = _browser(tmp_path, calls)
|
|
areas = _served(["structure.layouts"], {"detail": {"cross_sections": sections}})
|
|
assert areas["structure.layouts"]["areas"] == browser[0] and browser[0]
|
|
series = _trial("masshaul.series", [long_, sections, conv, None, ["cross"]])
|
|
assert series == browser[1] and series[0]["result"]["final_cumulative_m3"]
|
|
for index, case in enumerate(STRUCTURE_CASES):
|
|
got = _served(["cross.berm_spans"], {"berm": {"structures": case, "chainages": []}})
|
|
assert got["cross.berm_spans"]["spans"] == browser[2 + index], index
|
|
|
|
|
|
# ── ④ 이월 · 다시 얹기(R9 · F10 = 브라우저 부름 없음) ──────────────────────────────────
|
|
|
|
|
|
def test_carry_R9_서버_브라우저_같음(tmp_path):
|
|
seat = {"x": 10.0, "y": 5.0, "left": [0.0, 1.0]}
|
|
states = {
|
|
"crossdesign": {"grid:1": {"ditch_type": "U"}, "20.00": {"paved": True}},
|
|
"shoulderext": {"grid:2": 0.5, "@at": {"grid:2": 40.0}, "@seat": {"grid:2": seat}},
|
|
"bermdepth": {"at:55.00": 1.0},
|
|
"rockb": {"30.00": -2.5},
|
|
"cutslope": {},
|
|
}
|
|
before = [
|
|
{"key": "grid:1", "chainageM": 20.0, "seat": seat},
|
|
{"key": "grid:2", "chainageM": 40.0},
|
|
{"key": "end:EP", "chainageM": 60.0},
|
|
]
|
|
after = [
|
|
{"key": "grid:1", "chainageM": 22.0},
|
|
{"key": "grid:2", "chainageM": 41.0, "seat": seat},
|
|
{"key": "end:EP", "chainageM": 58.0},
|
|
]
|
|
empty = dict.fromkeys(states, {}) # 브라우저 읽기 = 빈 칸도 {}(readRaw)
|
|
cases = [[states, before, after], [states, before, None], [empty, None, after]]
|
|
browser = _browser(
|
|
tmp_path,
|
|
[
|
|
{"mod": "resources/tester/helper_511_a_carry.ts", "fn": "carryWith", "args": case}
|
|
for case in cases
|
|
],
|
|
)
|
|
for index, case in enumerate(cases):
|
|
assert _trial("stations.carry_keyed", case) == browser[index], index
|
|
assert browser[0]["states"]["crossdesign"] # 넘김이 실제로 일어남
|
|
|
|
|
|
# ── ⑧ 고정 브라우저(시험용 · R2 R4 C7 대조 · P7 서버 값 · R8 · D4 = 화면 검증) ──────────────────
|
|
|
|
_ROUTE = [[0, 0], [93, 7], [171, 61], [262, 48], [337, 112], [431, 101], [503, 160]]
|
|
_LIMITS = {"minRadiusM": 30, "limitRadiusM": 12, "limitArcM": 0, "stationM": 20, "intervalM": 10}
|
|
|
|
|
|
def _columns(planned):
|
|
n = len(planned)
|
|
return {
|
|
"planned": [list(p) for p in planned],
|
|
"curveOn": [True] * n,
|
|
"curveRadius": [None] * n,
|
|
"curveLock": [None] * n,
|
|
"curveArc": [None] * n,
|
|
"apexLock": [i in (0, n - 1) for i in range(n)],
|
|
}
|
|
|
|
|
|
def _profile_base():
|
|
import math
|
|
|
|
stations = [i * 20 for i in range(16)]
|
|
ground = [round(100 + 0.03 * s + 0.6 * math.sin(s / 13), 6) for s in stations]
|
|
policy = {
|
|
"station_interval_m": 20, "curve_radius_ratio": 1, "curve_tangent_max_ratio": 0.5,
|
|
"curve_skip_legal_exception": False, "default_curve_length_m": 20,
|
|
"default_curve_radius_m": 300, "balance_tolerance_percent": 10, "edit_step_m": 0.1,
|
|
"grade_violation_policy": "warn", "max_grade_pct": 14, "curve_skip_delta_pct": 0,
|
|
"paved": True, "min_vertical_radius_m": 100, "min_curve_length_m": 20,
|
|
} # fmt: skip
|
|
return {
|
|
"policy": policy,
|
|
"onlyExplicitCurves": False,
|
|
"basePvi": [{"chainage_m": s, "elevation_m": z} for s, z in zip(stations, ground)],
|
|
"chainage": stations,
|
|
"ground": ground,
|
|
"stations": [{"station_id": f"S{s}", "chainage_m": s} for s in stations],
|
|
}
|
|
|
|
|
|
def test_fixed_browser_R2_R4_P7_C7_서버_브라우저_같음(tmp_path):
|
|
pushed = [list(p) for p in _ROUTE]
|
|
pushed[2] = [174.0, 58.0]
|
|
base = _profile_base()
|
|
empty = {"station_offsets": {}, "curve_radii": {}}
|
|
saved = {"40.000": 5330, "200.000": 1084}
|
|
sections = _fixture("detail.json")["cross_sections"][:12]
|
|
cases = {
|
|
"route.handle": [
|
|
[[0, 0], [100, 0], [100, 100], "end", [105, 60]],
|
|
[[0, 0], [100, 0], [100, 100], "start", [60, -4]],
|
|
[[0, 0], [100, 0], [200, 0], "end", [150, 0]], # 직선 = null
|
|
],
|
|
"route.station_fit": [
|
|
[_columns(_ROUTE), _columns(pushed), _LIMITS],
|
|
[None, _columns(_ROUTE), {**_LIMITS, "intervalM": 20}],
|
|
],
|
|
"profile.tools": [
|
|
[base, empty, 40, 200, {}],
|
|
[base, {"station_offsets": {}, "curve_radii": dict(saved)}, 40, 200, saved],
|
|
[base, empty, 0, 300, {}],
|
|
],
|
|
"cross.fit": [[section] for section in sections],
|
|
}
|
|
b05 = "B05_Profile/logic/B05_Profile_"
|
|
where = {
|
|
"route.handle": (f"{b05}UI_RouteEdit_Curve.ts", "dragHandleTo"),
|
|
"route.station_fit": (f"{b05}RouteEdit_StationFit.ts", "fitAfter"),
|
|
"profile.tools": (f"{b05}UI_Profile_Straighten.ts", "straightenBetween"),
|
|
"cross.fit": ("B06_Section/logic/B06_Section_UI_Cross_Fit.ts", "toeOffsets"),
|
|
}
|
|
flat = [(id_, args) for id_, rows in cases.items() for args in rows]
|
|
browser = _browser(
|
|
tmp_path,
|
|
[{"mod": where[id_][0], "fn": where[id_][1], "args": args} for id_, args in flat],
|
|
)
|
|
for (id_, args), want in zip(flat, browser):
|
|
assert _trial(id_, args) == want, id_
|
|
assert browser[0] and browser[2] is None # 직선 = null
|
|
toes = [want for (id_, _), want in zip(flat, browser) if id_ == "cross.fit"]
|
|
assert any(toe["left"] is not None for toe in toes)
|