Files
Aislo/resources/tester/test_530_3_plan_logic.py
T
eomsangdonandClaude Opus 5.5 bf77b70be9 feat(D 예정노선): 저장 꼴에 지점 이름 · 선 이름 · 노드 기록 칸 · [저장] 몸 기준점 (PLAN 594-1 · 594-3 · 요구 463)
- 파일 points [{x, y, name?}] · lines[].name? · lines[].nodes?(꼭짓점 수와 같음 · kind free/length/slope/both · length_m · slope_pct) — 칸 없는 옛 파일 그대로 읽힘
- PUT 몸 point_names · site{lon, lat, address?} — 기준점이 바뀌면 같은 트랜잭션에서 projects.site_* 고침(주소 = 서버 VWorld · 못 찾으면 몸 주소)
- 이름 20 자 · 파일 이름 못 쓰는 글자 막음 — 서버 400(name_error) ↔ TS nameError 짝 · 노드 기록 수 틀림 400
- 선 이름 = 붙인 이름 · 없으면 차례 이름(lineNames ↔ line_names 짝 · 내보내기 조각 이름에 이어짐)
- TS 꼴 NodeKind · NodeRecord · PlanLine.name/nodes · BasePoint.name · ServerNetwork.point_names/site · searchPlaces 틀
- 시험 530_3 이름 거울 · 591_2 왕복 · 400 · 기준점

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

