"""PLAN 349-0p — 관 · 구조물 측점이 소수 자리(386.6 · 150.3)인 노선 꼴 · 입력 없는 측점 암 경계. · 세월교 · 물넘이 · 관이 소수 자리 누가거리에 선 노선 — cross 단계 결과(포장 · 노면 하강 · 기울임 · 면적 · 흙 · 암 나눔) = 서버 [확정] 보정 뒤 설계 · 저장 입력이 없는 측점(초안 새 측점) — 암 경계 = 기본 오프셋(서버 config · 화면이 서버 문맥에서 기억한 값) · 기본값이 안 오면 옛 그대로(경계 없음 = 깎기 전부 암) 짝: `helper_349_0n_cross_enforce.cjs` · 틀 = `test_349_0n_cross_enforce_mirror.py`. """ from __future__ import annotations import json import subprocess import sys from pathlib import Path 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)) from test_349_0n_cross_enforce_mirror import OFFSETS, _stations, _z # noqa: E402 import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402 import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402 import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402 import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402 from B06_Section.B06_Section_Engine_Culvert_Sets import culvert_sets_from # noqa: E402 from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402 from config.config_system_design import ( # noqa: E402 STANDARD_CROSS_SECTION, STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M, ) pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "typescript").is_dir(), reason="프론트엔드 의존성 미설치", ) PIPES = [ {"chainage_m": 150.3, "options": {"pipe_diameter_mm": 800}}, { "chainage_m": 320.4, "facility": "ford_pavement", "options": {"ford_width_m": 12, "ford_slope_deg": 4}, }, {"chainage_m": 386.6, "facility": "ford_bridge", "options": {"ford_height_m": 0.25}}, ] STRUCTURES = [ StructureInstance( structure_id="pv1", type_id="gravel_surfacing", placement="interval", start_m=40.3, end_m=80.6, ), ] OWN_ROCK = -1.0 # 저장 입력이 있는 측점(짝수 격자)만 — 나머지는 초안 새 측점처럼 입력 없음 def _inputs(stations: list[dict]) -> dict: return { s["key"]: {"sectionMode": "left_cut", "rockBoundaryOffsetM": OWN_ROCK} for s in stations if s["key"].startswith("grid:") and int(s["key"][5:]) % 2 == 0 } def _run(tmp: Path, name: str, default: float | None) -> dict: stations = _stations(PIPES) job = { "pipes": PIPES, "structures": [s.model_dump(mode="json") for s in STRUCTURES], "stations": stations, "probe": [0], "standard": STANDARD_CROSS_SECTION, "byKey": _inputs(stations), "rockBoundaryDefaultM": default, } path = tmp / f"{name}.json" path.write_text(json.dumps(job), encoding="utf-8") proc = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(HERE / "helper_349_0n_cross_enforce.cjs"), str(path)], capture_output=True, text=True, encoding="utf-8", timeout=120, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout)["stage"]["byKey"] @pytest.fixture(scope="module") def r(tmp_path_factory) -> dict: tmp = tmp_path_factory.mktemp("frac") return { "stations": _stations(PIPES), "with": _run(tmp, "with", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M), "without": _run(tmp, "without", None), } @pytest.fixture(scope="module") def server(r) -> list[dict]: """서버 [확정] — 저장분(입력 없는 측점은 config 암 경계) → 포장 구간 → 세월교 하강.""" patch = pytest.MonkeyPatch() sets = culvert_sets_from(parse_pipe_points(PIPES)) # 구간 읽기는 472-4 에서 `_Router_Design_Ranges` 로 나눔 — 정본 읽기를 그 모듈에서 갈아 끼움. for module in (ranges_mod, tilt_mod): patch.setattr(module, "load_culvert_sets", lambda _root: sets) patch.setattr(module, "load_structures", lambda _root: (0, STRUCTURES)) patch.setattr(structures_repo, "load_structures", lambda _root: (0, STRUCTURES)) own = _inputs(r["stations"]) longitudinal = { "design_profiles": [ { "samples": [ {"chainage_m": s["chainageM"], "elevation_m": s["elevation"]} for s in r["stations"] ] } ] } sections = [] for s in r["stations"]: samples = [ {"offset_m": o, "elevation_m": _z(o, s["chainageM"]), "valid": True} for o in OFFSETS ] rock = OWN_ROCK if s["key"] in own else STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M design = compute_cross_design( samples, s["elevation"], ground_type="ripping_rock", section_mode="left_cut", ditch_type="standard", standard=STANDARD_CROSS_SECTION, rock_boundary_offset_m=rock, ) sections.append({"chainage_m": s["chainageM"], "samples": samples, "design": design}) try: root = Path("unused") design_mod.enforce_pavement_ranges(longitudinal, sections, root, STANDARD_CROSS_SECTION) design_mod.enforce_ford_surface_drops(longitudinal, sections, root, STANDARD_CROSS_SECTION) finally: patch.undo() return sections def test_소수_자리_측점_결과_서버와_같음(r, server): keys = ("cut_area_m2", "cut_soil_area_m2", "cut_rock_area_m2", "fill_area_m2") for station, section in zip(r["stations"], server, strict=True): a, b = r["with"][station["key"]], section["design"] for key in keys: assert a[key] == pytest.approx(b[key], abs=2e-3), (station["key"], key) for key in ("paved", "tilted", "surface_drop_m", "rock_boundary_offset_m"): assert a.get(key) == b.get(key), (station["key"], key) by = {s["key"]: s["chainageM"] for s in r["stations"]} assert r["with"]["pipe:p2"]["surface_drop_m"] == 0.25 and by["pipe:p2"] == 386.6 assert r["with"]["pipe:p1"]["tilted"] and r["with"]["grid:16"]["paved"] # 320.4 ± 6 assert r["with"]["grid:4"]["paved"] and not r["with"]["grid:2"]["paved"] # 40.3 ~ 80.6 def test_입력_없는_측점_암_경계_기본값(r): rock = [ s["key"] for s in r["stations"] if r["with"][s["key"]]["cut_area_m2"] > 0.01 and s["key"] not in _inputs(r["stations"]) ] assert rock # 깎기 있는 새 측점이 실제로 있음 for key in rock: assert r["with"][key]["rock_boundary_offset_m"] == STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M assert r["without"][key]["cut_soil_area_m2"] == 0 # 기본값이 안 오면 옛 그대로(전부 암) assert any(r["with"][key]["cut_rock_area_m2"] > 0 for key in rock)