Files
Aislo/resources/tester/test_530_2_plan_analyze.py
T
eomsangdonandClaude Opus 5.5 0b738b20e4 feat(638-1): 예정노선 노선 세트(연도-번호) 서버 — 조각 set · 같은 세트 합침 · derived.sets 덩이 · 갈래 · 판 2 · 업체 열쇠 세트 · 확정 막음 · 연결 = 세트 한 줄(요구 547 · 548)
- 파일 pieces[].set · 판 2(옛 서버는 409 로 세트를 못 덮음) · 옛 조각 = 그 해 선 · 거리 차례 번호
- normalize — 같은 선 이웃 같은 세트 하나(앞 id · 마크 걷음) · TS Group.ts 짝 · 거울 사례 = sub6 fixtures 그대로
- derived.sets[] {key, year, no, parts, branch, length_m, pieces}
- D04 단위 = 세트(이름 「2027-1」 · 폴더 「2027-1_<선>」) · 덩이 2 이상 · 갈래 세트는 확정 409
- 업체 열쇠 = 세트 · 옛 조각 id 풀어 봄 · D03 [저장] 때 세트 열쇠로 · 사라진 세트 409
- 연결 = 세트 이은 한 줄 · 아니면 409 · D01 받은 목록 · B01 홈 = 세트 열쇠
- 낡은 시험 고침: 530_2 · 590_1(판 2) · 531_3 · 531_d04 · 563_2(조각 이름 → 세트 이름)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:34:39 +09:00

272 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"] == 2 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": [],
"sets": [],
}
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