"""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/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)