"""시설 속성 붙이기 거울 시험 — 화면 `carryIndices`(B05_Profile_UI_Drainage_Facility.ts) ↔ 서버 `carry_indices`(common_util_drainage_pipes.py) 가 같은 답을 내는지. 규칙: 허용 오차(0.05m) 안 가장 가까운 원본만 · 한 원본은 한 번만 · 못 붙인 관은 속성 없음. 화면 보관소는 관을 끌면(`move`) · 되돌리기로 옮기면(`follow`) 속성을 따라 옮김. """ from __future__ import annotations import json import re import shutil import subprocess from pathlib import Path import pytest from common_util.common_util_drainage_pipes import CARRY_TOLERANCE_M, carry_indices ROOT = Path(__file__).resolve().parents[2] FACILITY = ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility.ts" CULVERT = ROOT / "B06_Section" / "B06_Section_Api_Culvert_Options.ts" CASES = [ ([100.0, 200.0], [100.0, 200.0]), # 제자리 ([100.004], [100.0]), # 반올림 차이 ([95.0, 100.0], [100.0]), # 관 수 > 원본 수 — 앞 관이 BOX 를 가져가지 않음 ([150.37], [150.0]), # 허용 오차 밖 ([100.01, 100.02], [100.0]), # 한 원본은 한 번만 ([100.0], [99.97, 100.02]), # 허용 오차 안 가장 가까운 것 ([100.0, 100.0], [100.06, 100.04]), # 경계 — 0.06 은 밖 ([100.0], []), # 원본 없음 ] def _ts_block(body: str, head: str) -> str: """최상위 선언 하나를 떼어 냄 — 상수는 한 줄 · 함수는 첫 칸 `}` 까지.""" tail = r"[^\n]*;$" if head.startswith("const") else r"[\s\S]*?^\}$" found = re.search(rf"^export {head}{tail}", body, re.M) assert found, head return found.group(0).replace("export ", "") def _run_node(script: str) -> object: result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", script], capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_서버_규칙(): assert CARRY_TOLERANCE_M == 0.05 assert [carry_indices(t, s) for t, s in CASES] == [ [0, 1], [0], [None, 0], [None], [0, None], [1], [1, None], [None], ] @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_화면과_서버가_같은_답(): body = FACILITY.read_text(encoding="utf-8") script = ( _ts_block(body, "const CARRY_TOLERANCE_M") + "\n" + _ts_block(body, "function carryIndices") + f"\nconst cases = {json.dumps(CASES)};" + "\nconsole.log(JSON.stringify([CARRY_TOLERANCE_M," + " cases.map(([t, s]) => carryIndices(t, s))]));" ) tolerance, picks = _run_node(script) assert tolerance == CARRY_TOLERANCE_M assert picks == [carry_indices(t, s) for t, s in CASES] @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_화면_보관소_끌기_되돌리기_때_속성이_따라감(): """BOX(100) · 옵션 없는 관(80) — 관을 끌거나 되돌려도 BOX 속성은 BOX 관에만.""" body = FACILITY.read_text(encoding="utf-8") script = ( "type PipeFacility = string; type PipeSource = string; type DetailPipeInput = any;\n" "interface FacilityAttributes { facility: string; start_m?: number; end_m?: number;" " options?: Record }\n" + "\n".join( _ts_block(body, head) for head in ( "const CARRY_TOLERANCE_M", "function carryIndices", "function createFacilityStore", ) ) + "\n" + re.search(r"^function hasExtras[\s\S]*?^\}$", body, re.M).group(0) + """ const store = createFacilityStore(); store.replaceFromResponse([{ chainage_m: 80 }, { chainage_m: 100, facility: "box_culvert" }]); const kinds = (list) => store.attach(list.map((c) => ({ chainage_m: c, source: "user" }))) .map((p) => p.facility ?? "-"); const out = {}; out.관을_BOX_곁으로 = kinds([99, 100]); // 옵션 없는 관을 BOX 곁으로 끌기(보관소 변화 없음) store.move(100, 130); // BOX 를 끌기 out.BOX_끌기 = kinds([80, 130]); store.follow([80, 130], [80, 100]); // 되돌리기 — BOX 가 제자리로 out.되돌리기 = kinds([80, 100]); store.follow([80, 100], [81, 120]); // 둘이 한꺼번에 바뀜 — 옮기지 않음 out.둘_바뀜 = kinds([81, 120]); console.log(JSON.stringify(out)); """ ) assert _run_node(script) == { "관을_BOX_곁으로": ["-", "box_culvert"], "BOX_끌기": ["-", "box_culvert"], "되돌리기": ["-", "box_culvert"], "둘_바뀜": ["-", "-"], } def test_옛_관_이동_예약은_저장_때_안_씀(): body = CULVERT.read_text(encoding="utf-8") assert "readMoves" not in body and "movedTo" not in body and "moveKey" not in body