"""D03 거리재기 「노선 따라」 셈(PLAN 600-2 · 요구 463-23) — `logic/D03_PlanAnalysis_Measure.ts`. 같은 선 = 누가 차 · 갈래 ↔ 본선 = 갈래 고리를 따라 올라가 만나는 점까지 · 이어지지 않은 본선끼리 = null. TS 를 tsc 로 옮겨 node 로 돌림(서버 · 화면 없이). ./venv/Scripts/python.exe -m pytest resources/tester/test_600_2_plan_measure.py -q """ from __future__ import annotations import json import re import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Measure.ts" TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" # main 200 m (0,0)→(100,0)→(100,100) · b1 = main 꼭짓점 1 에서 60 m · b2 = b1 끝에서 40 m · other = 이어지지 않은 본선 NETWORK = { "pieces": [], "lines": [ {"id": "main", "vertices": [[0, 0], [100, 0], [100, 100]], "branch": None}, {"id": "b1", "vertices": [[100, 0], [160, 0]], "branch": {"line": "main", "vertex": 1}}, {"id": "b2", "vertices": [[160, 0], [160, 40]], "branch": {"line": "b1", "vertex": 1}}, {"id": "other", "vertices": [[0, 500], [50, 500]], "branch": None}, ], } CASES = { "same": ["routeSpan", NETWORK, {"line": "main", "atM": 40}, {"line": "main", "atM": 150}], "same_back": ["routeSpan", NETWORK, {"line": "main", "atM": 150}, {"line": "main", "atM": 40}], "branch_main_after": [ "routeSpan", NETWORK, {"line": "b1", "atM": 30}, {"line": "main", "atM": 150}, ], "branch_main_before": [ "routeSpan", NETWORK, {"line": "b1", "atM": 30}, {"line": "main", "atM": 60}, ], "twig_root": ["routeSpan", NETWORK, {"line": "b2", "atM": 40}, {"line": "main", "atM": 0}], "twig_twig": ["routeSpan", NETWORK, {"line": "b2", "atM": 10}, {"line": "b1", "atM": 20}], "apart": ["routeSpan", NETWORK, {"line": "main", "atM": 10}, {"line": "other", "atM": 10}], "near_branch": ["nearestOnRoute", NETWORK, [130, 5], {"line": "main", "atM": 10}], "near_other_excluded": ["nearestOnRoute", NETWORK, [20, 490], {"line": "main", "atM": 10}], } _RUNNER = """ import { readFileSync, writeFileSync } from "node:fs"; import * as measure from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Measure.js"; const cases = JSON.parse(readFileSync(process.argv[2], "utf8")); const out = {}; for (const [key, [name, ...args]] of Object.entries(cases)) out[key] = measure[name](...args); writeFileSync(process.argv[3], JSON.stringify(out)); """ @pytest.fixture(scope="module") def r(tmp_path_factory: pytest.TempPathFactory) -> dict: tmp = tmp_path_factory.mktemp("measure") out = tmp / "js" subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(TSC), str(TS_FILE), "--outDir", str(out), "--rootDir", str(ROOT), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", ], cwd=str(ROOT), check=True, capture_output=True, ) for emitted in out.rglob("*.js"): emitted.write_text( re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")), encoding="utf-8", ) (out / "runner.mjs").write_text(_RUNNER, encoding="utf-8") job, result = tmp / "job.json", tmp / "out.json" job.write_text(json.dumps(CASES), encoding="utf-8") subprocess.run( # noqa: S603 ["node", str(out / "runner.mjs"), str(job), str(result)], cwd=str(ROOT), check=True, capture_output=True, ) return json.loads(result.read_text(encoding="utf-8")) def test_같은_선은_누가_차(r): assert r["same"]["lengthM"] == pytest.approx(110) assert r["same"]["stations"] == [40, 150] assert r["same"]["path"][0] == [40, 0] assert r["same"]["path"][-1] == [100, 50] assert r["same_back"]["lengthM"] == pytest.approx(110) assert r["same_back"]["path"][0] == [100, 50] def test_갈래에서_본선으로(r): """갈래 30 m 지점 → 갈래 첫 점(30) → 본선 100 m → 150 m(50) = 80 · 본선 60 m 쪽 = 30 + 40 = 70.""" assert r["branch_main_after"]["lengthM"] == pytest.approx(80) assert r["branch_main_after"]["stations"] is None assert r["branch_main_after"]["path"][0] == [130, 0] assert r["branch_main_before"]["lengthM"] == pytest.approx(70) def test_갈래의_갈래는_고리를_따라_올라감(r): """b2 끝(40) → b1 끝(160,0)까지 → b1 첫 점(60) → 본선 0 m(100) = 200 m.""" assert r["twig_root"]["lengthM"] == pytest.approx(200) # b2 10 m(160,10) → b2 첫 점 10 → b1 20 m(120,0) 까지 40 = 50 assert r["twig_twig"]["lengthM"] == pytest.approx(50) def test_이어지지_않은_본선은_길_없음(r): assert r["apart"] is None def test_가까운_자리는_이어진_선_안에서만(r): assert r["near_branch"]["line"] == "b1" assert r["near_branch"]["atM"] == pytest.approx(30) assert r["near_other_excluded"]["line"] == "main"