- 새 순수 셈 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>
139 lines
5.2 KiB
Python
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"
|