"""PLAN 429 — [저장] 길 정합(분석 424 9장 ④⑤⑧⑨). - ④ 노선 [저장] 뒤 횡단 저장 = 고침이 없어도 한 번(서버가 지금 종단으로 횡단을 다시 셈 · 뒤에 나간 계획고 초안이 횡단 설계 높이에 실림) · 서버 [저장]이 횡단 설계 높이를 저장 종단에서 냄. - ⑤ 상단측 차례(`sync_uphill_overrides_into_designs`)가 소단 · 깎기 경사 · 조절 값을 남김. - ⑨ 추가 측점 = 계산 묶음 입력(stations 부터 다시) · 가게 초안 바뀜 · 되돌리기 → 묶음에 넣음. [저장] 차례 실패 = 그 차례에서 멈춤(`test_design_store_save.py`). Node 헬퍼 = `helper_429_save_path.cjs`. """ import asyncio import json import subprocess from pathlib import Path import pytest from B06_Section.B06_Section_Engine_Design import compute_cross_design from B06_Section.B06_Section_Router_Design import enforce_design_lines from config.config_system import STANDARD_CROSS_SECTION HERE = Path(__file__).parent ROOT = HERE.parents[1] @pytest.fixture(scope="module") def out(): proc = subprocess.run( ["node", str(HERE / "helper_429_save_path.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def _ground() -> list[dict]: """왼쪽이 높은 비탈 — 왼쪽 깎기 · 오른쪽 쌓기(편절).""" return [ {"offset_m": o / 2, "elevation_m": round(100.0 + 0.25 * o / 2, 4), "valid": True} for o in range(-40, 41) ] # ── ④ ──────────────────────────────────────────────────────────────────────── def test_노선_저장_뒤_고침이_없어도_횡단_저장_한_번(out): # 본문 없이 = 서버가 다시 셈만 · 풍선 자리는 늘 실음(445 H5 — 횡단 보기 [저장]과 같은 본문) assert out["noPatchSent"] == [["p1", 9, None, None, {"balloon_offsets": {}}]] assert out["noPatchReturn"] == 0 def test_서버_저장이_횡단_높이를_저장_종단에서(tmp_path): """초안 계획고(101.2)가 종단 정본에 나간 뒤 [저장] 다시 셈 = 그 높이 · 옛 설계(100.0) 아님.""" old = compute_cross_design( _ground(), 100.0, standard=STANDARD_CROSS_SECTION, ground_type="soil", section_mode="left_cut", ditch_side="left", ) # fmt: skip longitudinal = { "design_profiles": [ {"samples": [{"chainage_m": 0.0, "elevation_m": 101.2}, {"chainage_m": 40.0, "elevation_m": 101.2}]} ] } # fmt: skip sections = [{"chainage_m": 20.0, "samples": _ground(), "design": old}] assert enforce_design_lines(longitudinal, sections, tmp_path, STANDARD_CROSS_SECTION) == 1 assert sections[0]["design"]["design_elevation_m"] == pytest.approx(101.2, abs=1e-6) # ── ⑤ ──────────────────────────────────────────────────────────────────────── def test_상단측_차례가_소단_깎기경사_조절값_남김(tmp_path, monkeypatch): import B05_Profile.B05_Profile_Router_Confirm as confirm import B06_Section.B06_Section_Repository as repo import B06_Section.B06_Section_Repository_Bulk as bulk import B06_Section.B06_Section_Router_Design as design_mod user = { "cut_slope_ratio_user": 0.7, "berm": {"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0}, "berm_depth_left_m": 2.0, "basin_adjust": {"w": 1.5}, "structure_note": "글", "shoulder_extend_right_m": 0.3, } before = compute_cross_design( _ground(), 100.0, standard=STANDARD_CROSS_SECTION, ground_type="soil", section_mode="right_cut", ditch_side="right", ) # fmt: skip stored = {**before, **user} long_file = tmp_path / "long.json" long_file.write_text("{}", encoding="utf-8") async def designs(_connection, _route_id): return [{"chainage_m": 20.0, "design": stored}] async def longitudinal(*_args): return {"data": {"options": {"standard_cross_section": STANDARD_CROSS_SECTION}}} written: list = [] async def merge(_connection, *, route_id, entries, replace, project_id=None): written.extend(entries) monkeypatch.setattr(repo, "get_cross_section_designs", designs) monkeypatch.setattr(confirm, "get_longitudinal_section", longitudinal) monkeypatch.setattr(bulk, "merge_cross_section_designs", merge) monkeypatch.setattr(design_mod, "resolve_longitudinal_path", lambda *_: long_file) monkeypatch.setattr( design_mod, "read_cross_design_inputs", lambda *_a: (_ground(), 100.0, False, {}) ) monkeypatch.setattr(design_mod, "ford_surface_drops", lambda _root: []) asyncio.run( confirm.sync_uphill_overrides_into_designs( None, "p", 1, tmp_path, "long.json", [{"chainage_m": 20.0, "side": "left"}] ) ) assert len(written) == 1 chainage, after = written[0] assert chainage == 20.0 and before["ditch_type"] is None # 측구 끔 저장분도 셈 for key, value in user.items(): assert after[key] == value, key # 값 남음 assert after["cut_slope_ratio"] == pytest.approx(0.7) # 깎기 경사가 기하에 실림(키만 아님) assert after["shoulder_extend_right_m"] == pytest.approx(0.3) # ── ⑨ ──────────────────────────────────────────────────────────────────────── def test_추가_측점이_묶음_입력(out): assert "input.plusStations" in out["stationsDeps"] assert out["plusRan"][0] == "stations" and "route" not in out["plusRan"] # stations 부터 assert "cross" in out["plusRan"] # 뒤 단계까지 이어짐 assert out["plusSeen"] == [40] assert out["sameValue"] is None and out["sameRan"] == [] # 같은 값은 안 셈 def test_가게_추가_측점_초안이_묶음에(out): assert out["fed"] == [["plus", [12, 30]], ["plus", [12]]] # 초안 · 비우면 저장분 assert out["restoreFed"] == [["plus", [12, 55]]] # 되돌리기 = 입력과 다르면 넣음