Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_3' and 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -35,7 +35,9 @@ def test_d03_parts():
|
||||
tools = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts")
|
||||
layers = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts")
|
||||
assert 'createElement("progress")' not in ts and "createProgressBar(" not in ts
|
||||
assert "createProgressCircle({ size: 16, inline: true })" in ts # 604-3 받기 = 위치 칸 제목 서클
|
||||
assert (
|
||||
"createProgressCircle({ size: 16, inline: true })" in ts
|
||||
) # 604-3 받기 = 위치 칸 제목 서클
|
||||
site = _src(
|
||||
"D03_PlanAnalysis/D03_PlanAnalysis_UI_Site.ts"
|
||||
) # 596-2 위치 칸 · 596-5 등록 찾기 = 같은 부품
|
||||
@@ -52,7 +54,8 @@ def test_d03_parts():
|
||||
assert "createToolbar(" not in tools and "createIconButton(" not in tools
|
||||
for act in ("undo", "redo", "view3d", "measure", "stations", "slopes"):
|
||||
assert f'mapToolButton("{act}"' in tools or f'markButton("{act}"' in tools, act
|
||||
assert '"rotate-ccw", "rotate-cw"' in tools
|
||||
table = _src("A00_Common/map/map_toolbar.ts") # 624-2 — 차례 · 보임 = 공용 도구줄 표
|
||||
assert 'mapToolGroups("d03"' in tools and re.search(r'key: "rotate-ccw".*pages: BOTH', table)
|
||||
for b05_only in ("footprint", "station-snap", "contour-minor"):
|
||||
assert f'"{b05_only}"' not in tools, b05_only
|
||||
paint = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts") # 580 — 그림 나눔
|
||||
@@ -60,7 +63,11 @@ def test_d03_parts():
|
||||
# 569-2 · 581-1 · 581-2 — 바탕 겹 단추(공용 지도 도구줄 한 줄 · 묶음 셋) · 처음 켬 셋 · 취향 기억
|
||||
defs = _src("A00_Common/map/map_layer_list.ts") # 624-2 공용 겹 목록
|
||||
bar = _src("A00_Common/map/map_layer_bar.ts")
|
||||
assert "createMapTools({" in bar and "createIconButton({" in bar and "createMapLayerBar({" in layers
|
||||
assert (
|
||||
"createMapTools({" in bar
|
||||
and "createIconButton({" in bar
|
||||
and "createMapLayerBar({" in layers
|
||||
)
|
||||
assert "createToolbar(" not in layers and "createToolbar(" not in bar
|
||||
assert '"aislo:pref:d03.layers"' in defs and "writeByKey(MAP_LAYER_PAGES[page].pref" in bar
|
||||
keys = re.findall(r'key: "(\w+)",\s+icon: "\w+",\s+title:\s+"[^"]+",\s+on: (true|false)', defs)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""PLAN 624-2 — 공용 지도 부품(A00_Common/map) 지킴: 도구줄 표 · 겹 목록 페이지 표 · 범례 · 지형 칩 · D03 은 부르기만.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_624_2_map_parts.py -q
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MAP = ROOT / "A00_Common" / "map"
|
||||
D03 = ROOT / "D03_PlanAnalysis"
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _toolbar_rows() -> list[list[tuple[str, set[str]]]]:
|
||||
"""표 → 묶음마다 (열쇠, 보일 페이지)."""
|
||||
src = _read(MAP / "map_toolbar.ts")
|
||||
pages = {"BOTH": {"d03", "b05"}, "B05": {"b05"}}
|
||||
body = src[src.index("MAP_TOOLBAR") : src.index("const isAct")]
|
||||
groups = re.findall(r"\[\s*(\{.*?\})\s*,?\s*\]", body, re.S)
|
||||
return [
|
||||
[(k, pages[p]) for k, p in re.findall(r'key: "([\w-]+)".*?pages: (\w+)', g, re.S)]
|
||||
for g in groups
|
||||
]
|
||||
|
||||
|
||||
def test_toolbar_table_d03_same_as_before_plus_terrain() -> None:
|
||||
d03 = [[k for k, p in g if "d03" in p] for g in _toolbar_rows()]
|
||||
d03 = [g for g in d03 if g]
|
||||
# 옛 D03 도구줄 세 묶음 그대로 + 끝에 지형 칩
|
||||
assert d03 == [
|
||||
["undo", "redo"],
|
||||
["view3d", "rotate-ccw", "rotate-cw"],
|
||||
["stations", "slopes", "measure"],
|
||||
["terrain"],
|
||||
], d03
|
||||
b05 = {k for g in _toolbar_rows() for k, p in g if "b05" in p}
|
||||
assert {"contour-minor", "footprint", "station-snap", "snap-step", "terrain"} <= b05
|
||||
tools = _read(ROOT / "ui_template" / "ui_template_map_tools.ts")
|
||||
for key in {k for g in _toolbar_rows() for k, _ in g} - {"snap-step", "terrain"}:
|
||||
assert f'"{key}": {{' in tools or f"{key}: {{" in tools, key # 아이콘 · 이름 = MAP_TOOLS
|
||||
|
||||
|
||||
def test_layer_pages() -> None:
|
||||
src = _read(MAP / "map_layer_list.ts")
|
||||
assert 'd03: { hide: [], pref: "aislo:pref:d03.layers" }' in src
|
||||
b05 = re.search(r"b05: \{(.*?)\},", src, re.S).group(1)
|
||||
assert 'hide: ["bands", "forestRoadFile"]' in b05 # A⑨
|
||||
assert 'on: ["contour", "stream"]' in b05 # A② 지금 그대로
|
||||
assert "for (const key of hide) on[key] = false;" in src
|
||||
|
||||
|
||||
def test_common_parts_use_kit_and_no_page_imports() -> None:
|
||||
for path in MAP.glob("*.ts"):
|
||||
src = _read(path)
|
||||
assert len(src.splitlines()) <= 700, path.name
|
||||
for page in ("D03_PlanAnalysis", "B05_Profile", "B04_PreProcess"):
|
||||
assert f"../../{page}" not in src and f"../{page}" not in src, (path.name, page)
|
||||
for raw in ('createElement("button")', 'createElement("input")', 'createElement("select")'):
|
||||
assert raw not in src, (path.name, raw)
|
||||
chip = _read(MAP / "map_terrain_chip.ts")
|
||||
assert "createSegment({" in chip and 'root.dataset.act = "terrain"' in chip
|
||||
assert "createLegend(row)" in _read(MAP / "map_legend.ts")
|
||||
assert "createMapTools({" in _read(MAP / "map_layer_bar.ts")
|
||||
|
||||
|
||||
def test_d03_calls_common_only() -> None:
|
||||
layers = _read(D03 / "D03_PlanAnalysis_UI_Layers.ts")
|
||||
assert (
|
||||
'createMapLayerBar({\n page: "d03"' in layers
|
||||
and 'createMapLegend("d03-plan__legend")' in layers
|
||||
)
|
||||
assert len(layers.splitlines()) < 683 # 줄이기만
|
||||
gl = _read(D03 / "D03_PlanAnalysis_UI_MapGl.ts")
|
||||
assert "mapLayerSpecs(light, pick)" in gl and "function haloLine" not in gl
|
||||
defs = _read(D03 / "D03_PlanAnalysis_UI_LayerDefs.ts")
|
||||
assert "export const GROUPS" not in defs and "readPref" not in defs
|
||||
ui_map = _read(D03 / "D03_PlanAnalysis_UI_Map.ts")
|
||||
assert 'mapToolGroups("d03", {' in ui_map
|
||||
assert 'createTerrainChip({ page: "d03", value: "sheet", have: ["sheet"] })' in ui_map
|
||||
assert not (D03 / "D03_PlanAnalysis_UI_ForestLayers.ts").exists()
|
||||
@@ -0,0 +1,221 @@
|
||||
"""626-2 예정노선 다시 추적 `POST /route/follow` · 고친 예정노선 `expected_route_edit.csv`.
|
||||
|
||||
요구 530 D3 · D4 · D8. 못박는 것
|
||||
- follow = 체인 초기 폴리라인(`_ensure_planned_initial` 파일)과 같은 입력이면 같은 선 · 노드 ·
|
||||
곡선 · 편집표.
|
||||
- follow 는 아무것도 안 씀(프로젝트 폴더 그대로) · 경위도 입력 = TM 입력과 같은 선.
|
||||
- [저장](노선 초안 `expected`) = `expected_route_edit.csv` 씀 · `expected_route.csv` ·
|
||||
초기 폴리라인 · `initial_snapshot/` 그대로 · 없음 = 그대로 · [] = 지움 ·
|
||||
GET `/route/plan` `expected_edited`.
|
||||
- [초기화] = 고친 예정노선 지움(스냅샷에 섞여 있어도).
|
||||
- follow 길은 `/route/replan` · `as_initial` 을 안 부름.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile import B05_Profile_Router_Replan as replan
|
||||
from B05_Profile import B05_Profile_Router_RouteFollow as follow
|
||||
from common_util.common_util_crs import tm_to_lonlat
|
||||
from common_util.common_util_initial_snapshot import snapshot_dir
|
||||
from common_util.common_util_route_geometry import (
|
||||
expected_route_csv_path,
|
||||
expected_route_edit_path,
|
||||
planned_route_initial_path,
|
||||
write_route_csv,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
RADIUS_M = 30.0
|
||||
EPSG = 5186
|
||||
# 예정노선 점 묶음 꼴(3 m 간격 · 굽은 산길) — 사업지 TM
|
||||
LINE = [(200000.0 + 3 * i, 450000.0 + 40 * math.sin(i / 20)) for i in range(160)]
|
||||
|
||||
|
||||
def _patch(monkeypatch, root: Path) -> None:
|
||||
async def paths(_pid):
|
||||
return root, "stored"
|
||||
|
||||
async def criteria(_pid):
|
||||
return RADIUS_M, 0.0, 0.0
|
||||
|
||||
monkeypatch.setattr(replan, "_project_paths", paths)
|
||||
monkeypatch.setattr(replan, "_plan_criteria", criteria)
|
||||
monkeypatch.setattr(follow, "_project_paths", paths)
|
||||
monkeypatch.setattr(follow, "_plan_criteria", criteria)
|
||||
|
||||
|
||||
def _rows(path: Path) -> list[list[float]]:
|
||||
with path.open(encoding="utf-8") as file:
|
||||
return [[float(r["x"]), float(r["y"])] for r in csv.DictReader(file)]
|
||||
|
||||
|
||||
def _tree(root: Path) -> dict[str, str]:
|
||||
return {
|
||||
str(p.relative_to(root)): hashlib.sha1(p.read_bytes()).hexdigest()
|
||||
for p in sorted(root.rglob("*"))
|
||||
if p.is_file()
|
||||
}
|
||||
|
||||
|
||||
def test_follow_은_체인_초기선과_같은_선(tmp_path: Path) -> None:
|
||||
write_route_csv(expected_route_csv_path(tmp_path), [{"x": x, "y": y} for x, y in LINE])
|
||||
assert replan._ensure_planned_initial(tmp_path, RADIUS_M)
|
||||
target = planned_route_initial_path(tmp_path)
|
||||
table = json.loads(replan._curves_path(target).read_text(encoding="utf-8"))
|
||||
# 같은 입력 = 체인이 읽는 예정노선 파일 점(csv 4자리)
|
||||
same = [tuple(p) for p in replan._vertices_of(expected_route_csv_path(tmp_path))]
|
||||
line = follow.follow_route_line(same, RADIUS_M)
|
||||
assert line["planned"] == _rows(target)
|
||||
assert [[round(n["x"], 4), round(n["y"], 4)] for n in line["nodes"]] == _rows(
|
||||
replan._nodes_path(target)
|
||||
)
|
||||
assert line["curves"] == table["curves"] and line["curves"]
|
||||
assert line["node_edits"] == table.get("edits")
|
||||
assert line["station_snap"] is True and table["station_snap"] is True
|
||||
|
||||
|
||||
def test_follow_은_안_씀_경위도도_같은_선(tmp_path: Path, monkeypatch) -> None:
|
||||
_patch(monkeypatch, tmp_path)
|
||||
(tmp_path / "keep.txt").write_text("x", encoding="utf-8")
|
||||
before = _tree(tmp_path)
|
||||
body = follow.RouteFollowRequest(points=LINE)
|
||||
tm = asyncio.run(follow.follow_route(uuid4(), body))
|
||||
assert tm["status"] == "success" and len(tm["nodes"]) >= 3
|
||||
|
||||
class _Pool:
|
||||
@asynccontextmanager
|
||||
async def acquire(self):
|
||||
yield None
|
||||
|
||||
async def epsg(*_a):
|
||||
return EPSG
|
||||
|
||||
monkeypatch.setattr(follow, "get_db_pool", lambda: _Pool())
|
||||
monkeypatch.setattr(follow, "get_surface_crs_epsg", epsg)
|
||||
lonlat = [tm_to_lonlat(x, y, EPSG) for x, y in LINE]
|
||||
geo = asyncio.run(
|
||||
follow.follow_route(uuid4(), follow.RouteFollowRequest(points=lonlat, crs="lonlat"))
|
||||
)
|
||||
assert len(geo["nodes"]) == len(tm["nodes"])
|
||||
assert all(math.dist(a, b) < 0.01 for a, b in zip(geo["planned"], tm["planned"], strict=True))
|
||||
assert _tree(tmp_path) == before # 저장 0
|
||||
short = asyncio.run(follow.follow_route(uuid4(), follow.RouteFollowRequest(points=LINE[:1])))
|
||||
assert short.status_code == 400
|
||||
|
||||
|
||||
def _save(root: Path, expected) -> None:
|
||||
nodes = [LINE[0], LINE[80], LINE[-1]]
|
||||
draft = {"nodes": nodes, "curveOn": [True] * 3, "stationSnap": True, "expected": expected}
|
||||
body = replan.RouteSaveRequest(vertices=[], draft=draft)
|
||||
out = asyncio.run(replan.replan_route(uuid4(), body))
|
||||
assert out["status"] == "success"
|
||||
|
||||
|
||||
def test_저장은_고친_예정노선만_씀(tmp_path: Path, monkeypatch) -> None:
|
||||
_patch(monkeypatch, tmp_path)
|
||||
|
||||
async def recompute(*_a, **_k):
|
||||
return {"route_id": 1}
|
||||
|
||||
monkeypatch.setattr(replan, "_recompute", recompute)
|
||||
write_route_csv(expected_route_csv_path(tmp_path), [{"x": x, "y": y} for x, y in LINE])
|
||||
replan._ensure_planned_initial(tmp_path, RADIUS_M)
|
||||
(snapshot_dir(tmp_path) / "B05_Profile__route").mkdir(parents=True)
|
||||
(snapshot_dir(tmp_path) / "B05_Profile__route" / "planned_route.csv").write_text("x")
|
||||
keep = [expected_route_csv_path(tmp_path), planned_route_initial_path(tmp_path)]
|
||||
fixed = {p: p.read_bytes() for p in keep}
|
||||
shot = _tree(snapshot_dir(tmp_path))
|
||||
|
||||
edited = [list(p) for p in LINE[::10]]
|
||||
_save(tmp_path, edited)
|
||||
assert _rows(expected_route_edit_path(tmp_path)) == [
|
||||
[round(x, 4), round(y, 4)] for x, y in edited
|
||||
]
|
||||
assert asyncio.run(replan.read_route_plan(uuid4()))["expected_edited"] == _rows(
|
||||
expected_route_edit_path(tmp_path)
|
||||
)
|
||||
_save(tmp_path, None) # 없음 = 그대로
|
||||
assert expected_route_edit_path(tmp_path).is_file()
|
||||
_save(tmp_path, []) # 지움
|
||||
assert not expected_route_edit_path(tmp_path).exists()
|
||||
assert asyncio.run(replan.read_route_plan(uuid4()))["expected_edited"] == []
|
||||
assert {p: p.read_bytes() for p in keep} == fixed
|
||||
assert _tree(snapshot_dir(tmp_path)) == shot # 초기값 그대로
|
||||
|
||||
|
||||
def test_초기화는_고친_예정노선_지움(tmp_path: Path, monkeypatch) -> None:
|
||||
from B05_Profile import B05_Profile_Router_Corridor as corridor
|
||||
from B05_Profile import B05_Profile_Router_Lifecycle as lifecycle
|
||||
from common_util import common_util_initial_snapshot as snapshot
|
||||
|
||||
class _Cursor:
|
||||
rowcount = 1
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
return False
|
||||
|
||||
async def execute(self, *_a):
|
||||
pass
|
||||
|
||||
class _Connection:
|
||||
def cursor(self, *_a, **_k):
|
||||
return _Cursor()
|
||||
|
||||
async def begin(self):
|
||||
pass
|
||||
|
||||
async def commit(self):
|
||||
pass
|
||||
|
||||
async def rollback(self):
|
||||
pass
|
||||
|
||||
class _Pool:
|
||||
@asynccontextmanager
|
||||
async def acquire(self):
|
||||
yield _Connection()
|
||||
|
||||
def _async(value):
|
||||
async def run(*_a, **_k):
|
||||
return value
|
||||
|
||||
return run
|
||||
|
||||
# 추천 [적용]이 고친 예정노선째 스냅샷을 뜬 경우도 — 복원 뒤 지움
|
||||
stored = snapshot_dir(tmp_path) / "B05_Profile__route"
|
||||
stored.mkdir(parents=True)
|
||||
(stored / "expected_route_edit.csv").write_text("sequence,x,y\n", encoding="utf-8")
|
||||
(snapshot_dir(tmp_path) / "db.json").write_text(json.dumps({"routes": [{"id": 1}]}))
|
||||
write_route_csv(expected_route_edit_path(tmp_path), [{"x": 0, "y": 0}, {"x": 1, "y": 1}])
|
||||
monkeypatch.setattr(lifecycle, "get_db_pool", lambda: _Pool())
|
||||
monkeypatch.setattr(lifecycle, "get_project_storage_relative_path", _async("p"))
|
||||
monkeypatch.setattr(lifecycle, "resolve_stored_project_path", lambda _p: str(tmp_path))
|
||||
monkeypatch.setattr(lifecycle, "get_latest_route", _async({"id": 9}))
|
||||
monkeypatch.setattr(lifecycle, "start_stage", _async(None))
|
||||
monkeypatch.setattr(snapshot, "restore_initial_snapshot", _async(9))
|
||||
monkeypatch.setattr(corridor, "prune_corridor_files", lambda *_a: 0)
|
||||
response = asyncio.run(lifecycle.reset_route_design(uuid4(), None))
|
||||
assert json.loads(response.body)["restored"] is True
|
||||
assert not expected_route_edit_path(tmp_path).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["B05_Profile_Router_RouteFollow.py"])
|
||||
def test_follow_길은_replan_as_initial_안_부름(name: str) -> None:
|
||||
text = (ROOT / "B05_Profile" / name).read_text(encoding="utf-8")
|
||||
code = text.split('"""', 2)[2] # 머리글 뺀 몸통
|
||||
assert "replan_route" not in code and "as_initial" not in code
|
||||
assert "expected_route_csv_path" not in code and "write_" not in code
|
||||
Reference in New Issue
Block a user