# -*- coding: utf-8 -*- """467 · 577-2 (요구 272 · 434 · 437) — 소단 원천 한 벌 = 측점에 얹은 제원(옹벽처럼). 못박는 것 - 측점 얹기 — 구조물 → 측점마다 `section.berm`(덮는 소단 없음 = null · 겹치면 먼저 놓은 것) · TS `attachBermSpecs` ↔ 파이썬 `attach_berm_specs`. - 다시 셈 = 얹힌 제원 · 칸 없음(아직 안 얹음) = 저장분 `design.berm` · null = 소단 없음 — 얹힌 값 = 저장분 값 = [저장] 서버 값(`enforce_design_lines`). - 거울 — TS `bermSpansFromStructures` ↔ 파이썬 `load_berm_spans` · TS `storedBermSpec` ↔ 파이썬 `berm_from`. - 세션 사본(`berm` 칸 · 시설 줄 `syncBerm`) 없음. Node 헬퍼 `helper_272_berm_source.mjs` 가 vite ssr 로 묶은 진짜 모듈을 부름. """ 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)) from B06_Section.B06_Section_Engine_Structures_Wall import ( # noqa: E402 attach_berm_specs, load_berm_spans, ) from B06_Section.B06_Section_Router_Design import berm_from, enforce_design_lines # noqa: E402 from config.config_system import ( # noqa: E402 STANDARD_CROSS_SECTION, STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M, ) def _berm(start, end, **options): return { "type_id": "berm", "placement": "interval", "start_m": start, "end_m": end, "options": options, } SAVED = [_berm(0.0, 100.0, width_m=1.0, interval_m=3.0, slope_deg=0.0, depth_m=1.0)] STRUCTURE_CASES = [ SAVED, [ _berm(50.0, 80.0, width_m=0), # 폭 0 = 없음 _berm(10.0, 30.0, depth_m=""), # 빈 깊이 = 기본 1.0 _berm(30.0, 40.0, depth_m=-2, width_m="0.7"), # 음수 깊이 = 0 · 글 수 _berm(60.0, 70.0, width_m="abc", interval_m=2.5, slope_deg=4), # 못 읽는 폭 = 기본 ], [{"type_id": "berm", "placement": "point", "chainage_m": 25.0, "options": {}}], [], ] STORED_CASES = [ {"width_m": 0.5}, {"width_m": 0.5, "depth_m": -1}, {}, {"width_m": "0.7", "interval_m": 2}, None, {"width_m": None}, {"width_m": ""}, ] GROUND = [ {"offset_m": o / 2, "elevation_m": round(96.0 + 0.02 * o / 2, 4), "valid": True} for o in range(-40, 41) ] STORED_BERM = {"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0} CHAINAGES = [0.0, 10.0, 25.0, 30.0, 35.0, 55.0, 65.0, 100.0, 100.5, None] LONG = { "design_profiles": [ { "samples": [ {"chainage_m": 0, "elevation_m": 100}, {"chainage_m": 200, "elevation_m": 100}, ] } ] } def _project(root: Path, structures: list[dict]) -> Path: route = root / "B05_Profile" / "route" route.mkdir(parents=True) (route / "structures.json").write_text( json.dumps({"revision": 1, "structures": structures}, ensure_ascii=False), encoding="utf-8" ) return root @pytest.fixture(scope="module") def out(tmp_path_factory): work = tmp_path_factory.mktemp("b272berm") build = subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"), "build", "--configLoader", "runner", "--ssr", "../resources/tester/helper_272_berm_source_entry.ts", "--outDir", str(work), "--logLevel", "error", ], cwd=ROOT, capture_output=True, text=True, encoding="utf-8", timeout=300, ) assert build.returncode == 0, build.stderr bundle = work / "bundle.mjs" shutil.copy(work / "helper_272_berm_source_entry.js", bundle) job = work / "job.json" job.write_text( json.dumps( { "standard": STANDARD_CROSS_SECTION, "rockDefault": STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M, "structureCases": STRUCTURE_CASES, "storedCases": STORED_CASES, "ground": GROUND, "storedBerm": STORED_BERM, "chainages": CHAINAGES, }, ensure_ascii=False, ), encoding="utf-8", ) proc = subprocess.run( # noqa: S603 ["node", str(HERE / "helper_272_berm_source.mjs"), str(bundle), 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 test_거울_구조물_소단_구간(out, tmp_path): for index, (structures, ts) in enumerate(zip(STRUCTURE_CASES, out["spans"], strict=True)): py = load_berm_spans(_project(tmp_path / f"c{index}", structures)) assert py is not None, index want = [{"start_m": s, "end_m": e, **spec} for s, e, spec in py] assert len(ts) == len(want), (index, ts, want) for got, exp in zip(ts, want, strict=True): assert got == pytest.approx(exp), (index, got, exp) def test_거울_저장분_소단_제원(out): for spec, ts in zip(STORED_CASES, out["stored"], strict=True): py = berm_from(spec, {}) if py is None: assert ts is None, spec continue want = {k: getattr(py, k) for k in ("width_m", "interval_m", "slope_deg", "depth_m")} assert ts == pytest.approx(want), spec def test_거울_측점_얹기(out, tmp_path): for index, (structures, ts) in enumerate(zip(STRUCTURE_CASES, out["attach"], strict=True)): spans = load_berm_spans(_project(tmp_path / f"a{index}", structures)) py = [{"chainage_m": c} for c in CHAINAGES] attach_berm_specs(py, spans) assert [("berm" in r) for r in ts] == [("berm" in r) for r in py], index for got, exp in zip(ts, py, strict=True): if exp.get("berm") is None: assert got.get("berm") is None, (index, got, exp) else: assert got["berm"] == pytest.approx(exp["berm"]), (index, got, exp) first = out["attach"][0] assert first[1]["berm"] == pytest.approx(STORED_BERM) and first[8]["berm"] is None assert "berm" not in first[-1] # 누가거리 없음 = 칸 없음 def test_다시_셈_B05만_B06_저장_같은_값(out, tmp_path): r = out["refresh"] no_copy, same, empty = r["unattached"], r["attachedSame"], r["attachedNone"] assert no_copy["fill"] == same["fill"] and no_copy["cut"] == same["cut"] # 안 얹음 = 저장분 assert empty["fill"] != no_copy["fill"] # 얹었는데 없음 = 소단 없음(저장분으로 안 물러섬) # [저장] = 서버가 구조물 정본으로 다시 셈 root = _project(tmp_path / "save", SAVED) design = {"ground_type": "soil", "section_mode": "both_fill", "ditch_type": "standard"} sections = [{"chainage_m": 40.0, "samples": GROUND, "design": design}] assert enforce_design_lines(LONG, sections, root) == 1 assert abs(sections[0]["design"]["fill_area_m2"] - same["fill"]) < 1e-3 bare = [{"chainage_m": 40.0, "samples": GROUND, "design": dict(design)}] enforce_design_lines(LONG, bare, _project(tmp_path / "bare", [])) assert abs(bare[0]["design"]["fill_area_m2"] - empty["fill"]) < 1e-3 def test_사본_없음(): for rel in ( "A00_Common/design/design_facility_lane.ts", "A00_Common/b_page_state.ts", "B06_Section/B06_Section_UI_Page_Persist.ts", "B06_Section/B06_Section_Cross_Refresh.ts", "A00_Common/design/design_shared.ts", ): text = (ROOT / rel).read_text(encoding="utf-8") for word in ( "writeBermSpans", "syncBerm", "putBerm", 'readState("berm"', "bermAt:", ): assert word not in text, (rel, word) shape = (ROOT / "common_util" / "common_util_structure_shape_keys.ts").read_text( encoding="utf-8" ) assert '"berm"' in shape def test_시설_줄_종류_표_전_측점_안_칠함(out): assert out["paint"]["before"] == 0 # 종류 표 전 = 빈 측점을 종단도에 안 씀 assert out["paint"]["after"] == 1 # 받은 뒤 한 번