Files
Aislo/resources/tester/test_530_2_plan_router.py
T
eomsangdonandClaude Opus 5.5 0c463bcc64 feat(D 예정노선): 지형 받기 서버 — 지적 뺌 · 실패 조각만 · 위치 바꿈 멀다 · 띠 · reset · 칸 상한 · 끊김 · 단계 글 · 임도망 ±5 km (PLAN 595 · 요구 463)
- 지적도를 지형 일에서 뺌(겹 GET 이 받음) · 지적 실패 조각만 다시(열쇠 failed_tiles)
- 받기 몸 center · reset — 5 km 넘음 far · 다른 TM 띠 band 물음 · reset = 옛 범위 · 병합 도엽 · 위성 버림
- 받을 칸 500 만 넘음 413 · extent max_cells · lon/lat
- 진행 기록 파일 — 서버 다시 켜짐 = interrupted(percent < 100 · message 없음) · 받은 적 없음 percent 100
- 진행 단계 글 n/5 · 받은 범위에 epsg · center 기록
- 임도망 자르기 = 기준점 ±5 km ∪ 받은 지형 · 지형 전에도 올림 · bounds · distance_m · 밖뿐 = 거리 글
- 시험 530_1 · 530_2 · 569_1 · 584_3 덧

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

274 lines
11 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.
"""D 지형 API(PLAN 530-1 · 561-2 · 591-2 · 595) — 범위 = 지점(설계 내용 파일) 상자 + 여유 · 지점 ·
선이 없으면 등록 기준점 둘레 ± 1 km · 받기 = 한 벌 · 위치 칸 가운데 = 둘레 덧 · 멀다 · 다른 띠
물음 · 새로(reset) · 칸 상한 413 · 서버 다시 켜짐 = 끊김.
옛 분석 API 시험(GET · PUT analysis · 꺾임점 고침)은 옛 추천 엔진과 함께 걷음(PLAN 562-2) ·
노선 정본 시험 = `test_530_2_plan_analyze.py` · 지형 셈 시험 = `test_530_1_plan_terrain.py`.
./venv/Scripts/python.exe -m pytest resources/tester/test_530_2_plan_router.py -q
"""
import asyncio
import json
from contextlib import asynccontextmanager
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from D03_PlanAnalysis import D03_PlanAnalysis_Router as router_mod
PID = "12953f5a-5ba1-42ad-a427-8ac857a58bc2"
P0, P2 = (0.0, 0.0), (900.0, 400.0)
URL = f"/api/projects/{PID}/plan/terrain"
OX, OY = 200000.0, 500000.0 # 595 시험 = 중부 띠 안(경도 127°)
Q0, Q2 = (OX + P0[0], OY + P0[1]), (OX + P2[0], OY + P2[1])
class _Pool:
@asynccontextmanager
async def acquire(self):
yield None
@pytest.fixture
def client(tmp_path, monkeypatch):
plan = {
"epsg": 5186,
"points": [{"x": P0[0], "y": P0[1]}, {"x": P2[0], "y": P2[1]}],
"lines": [],
}
info = {"year_start": 2026, "site": None}
async def load_plan(project_id):
return tmp_path, plan
async def project_info(project_id):
return info
monkeypatch.setattr(router_mod.store, "load_plan", load_plan)
monkeypatch.setattr(router_mod.network, "project_info", project_info)
monkeypatch.setattr(router_mod, "get_db_pool", lambda: _Pool())
app = FastAPI()
app.include_router(router_mod.router)
test = TestClient(app)
test.plan = plan
test.info = info
test.root = tmp_path
return test
def test_extent_uses_whole_ends(client) -> None:
got = client.get(f"{URL}/extent?margin_m=400").json()
m = 400.0
assert got["margin_m"] == m and got["have"] is None and got["covered"] is False
# 지점 모두(D02 정본) — 연도 구간 아님
assert got["box_m"] == [P0[0] - m, P0[1] - m, P2[0] + m, P2[1] + m]
extra = client.get(f"{URL}/extent", params={"extra": json.dumps([[2000, 0]])}).json()
assert extra["box_m"][2] == 2000 + extra["margin_m"]
assert client.get(f"{URL}/extent", params={"extra": "[1]"}).status_code == 400
client.plan["points"] = [] # 지점 · 기준점 빔
assert client.get(f"{URL}/extent").status_code == 400
def test_extent_site_first(client) -> None:
"""591-2 — 지점 · 선 없음 = 기준점 둘레 ± 1 km(처음 여유 포함) ·
좌표계 = 파일 머리 없으면 기준점 띠."""
from common_util.common_util_crs import lonlat_to_tm
client.plan.update(epsg=None, points=[])
client.info["site"] = {"lon": 127.8, "lat": 36.3, "address": None}
got = client.get(f"{URL}/extent").json()
x, y = lonlat_to_tm(127.8, 36.3, 5186)
r = router_mod.terrain.SITE_RADIUS_M
assert got["epsg"] == 5186
assert got["box_m"] == pytest.approx([x - r, y - r, x + r, y + r])
# 지점이 있으면 기준점은 안 넣음(받은 범위가 이미 덮음 · 옛 프로젝트 그대로)
client.plan.update(epsg=5186, points=[{"x": P0[0], "y": P0[1]}])
got = client.get(f"{URL}/extent?margin_m=400").json()
assert got["box_m"] == [P0[0] - 400, P0[1] - 400, P0[0] + 400, P0[1] + 400]
def test_post_runs_one_set(client, monkeypatch) -> None:
calls: list = []
saved: list = []
works: list = []
def ensure(root, points, epsg, margin_m, report, **kw):
calls.append((points, epsg, margin_m))
return {"sheets": {"downloaded": ["1"]}, "models": [{"model_file_path": "m"}]}
async def save(project_id, models, epsg):
saved.append((models, epsg))
def start(project_id, kind, work, root=None):
works.append(work)
return {"status": "accepted", "kind": kind}
def post(body):
res = client.post(URL, json=body)
asyncio.run(works.pop()(lambda percent, message: None))
return res
monkeypatch.setattr(router_mod.terrain, "ensure_plan_terrain", ensure)
monkeypatch.setattr(router_mod, "_save_models", save)
monkeypatch.setattr(router_mod, "_start_job", start)
res = post({"margin_m": 500, "extra": [[5, 6]]})
assert res.json() == {"status": "accepted", "kind": "terrain"}
assert calls == [([P0, P2, (5.0, 6.0)], 5186, 500.0)]
assert saved == [([{"model_file_path": "m"}], 5186)]
# 옛 화면 꼴(points 몸) — 여유 처음 값 · 전체 시점 · 종점
post({"points": []})
assert calls[-1] == ([P0, P2], 5186, None)
# ── 595 위치 바꿈 · 상한 · 끊김 ──
@pytest.fixture
def posting(client, monkeypatch):
"""POST 받기 — 일은 바로 돌림 · ensure 부름(점 · 좌표계 · reset · center) 기록."""
calls: list = []
def ensure(root, points, epsg, margin_m, report, reset=False, center=None):
calls.append({"points": points, "epsg": epsg, "reset": reset, "center": center})
return {"sheets": {"downloaded": []}, "models": []}
async def save(project_id, models, epsg):
return None
works: list = []
def start(project_id, kind, work, root=None):
works.append(work)
return {"status": "accepted", "kind": kind}
def post(body):
res = client.post(URL, json=body)
while works: # 일은 응답 뒤 돌림(앱 고리 밖)
asyncio.run(works.pop()(lambda percent, message: None))
return res
monkeypatch.setattr(router_mod.terrain, "ensure_plan_terrain", ensure)
monkeypatch.setattr(router_mod, "_save_models", save)
monkeypatch.setattr(router_mod, "_start_job", start)
client.plan["points"] = [{"x": Q0[0], "y": Q0[1]}, {"x": Q2[0], "y": Q2[1]}]
client.calls = calls
client.run_post = post
return client
def _have(client, box, epsg=5186, center=None):
meta = {"box": list(box), "margin_m": 350.0, "epsg": epsg}
if center:
meta["center"] = list(center)
folder = router_mod.terrain.models_dir_of(client.root)
folder.mkdir(parents=True, exist_ok=True)
(folder / router_mod.terrain.TERRAIN_META).write_text(json.dumps(meta), "utf-8")
def test_center_near_adds_site_ring(posting) -> None:
"""가까운 위치 바꿈 = 지점 + 새 가운데 둘레 · 받은 범위와 합침."""
from common_util.common_util_crs import lonlat_to_tm, tm_to_lonlat
_have(posting, (OX - 500.0, OY - 500.0, OX + 1500.0, OY + 1000.0))
lon, lat = tm_to_lonlat(OX + 3000.0, OY, 5186) # 받은 범위에서 1.5 km
res = posting.run_post({"center": {"lon": lon, "lat": lat}}).json()
assert res["status"] == "accepted"
call = posting.calls[-1]
x, y = lonlat_to_tm(lon, lat, 5186)
r = router_mod.terrain.SITE_RADIUS_M - router_mod.terrain.DEFAULT_MARGIN_M
assert call["points"][:2] == [Q0, Q2] and call["points"][2] == pytest.approx((x - r, y - r))
assert call["reset"] is False and call["center"] == (lon, lat)
# extent 에도 캐시 가운데 = 둘레가 상자에 들어감
got = posting.get(f"{URL}/extent", params={"lon": lon, "lat": lat}).json()
assert got["box_m"][2] == pytest.approx(x + router_mod.terrain.SITE_RADIUS_M)
def test_center_far_asks_then_reset(posting) -> None:
"""5 km 넘음 = 「far」(받지 않음) · reset = 옛 범위 버리고 새 ±1 km(지점 안 넣음)."""
from common_util.common_util_crs import tm_to_lonlat
_have(posting, (OX - 500.0, OY - 500.0, OX + 1500.0, OY + 1000.0))
lon, lat = tm_to_lonlat(OX + 8000.0, OY, 5186) # 6.5 km
body = {"center": {"lon": lon, "lat": lat}}
far = posting.run_post(body).json()
assert far["status"] == "far" and far["distance_m"] == pytest.approx(6500, abs=2)
assert "km" in far["message"] and posting.calls == []
res = posting.run_post({**body, "reset": True}).json()
assert res["status"] == "accepted"
call = posting.calls[-1]
assert call["reset"] is True and len(call["points"]) == 2 # 둘레 두 귀만
assert call["points"][0][0] == pytest.approx(OX + 8000 - 650, abs=1)
def test_center_other_band_asks_then_moves(posting) -> None:
"""다른 TM 띠 = 「band」 · 확인(reset) 뒤 그 띠 · 그 뒤엔 받은 지형 띠를 따름."""
posting.plan["lines"] = [{"id": "a"}]
body = {"center": {"lon": 129.1017, "lat": 36.8128}} # 용화(동부 5187)
ask = posting.run_post(body).json()
assert ask == {
"status": "band",
"epsg": 5186,
"center_epsg": 5187,
"message": ask["message"],
}
assert posting.calls == []
# 확인 전 extent = 가운데 무시(옛 띠 그대로)
assert posting.get(f"{URL}/extent", params={"lon": 129.1, "lat": 36.8}).json()["epsg"] == 5186
posting.run_post({**body, "reset": True})
call = posting.calls[-1]
assert call["epsg"] == 5187 and call["reset"] is True and len(call["points"]) == 2
# 받은 뒤([저장] 전) — 가운데 없이 물어도 받은 지형 띠 · 가운데 둘레
(x0, y0), (x1, y1) = call["points"]
box = [x0 - 400, y0 - 400, x1 + 400, y1 + 400]
_have(posting, box, epsg=5187, center=(129.1017, 36.8128))
got = posting.get(f"{URL}/extent").json()
assert got["epsg"] == 5187 and got["covered"] is True
def test_cells_limit_413(posting) -> None:
posting.plan["points"].append(
{"x": OX + 6000.0, "y": OY + 6000.0}
) # 6.7 × 6.7 km = 1,100 만 칸
res = posting.run_post({})
assert res.status_code == 413 and "상한 500만" in res.json()["detail"]
assert posting.calls == []
def test_progress_interrupted_after_restart(client, monkeypatch) -> None:
"""도는 기록이 남았는데 기억에 없음(서버 다시 켜짐) = 끊김 · 받은 적 없음 = percent 100."""
root = client.root
async def tile_root(project_id):
return root
monkeypatch.setattr(router_mod, "_tile_root", tile_root)
url = f"/api/projects/{PID}/plan/progress"
router_mod._JOBS.clear()
idle = client.get(url).json()["terrain"]
assert idle == {**router_mod.IDLE}
path = router_mod._job_path(root, "terrain")
seen = []
async def work(report):
report(65, "5/5 사업 지형면 만드는 중")
seen.append(json.loads(path.read_text("utf-8")))
async def run():
router_mod._start_job(PID, "terrain", work, root)
await asyncio.sleep(0.05)
asyncio.run(run())
assert seen[0]["running"] is True and seen[0]["percent"] == 65 # 도는 동안 기록
assert json.loads(path.read_text("utf-8"))["running"] is False # 끝 기록
path.write_text(json.dumps(seen[0]), "utf-8") # 도는 중 꺼짐
router_mod._JOBS.clear() # 다시 켜짐 = 기억 빔
got = client.get(url).json()["terrain"]
assert got["running"] is False and got["interrupted"] is True
assert got["percent"] == 65 and got["message"] is None and got["error"] is None
# 끝난 기록 = 끊김 아님
path.write_text(json.dumps({**seen[0], "running": False}), "utf-8")
assert client.get(url).json()["terrain"] == {**router_mod.IDLE}