Files
Aislo/resources/tester/test_511_a_calc_mirror.py
T
eomsangdonandClaude Opus 5.5 d3315da993 refactor(472-3 B06 벽 · 기슭막이 · 배치): 구조물 기하 logic 분리 · 그림 파일은 그리기만
- 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>
2026-10-23 19:22:27 +09:00

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)