"""배수관 세트 거울 테스트 — 파이썬 엔진과 TS 짝이 **같은 값**을 내는지 대조한다. 짝: `B06_Section/B06_Section_Engine_Culvert.py` ↔ `common_util/common_util_culvert_sets.ts` 한쪽만 고치면 화면과 저장본이 갈리므로, 다섯 시설을 모두 태워 딕셔너리째 비교한다. 레지스트리 기본값은 파이썬이 읽은 것을 그대로 TS 에 넘긴다 — 출처가 같아야 기본값 차이가 아니라 **산식 차이**만 잡힌다. """ import json import subprocess import sys from pathlib import Path import pytest PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402 attach_culvert_sets, pipe_points_file, ) TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" # 다섯 시설 × 기본값 폴백/명시값 섞기. 물넘이포장은 폭 절반이 옆 측점까지 걸친다. POINTS = [ {"chainage_m": 20.0, "facility": "pipe", "options": None}, { "chainage_m": 40.0, "facility": "pipe", "options": { "pipe_diameter_mm": "800", "pipe_kind": "흄관", "inlet_type": "집수정", "inlet_basin_length_m": 2.5, "outlet_revet_height_m": 1.8, "outlet_revet_length_m": 7.5, "outlet_revet_form": "돌쌓기", }, }, { "chainage_m": 60.0, "facility": "ford_bridge", "options": { "ford_width_m": 12.0, "ford_height_m": 0.4, "pipe_count": 2, "wing_in": "있음", "wing_in_length_m": 3.0, "wing_in_angle_deg": 30.0, "wing_out": "없음", }, }, { "chainage_m": 80.0, "facility": "box_culvert", "options": {"body_width_m": 3.0, "body_height_m": 2.5, "wing_out_length_m": 2.0}, }, { "chainage_m": 100.0, "facility": "ford_pavement", "options": {"ford_width_m": 9.0, "ford_height_m": 0.25, "ford_slope_pct": 3.0}, }, { "chainage_m": 120.0, "facility": "revetment", "options": { "side": "좌", "tiers": 2, "inlet_revet_height_m": 1.2, "outlet_revet_length_m": 6.0, "form": "메쌓기", }, }, ] # 물넘이포장(폭 9m)이 ±4.5m 까지 걸치는지 보려고 95·105 측점을 함께 둔다. CHAINAGES = [0.0, 20.0, 40.0, 60.0, 80.0, 95.0, 100.0, 105.0, 120.0, 140.0] _TS_RUNNER = """ import { readFileSync, writeFileSync } from "node:fs"; import { attachCulvertSets, buildCulvertSets } from "./common_util_culvert_sets.js"; const input = JSON.parse(readFileSync(process.argv[2], "utf8")); const sets = buildCulvertSets(input.points, input.registry); const sections = input.chainages.map((chainage) => ({ chainage_m: chainage })); attachCulvertSets(sections, sets); writeFileSync(process.argv[3], JSON.stringify(sections)); """ def _python_sections() -> list[dict]: """파이썬 엔진이 얹은 결과 — 정본 파일을 거쳐 공개 경로로 부른다.""" import tempfile with tempfile.TemporaryDirectory(prefix="culvert_mirror_") as workdir: root = Path(workdir) target = pipe_points_file(root) target.parent.mkdir(parents=True, exist_ok=True) target.write_text(json.dumps({"points": POINTS}, ensure_ascii=False), encoding="utf-8") sections = [{"chainage_m": value} for value in CHAINAGES] attach_culvert_sets(root, sections) return sections def _registry() -> dict: types = structure_type_map() table = {} for type_id in ("pipe", "ford_bridge", "ford_pavement", "box_culvert"): entry = types.get(type_id) table[type_id] = {option.key: option.default for option in entry.options} if entry else {} return table def _ts_sections(tmp_path: Path) -> list[dict]: """TS 짝을 프로젝트 tsc 로 옮겨 실제 코드를 그대로 돌린다.""" out = tmp_path / "js" subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(TSC), str(PROJECT_ROOT / "common_util" / "common_util_culvert_sets.ts"), "--outDir", str(out), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", ], cwd=str(PROJECT_ROOT), check=True, capture_output=True, ) (out / "runner.mjs").write_text(_TS_RUNNER, encoding="utf-8") payload = tmp_path / "input.json" result = tmp_path / "output.json" payload.write_text( json.dumps({"points": POINTS, "registry": _registry(), "chainages": CHAINAGES}), encoding="utf-8", ) subprocess.run( # noqa: S603 ["node", str(out / "runner.mjs"), str(payload), str(result)], cwd=str(PROJECT_ROOT), check=True, capture_output=True, ) return json.loads(result.read_text(encoding="utf-8")) @pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") def test_culvert_sets_match_between_python_and_ts(tmp_path: Path) -> None: expected = _python_sections() actual = _ts_sections(tmp_path) assert len(actual) == len(expected) for index, (left, right) in enumerate(zip(expected, actual, strict=True)): assert left == right, f"{CHAINAGES[index]}m 측점 세트가 갈렸다" # 시설이 실제로 붙었는지 — 빈 결과끼리 같아서 통과하는 것을 막는다. assert sum(1 for item in expected if len(item) > 1) >= 6