Files
Aislo/resources/tester/test_530_2_plan_analyze.py
T
eomsangdonandClaude Opus 5.5 152516bb59 feat(D 예정노선): 옛 DB 설계 칸 읽기 걷음 · 지움 SQL 025(만들기만) — 설계 내용 = 폴더 파일만 (PLAN 590-3 · 요구 458 · 459)
- 옮김 끝 — 파일 없는 PLAN 10 개 = 옛 칸 → plan_network.json 한 번(일회 스크립트 · 11 개 모두 파일 대 DB 대조 같음)
- 저장 모듈 = 파일 · 없으면 빈 설계(옛 칸 · 표 안 읽음) · 옛 연도 노선 접기(legacy_network) · 응답 legacy 칸 걷음
- D04 조각 노선 열쇠 route_json → route(옛 칸 이름과 헷갈림 걷음 · 메모리 값)
- 복제 도구 = 옛 D 표 안 뜸 · 예정노선 설계는 폴더 파일로 따라감
- 024_plan_site_fill.py 지움 — 옛 칸을 읽는 일회 도구 · 기준점 11 개 모두 채움
- SQL 025 = 단계 번호(PLAN_BASE 걷음) · plan_networks · plan_route_pieces · projects.plan_base_json · 연도 줄 노선 칸 · 업체 줄 year 지움 — 공유 DB 적용 안 함(데스크탑 임시 배포가 새 코드를 받은 뒤 · 임시 DB 두 번 돌림 확인)
- 시험: 590_1 옮김 시험 셋 → 파일 없음 = 빈 설계 · 코드에 옛 칸 이름 0 · 530_2 옛 연도 노선 시험 지움(그 길 걷음) · 591_2 옛 지점 꼴 시험 지움 · 591_1 채움 도구 시험 지움(도구 지움) · 573_1 = 파일째 따라감 · 옛 표 목록 밖 허용(025 전) · 531_3 · 563_2 · 593 = 열쇠 이름

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

268 lines
10 KiB
Python

