⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt` 가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는 시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음. - `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침** - `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시) - 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q` 옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0 ⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
"""배수관 세트 거울 테스트 — 파이썬 엔진과 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
|