Files
Aislo/resources/tester/test_600_2_plan_measure.py
T
eomsangdonandClaude Sonnet 5.5 7ae441be67 feat(D 예정노선): 거리재기 첫 점이 노선 위면 노선 따라 길이 (PLAN 600-2 · 요구 463-23)
- 새 순수 셈 logic/D03_PlanAnalysis_Measure — 같은 선 누가 차 · 갈래 고리 따라 올라가 만남 · 이어지지 않으면 곧은 거리
- 재기 겹 — 첫 누름 선 8 px · 꺾임점 12 px 안이면 노선 따라 · 굵게 그림 · 글 「노선 따라 L m · 측점 · Δh」
- Page.ts 한 줄(노선 넘김) · 새 시험 test_600_2_plan_measure

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:40:29 +09:00

139 lines
5.2 KiB
Python

"""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"