"""D 예정노선 2단계 노선 정본(PLAN 562-2 · 590-1) — 손 그은 선 · 갈래 · 마크 · 조각 →
서버가 다시 셈 · 연도 묶음 · 옛 연도 노선을 선으로 · GET · PUT(산림청만 · 틀린 꼴 거절 ·
업체 지정된 조각 지움 막음) ·
[저장] = 설계 내용 파일 + 연도 상태 줄만(옛 DB 설계 칸 안 씀) · 파일 쓰기 실패 = DB 되돌림.
옛 추천 엔진 시험(합성 지형 갈래 넷 · 저장본 · 나란히 셈)은 엔진과 함께 걷음.
./venv/Scripts/python.exe -m pytest resources/tester/test_530_2_plan_analyze.py -q
"""
import json
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from common_util.common_util_auth import require_company
from D03_PlanAnalysis import D03_PlanAnalysis_Network as net
from D03_PlanAnalysis import D03_PlanAnalysis_Store as store
PID = "12953f5a-5ba1-42ad-a427-8ac857a58bc2"
MAIN = [[0.0, 0.0], [30.0, 40.0], [30.0, 100.0], [90.0, 180.0]] # 50 + 60 + 100 = 210 m
BRANCH = [[30.0, 100.0], [30.0, 150.0]] # 본선 점 2 에서 50 m
def _network(**over) -> dict:
body = {
"epsg": 5186,
"lines": [
{"id": "main", "vertices": MAIN, "branch": None},
{"id": "b1", "vertices": BRANCH, "branch": {"line": "main", "vertex": 2}},
],
"marks": [{"line": "main", "at_m": 75.0}],
"pieces": [
{"id": "p1", "line": "main", "from_m": 0, "to_m": 75.0, "year": 2026},
{"id": "p2", "line": "main", "from_m": 75.004, "to_m": 210.0, "year": 2027},
{"id": "p3", "line": "b1", "from_m": 0, "to_m": 50.0, "year": 2026},
],
}
body.update(over)
return body
def test_derive_pieces_lengths_stations() -> None:
got = net.derive(_network(), 2026)
assert [(x["id"], x["length_m"]) for x in got["lines"]] == [("main", 210), ("b1", 50)]
assert got["lines"][0]["stations"][-2:] == [200, 210] and got["lines"][1]["stations"] == [
0,
20,
40,
50,
]
pieces = {p["id"]: p for p in got["pieces"]}
assert [p["id"] for p in got["pieces"]] == ["p1", "p2", "p3"] # 선 차례 · 앞에서부터
# 경계 = 마크 그대로(보낸 75.004 아님)
assert pieces["p2"]["from_m"] == 75.0 and pieces["p2"]["length_m"] == 135.0
assert pieces["p1"]["planned"] == [[0, 0], [30, 40], [30, 65]]
assert pieces["p1"]["stations"] == [0, 20, 40, 60, 75]
assert pieces["p2"]["stations"][:2] == [75, 80]
assert pieces["p3"]["planned"] == BRANCH # 갈래 측점 = 갈래마다 No.0
@pytest.mark.parametrize(
"over",
[
{"marks": [{"line": "main", "at_m": 210.0}]}, # 마크가 끝
{"marks": [{"line": "zz", "at_m": 10.0}]},
{"marks": [{"line": "main", "at_m": 75.0}, {"line": "main", "at_m": 75.0005}]},
{"pieces": [{"id": "p1", "line": "main", "from_m": 0, "to_m": 80.0, "year": 2026}]},
{"pieces": [{"id": "p1", "line": "main", "from_m": 0, "to_m": 75.0, "year": 2025}]},
{
"pieces": [
{"id": "p1", "line": "main", "from_m": 0, "to_m": 75.0, "year": 2026},
{"id": "p9", "line": "main", "from_m": 0, "to_m": 75.0, "year": 2027},
]
},
{
"lines": [
{"id": "b1", "vertices": BRANCH, "branch": {"line": "main", "vertex": 2}},
{"id": "main", "vertices": MAIN, "branch": None},
],
"marks": [],
"pieces": [],
}, # 부모가 뒤
{
"lines": [
{"id": "main", "vertices": MAIN, "branch": None},
{"id": "b1", "vertices": BRANCH, "branch": {"line": "main", "vertex": 1}},
],
}, # 갈래 첫 점 ≠ 부모 점
{"lines": [{"id": "main", "vertices": [[0, 0], [float("nan"), 1]], "branch": None}]},
],
)
def test_derive_rejects(over) -> None:
with pytest.raises(net.NetworkError):
net.derive(_network(**over), 2026)
def test_year_rows_group_pieces() -> None:
rows = net.year_rows(net.derive(_network(), 2026)["pieces"])
assert list(rows) == [2026, 2027]
first = rows[2026]["route"]
assert first["mode"] == "manual" and [p["id"] for p in first["pieces"]] == ["p1", "p3"]
assert first["planned"] == [[0, 0], [30, 40], [30, 65]] and first["length_m"] == 125.0
assert rows[2026]["start"] == [0, 0] and rows[2026]["end"] == [30, 65]
assert rows[2027]["route"]["start_chainage_m"] == 75.0
class _Cursor:
def __init__(self, db):
self.db = db
self.last = ""
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
async def execute(self, sql, args=None):
self.last = sql
self.db.log.append((sql, args))
async def fetchone(self):
return self._rows()[0] if self._rows() else None
async def fetchall(self):
return self._rows()
def _rows(self):
for key, rows in self.db.answers.items():
if key in self.last:
return rows
return []
class _Connection:
def __init__(self, db):
self.db = db
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
async def begin(self):
pass
async def commit(self):
self.db.log.append(("COMMIT", None))
async def rollback(self):
self.db.log.append(("ROLLBACK", None))
def cursor(self, *args):
return _Cursor(self.db)
class _Db:
def __init__(self):
self.log: list = []
self.answers: dict = {"FROM projects": [(2026, None, None, None)]} # 연도 · 기준점(591-2)
def acquire(self):
return _Connection(self)
BASE = {"epsg": 5186, "points": [{"x": 0.0, "y": 0.0}, {"x": 90.0, "y": 180.0}]}
@pytest.fixture
def client(monkeypatch, tmp_path):
"""파일 = tmp_path 진짜 파일 · 없으면 `old` 설계(지점만) · DB = 가짜 기록."""
db = _Db()
monkeypatch.setattr(net, "get_db_pool", lambda: db)
old = {**store.empty_plan(5186), "points": BASE["points"]}
async def load(project_id):
plan = store.read_plan(tmp_path)
return tmp_path, plan or dict(old)
monkeypatch.setattr(store, "load_plan", load)
app = FastAPI()
app.include_router(net.router)
session = {"role": "USER", "email": "a@forest.go.kr"}
app.dependency_overrides[require_company] = lambda: session
test = TestClient(app)
test.db = db
test.session = session
test.old = old
test.file = store.plan_path(tmp_path)
return test
URL = f"/api/projects/{PID}/plan/network"
def test_get_reads_plan_or_empty(client) -> None:
assert client.get(URL).json()["network"] is None
client.old.update(_network())
got = client.get(URL).json()
assert len(got["derived"]["pieces"]) == 3 and got["network"]["epsg"] == 5186
assert got["base"]["points"][1]["x"] == 90.0 and got["year_start"] == 2026
assert not client.file.exists() # 열기만 = 쓰지 않음
def test_put_writes_file_and_year_status_only(client) -> None:
got = client.put(URL, json=_network())
assert got.status_code == 200 and got.json()["derived"]["pieces"][1]["from_m"] == 75.0
assert got.json()["year_start"] == 2026
saved = json.loads(client.file.read_text("utf-8"))
assert saved["version"] == 1 and saved["epsg"] == 5186
assert saved["points"] == BASE["points"] # 몸에 지점 없음 = 그대로
assert [p["id"] for p in saved["pieces"]] == ["p1", "p2", "p3"]
assert "length_m" not in json.dumps(saved) # 파생 저장 안 함
log = client.db.log
assert log[-1][0] == "COMMIT"
sqls = [s for s, _ in log]
for column in ("plan_route_pieces", "plan_networks", "plan_base_json", "route_json"):
assert not [s for s in sqls if column in s and not s.startswith("SELECT")], column
years = [a for s, a in log if s.startswith("INSERT INTO plan_route_years")]
assert [a[1] for a in years] == [2026, 2027] and all(len(a[2]) == 40 for a in years)
assert ("UPDATE projects SET project_year_end = %s WHERE id = %s", (2027, PID)) in log
stale = [a for s, a in log if "year NOT IN" in s]
assert stale == [(PID, 2026, 2027)] # 없는 연도 줄 지움(빈 줄 남기지 않음)
def test_put_reset_drops_year_rows(client) -> None:
got = client.put(URL, json={"epsg": 5186, "lines": [], "marks": [], "pieces": []})
assert got.status_code == 200 and got.json()["derived"] == {"lines": [], "pieces": []}
sqls = [s for s, _ in client.db.log]
assert "DELETE FROM plan_route_years WHERE project_id = %s" in sqls
assert not any("project_year_end" in s for s in sqls)
assert json.loads(client.file.read_text("utf-8"))["lines"] == []
def test_put_file_failure_rolls_back(client, monkeypatch) -> None:
def broken(root, plan):
raise OSError("디스크")
monkeypatch.setattr(store, "save_plan", broken)
with pytest.raises(OSError):
client.put(URL, json=_network())
assert ("ROLLBACK", None) in client.db.log and ("COMMIT", None) not in client.db.log
def test_put_rejects(client) -> None:
bad = _network(marks=[{"line": "main", "at_m": 500.0}])
assert client.put(URL, json=bad).status_code == 400
assert (
client.put(URL, json=_network(lines=[{"id": "a b", "vertices": MAIN}])).status_code == 422
)
# 업체가 지정된 조각을 지우면 막음 · 파일 안 씀
client.db.answers["piece_id IS NOT NULL"] = [("p2",)]
one = _network(
lines=[{"id": "main", "vertices": MAIN}],
marks=[],
pieces=[{"id": "p1", "line": "main", "from_m": 0, "to_m": 210, "year": 2026}],
)
assert client.put(URL, json=one).status_code == 409
assert not client.file.exists() and ("ROLLBACK", None) in client.db.log
assert not [s for s, _ in client.db.log if s.startswith("INSERT")]
client.db.answers["piece_id IS NOT NULL"] = []
assert client.put(URL, json=_network(epsg=5187)).status_code == 400 # 파일 좌표계와 다름
client.session["email"] = "a@gmail.com"
assert client.put(URL, json=_network()).status_code == 403