294 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""D03 손 긋기 꼴 다루기(PLAN 562-3 · 563-1 · 580) — TS `logic/D03_PlanAnalysis_Plan.ts`.
조각이 선을 빈틈없이 덮음 · 마크 놓기 · 지우기 · 갈래 차례 밀기 · 갈래 붙은 점 못 뺌 ·
서버 꼴 읽기 · 화면 꼴(선 · 마크 · 조각)을 서버 셈(`derive`)이 그대로 받음 ·
580 = 종점에서 그은 선 뒤집어 잇기 · 갈래로 붙음 · 꺾임점 옮김 · 지움(마크 비율) ·
선 지움 · 방향 바꿈 · 선 통째 연도 · 마크 옮김 · 선 이름 거울(D04 `line_names` · 붙인 이름 594-1) ·
이름 규칙 거울(`nameError` ↔ `name_error` · 594-1).
./venv/Scripts/python.exe -m pytest resources/tester/test_530_3_plan_logic.py -q
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from D03_PlanAnalysis import D03_PlanAnalysis_Network as server # noqa: E402
from D04_PlanConfirm.D04_PlanConfirm_Repository import line_names # noqa: E402
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_Plan.ts"
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
_RUNNER = """
import { writeFileSync } from "node:fs";
import * as plan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.js";
const Y = 2026;
const empty = plan.emptyNetwork();
let n = plan.newLine(empty, [0, 0]);
n = plan.appendVertex(n, 0, [100, 0]);
n = plan.appendVertex(n, 0, [100, 100]);
n = plan.normalizePieces(n, Y);
const one = n;
n = plan.normalizePieces(plan.splitPiece(n, "main", 50), Y);
const split = n;
const tooClose = plan.splitPiece(n, "main", 50.5);
n = plan.normalizePieces(plan.setPieceYear(n, 1, 2027), Y);
n = plan.addBranch(n, 0, 1);
n = plan.appendVertex(n, 1, [160, 0]);
n = plan.normalizePieces(n, Y);
const branched = n;
const inserted = plan.insertVertex(n, 0, 0, [40, 0]);
const blocked = plan.dropLastVertex(inserted, 0);
const dropped = plan.dropLastVertex(n, 1);
const guard = plan.dropLastVertex(plan.dropLastVertex(n, 0), 0);
const removed = plan.normalizePieces(plan.removeMark(n, "main", 50.4, 1), Y);
const grown = plan.normalizePieces(plan.appendVertex(n, 0, [100, 150]), Y);
const read = plan.fromServer(
n.lines,
[{ line: "main", at_m: 50 }, { line: "main", at_m: 150 }],
[{ id: "p9", line: "main", from_m: 0, to_m: 50, year: 2028 }],
Y,
);
// 580 — 본선(0,0)→(100,0) 반쯤 · 종점(200,0)에서 거꾸로 그은 선이 본선 꼬리에 닿음 → 한 선
let j = plan.normalizePieces(plan.appendVertex(plan.newLine(empty, [0, 0]), 0, [100, 0]), Y);
j = plan.newLine(j, [200, 0]);
j = plan.appendVertex(j, 1, [150, 50]);
j = plan.appendVertex(j, 1, [100, 0]);
j = plan.normalizePieces(j, 2027);
const joined = plan.normalizePieces(plan.attachHead(plan.reverseLine(j, 1), 1), Y);
const sameYear = plan.normalizePieces(
plan.attachHead(plan.reverseLine(plan.setLineYear(j, j.lines[1].id, Y), 1), 1),
Y,
);
// 종점에서 그은 선이 본선 꺾임점에 닿음 → 그 점의 갈래(선 차례 = 부모 뒤)
let b = plan.normalizePieces(
plan.appendVertex(plan.appendVertex(plan.newLine(empty, [0, 0]), 0, [100, 0]), 0, [200, 0]),
Y,
);
b = plan.newLine(b, [100, 80]);
b = plan.appendVertex(b, 1, [100, 0]);
const asBranch = plan.attachHead(plan.reverseLine(b, 1), 1);
// 꺾임점 옮김 · 지움 — 마크 비율 · 붙은 갈래 첫 점
let m = plan.normalizePieces(
plan.appendVertex(plan.appendVertex(plan.newLine(empty, [0, 0]), 0, [100, 0]), 0, [100, 100]),
Y,
);
m = plan.normalizePieces(plan.splitPiece(m, "main", 150), Y);
m = plan.normalizePieces(plan.appendVertex(plan.addBranch(m, 0, 1), 1, [160, 0]), Y);
const movedV = plan.normalizePieces(plan.moveVertex(m, 0, 1, [100, -100]), Y);
let r = plan.normalizePieces(
plan.appendVertex(plan.appendVertex(plan.newLine(empty, [0, 0]), 0, [100, 0]), 0, [100, 100]),
Y,
);
r = plan.normalizePieces(plan.splitPiece(r, "main", 150), Y);
const removedV = plan.normalizePieces(plan.removeVertex(r, 0, 1), Y);
const removeBlocked = plan.removeVertex(m, 0, 1);
const lineBlocked = plan.removeLine(m, 0);
const lineGone = plan.normalizePieces(plan.removeLine(m, 1), Y);
const reversed = plan.normalizePieces(plan.reverseLine(m, 0), Y);
const branchFlip = plan.reverseLine(m, 1);
const lineYear = plan.setLineYear(m, "main", 2029);
const markMoved = plan.normalizePieces(plan.moveMark(m, 1, 120), Y);
const markClamp = plan.normalizePieces(plan.moveMark(m, 1, 500), Y);
const names = [
plan.lineNames([{ branch: null }]),
plan.lineNames([{ branch: null }, { branch: null }, { branch: { line: "main", vertex: 1 } }]),
plan.lineNames([{ branch: null }, { branch: { line: "main", vertex: 1 } }]),
plan.lineNames([
{ branch: null, name: " 임도 A " },
{ branch: null },
{ branch: { line: "main", vertex: 1 }, name: "" },
]),
];
const B = String.fromCharCode(92);
const NAME_CASES = ["가".repeat(20), "가".repeat(21), " 임도 ", "", "a/b", "a" + B + "b",
"a:b", "a*b", "a?b", 'a"b', "a<b", "a>b", "a|b", "a" + String.fromCharCode(9) + "b",
"용화-1_(2027)"];
const nameErrors = NAME_CASES.map((name) => [name, plan.nameError(name)]);
writeFileSync(process.argv[2], JSON.stringify({
one, split, tooClose, branched, inserted, blocked, dropped, guard, removed, grown, read,
marks: plan.marksOf(branched),
empty, joined, sameYear, asBranch, m, movedV, removedV, removeBlocked, lineBlocked, lineGone,
reversed, branchFlip, lineYear, markMoved, markClamp, names, nameErrors, NAME_MAX: plan.NAME_MAX,
}));
"""
def _run_ts(tmp_path: Path) -> dict:
out = tmp_path / "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")
result = tmp_path / "out.json"
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(result)],
cwd=str(ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
def _spans(network: dict, line: str = "main") -> list[tuple[float, float, int]]:
return [(p["from_m"], p["to_m"], p["year"]) for p in network["pieces"] if p["line"] == line]
def test_pieces_cover_and_split(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
assert _spans(got["one"]) == [(0, 200, 2026)]
assert _spans(got["split"]) == [(0, 50, 2026), (50, 200, 2026)]
assert [p["id"] for p in got["split"]["pieces"]] == ["p1", "p2"] # 앞 조각이 id 를 지님
assert got["tooClose"] == got["split"] # 마크 1 m 안 = 안 놓음
assert _spans(got["removed"]) == [(0, 200, 2026)] # 마크 지움 = 앞 조각 연도
assert _spans(got["grown"]) == [(0, 50, 2026), (50, 250, 2027)] # 선이 늘면 끝 조각이 늚
assert got["empty"] == {"lines": [], "pieces": []} # 빈 꼴 = 선 없음(시점 · 종점에서 시작)
def test_branch_order_and_guard(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
branch = got["branched"]["lines"][1]
assert branch["branch"] == {"line": "main", "vertex": 1} and branch["vertices"][0] == [100, 0]
# 585-3 갈래 처음 해 = 붙은 자리(본선 100 m = 2027 구간) 해
assert _spans(got["branched"], branch["id"]) == [(0, 60, 2027)]
# 본선 앞 구간에 꺾임점을 끼우면 갈래가 붙은 차례가 한 칸 밀림
assert got["inserted"]["lines"][0]["vertices"][1] == [40, 0]
assert got["inserted"]["lines"][1]["branch"]["vertex"] == 2
assert got["blocked"]["lines"][0]["vertices"][-1] == [100, 0] # 끝 점은 갈래 없음 → 빠짐
assert "갈래가 붙은 점" in got["guard"] # 갈래가 붙은 점은 못 뺌
assert got["dropped"]["lines"][1]["vertices"] == [[100, 0]]
def test_from_server_fills_marks(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
# 조각 없는 구역(마크만) = 처음 연도 · 새 id
assert _spans(got["read"]) == [(0, 50, 2028), (50, 150, 2026), (150, 200, 2026)]
assert got["read"]["pieces"][0]["id"] == "p9"
assert len({p["id"] for p in got["read"]["pieces"]}) == 4 # 갈래 조각 포함 겹침 없음
def test_server_accepts_screen_body(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
body = {**got["branched"], "marks": got["marks"], "epsg": 5186}
derived = server.derive(body, 2026)
assert [(p["line"], p["from_m"], p["to_m"], p["year"]) for p in derived["pieces"]] == [
("main", 0.0, 50.0, 2026),
("main", 50.0, 200.0, 2027),
("branch-1", 0.0, 60.0, 2027),
]
def test_reversed_stroke_joins_or_branches(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
joined = got["joined"]
# 종점에서 그은 선 = 뒤집혀 본선 꼬리에 이어 한 선 · No.0 = 시점 쪽 · 다른 해면 이음 자리 마크
assert len(joined["lines"]) == 1
assert joined["lines"][0]["vertices"] == [[0, 0], [100, 0], [150, 50], [200, 0]]
total = 100 + 2 * (50**2 * 2) ** 0.5
spans = _spans(joined)
assert spans[0] == (0, 100, 2026) and spans[1][0] == 100 and spans[1][2] == 2027
assert abs(spans[1][1] - total) < 1e-3
assert len(got["sameYear"]["pieces"]) == 1 # 같은 해면 이음 자리 마크 없음
branch = got["asBranch"]["lines"][1]
assert branch["branch"] == {"line": "main", "vertex": 1}
assert branch["vertices"] == [[100, 0], [100, 80]]
body = {**got["asBranch"], "marks": [], "pieces": [], "epsg": 5186}
server.derive(body, 2026) # 서버가 그대로 받음(갈래 첫 점 = 부모 점)
body = {**joined, "marks": [], "epsg": 5186}
body["marks"] = [
{"line": p["line"], "at_m": p["from_m"]} for p in joined["pieces"] if p["from_m"] > 0
]
assert len(server.derive(body, 2026)["pieces"]) == 2
def test_vertex_move_remove_keeps_marks(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
# 본선 (0,0)-(100,0)-(100,100) · 마크 150 m(둘째 구간 반) · 갈래 = 꺾임점 1 에서 (160,0)
moved = got["movedV"]
assert moved["lines"][0]["vertices"][1] == [100, -100]
assert moved["lines"][1]["vertices"][0] == [100, -100] # 붙은 갈래 첫 점 같이
first = (100**2 + 100**2) ** 0.5
mark = [p for p in moved["pieces"] if p["line"] == "main"][1]["from_m"]
assert abs(mark - (first + 100)) < 1e-3 # 둘째 구간(길이 200) 반
removed = got["removedV"]
assert removed["lines"][0]["vertices"] == [[0, 0], [100, 100]]
share = 0.75 * (2 * 100**2) ** 0.5 # 합친 구간(200 m) 안 150 m = 3/4
assert abs(_spans(removed)[1][0] - share) < 1e-3
assert "갈래가 붙은 점" in got["removeBlocked"]
assert "갈래가 붙은 선" in got["lineBlocked"]
assert [line["id"] for line in got["lineGone"]["lines"]] == ["main"]
assert all(p["line"] == "main" for p in got["lineGone"]["pieces"])
assert "갈래는 방향을" in got["branchFlip"]
def test_reverse_year_mark_move(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
rev = got["reversed"]
assert rev["lines"][0]["vertices"] == [[100, 100], [100, 0], [0, 0]]
assert rev["lines"][1]["branch"] == {"line": "main", "vertex": 1} # 3 − 1 − 1
assert _spans(rev) == [(0, 50, 2026), (50, 200, 2026)]
assert {p["year"] for p in got["lineYear"]["pieces"] if p["line"] == "main"} == {2029}
assert _spans(got["markMoved"]) == [(0, 120, 2026), (120, 200, 2026)]
assert _spans(got["markClamp"]) == [(0, 199, 2026), (199, 200, 2026)] # 끝에서 1 m 남김
body = {**rev, "marks": [{"line": "main", "at_m": 50}], "epsg": 5186}
server.derive(body, 2026)
def test_line_names_mirror(tmp_path: Path) -> None:
got = _run_ts(tmp_path)
cases = [
[{"branch": None}],
[{"branch": None}, {"branch": None}, {"branch": {"line": "main", "vertex": 1}}],
[{"branch": None}, {"branch": {"line": "main", "vertex": 1}}],
[
{"branch": None, "name": " 임도 A "},
{"branch": None},
{"branch": {"line": "main", "vertex": 1}, "name": ""},
],
]
assert got["names"] == [line_names(c) for c in cases]
assert got["names"][3] == ["임도 A", "본선 2", "갈래 1"] # 붙인 이름 · 빈 이름 = 차례 이름
assert got["names"][1] == ["본선 1", "본선 2", "갈래 1"] # 시점 둘 = 둘째도 본선
assert got["names"][2] == ["본선", "갈래 1"] # 옛 꼴 이름 그대로
def test_name_rule_mirror(tmp_path: Path) -> None:
"""594-1 이름 규칙 — TS `nameError` ↔ 서버 `name_error` 같은 판정 · 20 자 · 금지 글자."""
got = _run_ts(tmp_path)
assert got["NAME_MAX"] == server.NAME_MAX == 20
for name, error in got["nameErrors"]:
assert (error is None) == (server.name_error(name) is None), name
ok = {name for name, error in got["nameErrors"] if error is None}
assert ok == {"가" * 20, " 임도 ", "", "용화-1_(2027)"}