"""노면공 포장 제원(88) — 파이썬·TS 짝 거울 시험. `common_util/common_util_structure_pavement.py`(서버 · 저장분) 와 `.ts`(브라우저 · 초안)가 같은 입력에 같은 `section.pavement` 를 내는지 대조. TS 는 프로젝트 tsc 로 옮겨 실제 코드 그대로 돌림. """ import json import subprocess import sys import tempfile from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" from common_util.common_util_structure_pavement import ( # noqa: E402 attach_pavement_specs, pavement_specs_from, ) STRUCTURES = [ { "type_id": "pavement_concrete", "placement": "interval", "chainage_m": None, "start_m": 100.0, "end_m": 160.0, "options": {"pave_spec": "콘크리트", "height_m": 0.25, "widening_off": "120-140, x-1,,"}, }, { # 뒤집힌 구간 · 두께 빔 · 사양 빔 — 두 벌 다 작은 값을 시작으로 · null 로. "type_id": "gravel_surfacing", "placement": "interval", "start_m": 150.0, "end_m": 130.0, "options": {"pave_spec": "", "height_m": "0.3"}, }, {"type_id": "pavement_concrete", "start_m": None, "end_m": None, "options": {}}, {"type_id": "masonry_wet", "start_m": 0.0, "end_m": 500.0, "options": {}}, { "type_id": "pavement_concrete", "chainage_m": 300.0, "options": {"pave_spec": "아스콘", "height_m": 0.05, "widening_off": "290-310"}, }, ] TYPE_IDS = ["pavement_concrete", "gravel_surfacing"] CHAINAGES = [99.97, 100.0, 119.99, 130.0, 145.0, 160.02, 160.05, 300.0, 400.0] _RUNNER = """ import { readFileSync, writeFileSync } from "node:fs"; import { attachPavementSpecs, pavementSpecsFrom } from "./common_util_structure_pavement.js"; const input = JSON.parse(readFileSync(process.argv[2], "utf8")); const sections = input.chainages.map((chainage_m) => ({ chainage_m })); attachPavementSpecs(sections, pavementSpecsFrom(input.structures, new Set(input.type_ids))); writeFileSync(process.argv[3], JSON.stringify(sections)); """ def _python_result() -> list[dict]: sections = [{"chainage_m": value} for value in CHAINAGES] attach_pavement_specs(sections, pavement_specs_from(STRUCTURES, set(TYPE_IDS))) return sections def _ts_result(tmp_path: Path) -> list[dict]: out = tmp_path / "js" subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(TSC), str(PROJECT_ROOT / "common_util" / "common_util_structure_pavement.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(_RUNNER, encoding="utf-8") payload = tmp_path / "input.json" result = tmp_path / "output.json" payload.write_text( json.dumps({"structures": STRUCTURES, "type_ids": TYPE_IDS, "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")) def test_pavement_specs_mirror(): with tempfile.TemporaryDirectory() as workdir: ts = _ts_result(Path(workdir)) py = _python_result() assert ts == py by = {section["chainage_m"]: section.get("pavement") for section in py} # 범위 밖 · 경계 오차(0.02 m) 안 assert by[99.97] is None and by[160.05] is None and by[400.0] is None assert [entry["type_id"] for entry in by[100.0]] == ["pavement_concrete"] assert by[160.02][0]["thickness_m"] == 0.25 # 확폭 끈 구간 120~140 (±0.02) — 끔 · 못 읽는 조각은 버림 assert by[119.99][0]["widening_on"] is False assert by[100.0][0]["widening_on"] is True # 겹친 범위 = 둘 다 · 뒤집힌 구간 정렬 · 문자 두께 · 빈 사양 = null assert [entry["type_id"] for entry in by[145.0]] == ["pavement_concrete", "gravel_surfacing"] gravel = by[145.0][1] assert (gravel["start_m"], gravel["end_m"]) == (130.0, 150.0) assert gravel["thickness_m"] is None and gravel["spec"] is None # 기준 측점만 있는 옛 항목 = 그 측점 한 점 assert by[300.0] == [ { "type_id": "pavement_concrete", "spec": "아스콘", "thickness_m": 0.05, "start_m": 300.0, "end_m": 300.0, "widening_on": False, } ]