Files
Aislo/resources/tester/test_763_3_surface_change.py
T
eomsangdonandClaude Opus 5.5 57a6c0ee22 fix(765-8): 벌마다 평면노선 따로 — 받은 노선을 그 벌 지형으로 잘라 벌 예정노선으로 두고 따라감(요구 612)
- 벌 예정노선 terrain_route.csv(벌 몫) — 체인이 받은 노선을 그 지형으로 잘라 한 번 적음 · 초기 폴리라인(merged_follow) · 화면 예상노선 · 고칠 선 끝이 이것을 바탕으로(route_source_path · 없으면 공유 예상노선)
- 공유 예상노선 expected_route.csv = 설계 지형으로 자른 하나(추천 열쇠 그대로) · 고친 예정노선 expected_route_edit.csv 는 벌 몫(다른 벌에 안 번짐)
- 검증_sub5: 라이다 벌 1,352.9 m · 곡선 19(옛 값과 같음) · 수치지형도 벌 2,110.2 m · 곡선 28(예정노선 끝까지) · 칩 왕복 · 벌마다 [초기화] 값 같음 · 공유 예상노선 그대로
- 시험 test_765_design_sets — 벌 몫 가름에 벌 예정노선 · 고친 예정노선 · 벌 예정노선 자름 · 초기 폴리라인 바탕 둘 덧 · 지운 csf 필터 글자를 ptd · classification 으로(637 · 763_3 · 765)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:56:10 +09:00

268 lines
9.9 KiB
Python

"""763-3 (요구 605) B04 분석 · 조합 짓기 = 보기만 · 다른 서피스 [확정] = 업로드와 같은 전처리 체인 ·
같은 서피스 다시 [확정] = 다시 셈 없음 · 체인 실패에도 추천 미리 셈 · B05~B09 는 서피스를 안 바꿈.
./venv/Scripts/python.exe -m pytest resources/tester/test_763_3_surface_change.py -q
"""
import asyncio
import re
from contextlib import asynccontextmanager
from pathlib import Path
from uuid import uuid4
import pytest
from B03_FileInput import B03_FileInput_Service_SurfaceChange as change
from B04_PreProcess import B04_PreProcess_Router as b04
from B04_PreProcess.B04_PreProcess_Schema import SurfaceAnalyzeRequest, SurfaceConfirmRequest
ROOT = Path(__file__).resolve().parents[2]
CONFIRMED = {
"source_filter": "classification",
"method": "implicit",
"smooth": False,
"contour_interval_m": 1.0,
}
class _Cursor:
def __init__(self, log):
self.log = log
async def __aenter__(self):
return self
async def __aexit__(self, *_a):
return False
async def execute(self, sql, *_a):
self.log.append(" ".join(str(sql).split()))
async def fetchone(self):
return None
class _Connection:
def __init__(self, log):
self.log = log
def cursor(self, *_a):
return _Cursor(self.log)
async def begin(self):
self.log.append("BEGIN")
async def commit(self):
self.log.append("COMMIT")
async def rollback(self):
self.log.append("ROLLBACK")
class _Pool:
def __init__(self, log):
self.log = log
@asynccontextmanager
async def acquire(self):
yield _Connection(self.log)
def _async(value):
async def inner(*_a, **_k):
return value
return inner
@pytest.fixture
def b04_world(tmp_path, monkeypatch):
log: list[str] = []
calls: dict[str, list] = {"progress": [], "chain": [], "confirm": [], "contour": []}
las = tmp_path / "raw.las"
las.write_bytes(b"")
monkeypatch.setattr(b04, "get_db_pool", lambda: _Pool(log))
monkeypatch.setattr(b04, "get_project_storage_relative_path", _async("x"))
monkeypatch.setattr(b04, "resolve_stored_project_path", lambda _p: str(tmp_path))
monkeypatch.setattr(b04, "get_input_file", _async({"raw_file_path": "raw.las"}))
monkeypatch.setattr(b04, "list_project_point_cloud_paths", _async([las]))
monkeypatch.setattr(
b04,
"run_surface_analysis",
lambda *_a, **_k: {"ground_summary": {}, "manifest": {"status": "ok"}},
)
monkeypatch.setattr(b04, "save_surface_analysis_to_db", _async([1, 2]))
monkeypatch.setattr(b04, "confirmed_surface_params", _async(dict(CONFIRMED)))
monkeypatch.setattr(
b04, "write_surface_progress", lambda _r, p, stage, _m: calls["progress"].append(stage)
)
monkeypatch.setattr(
b04,
"list_surface_models",
_async(
[
{
"id": 7,
"model_type": "implicit",
"generation_params": {"source_filter": "classification"},
},
{"id": 8, "model_type": "implicit", "generation_params": {"source_filter": "ptd"}},
]
),
)
async def confirm(_c, _pid, model_id, selection):
calls["confirm"].append((model_id, selection["source_filter"]))
async def contour(_c, _pid, value):
calls["contour"].append(value)
async def chain(pid, model_id):
calls["chain"].append(model_id)
monkeypatch.setattr(b04, "confirm_surface_selection", confirm)
monkeypatch.setattr(b04, "update_contour_interval_param", contour)
monkeypatch.setattr(change, "run_surface_change_chain", chain)
calls["log"] = log
return calls
def test_분석은_보기만_단계_확정_그대로(b04_world) -> None:
request = SurfaceAnalyzeRequest(input_file_id=3, source_filters=["ptd"], methods=["implicit"])
answer = asyncio.run(b04.analyze_surface(uuid4(), request))
assert answer.surface_model_ids == [1, 2]
sql = " | ".join(b04_world["log"])
assert "project_workflow_stages" not in sql and "surface_models" not in sql
assert b04_world["progress"][-1] == "completed" # 확정 그대로 — 「확정하세요」 아님
source = (ROOT / "B04_PreProcess/B04_PreProcess_Router.py").read_text(encoding="utf-8")
body = source.split("async def analyze_surface(", 1)[1].split("\n@router.", 1)[0]
for name in ("start_stage", "fail_stage", "update_stage_progress", "schedule_precompute"):
assert name not in body, name
def test_조합_다시_적어도_확정_행_그대로() -> None:
source = (ROOT / "B04_PreProcess/B04_PreProcess_Repository.py").read_text(encoding="utf-8")
assert "status = IF(status = 'CONFIRMED', 'CONFIRMED', 'COMPLETE')" in source
assert "clear_confirmed_surface_models" not in source
def _confirm(model_id: int, contour: float | None = None):
request = SurfaceConfirmRequest(model_id=model_id, smooth=False, contour_interval_m=contour)
async def run():
answer = await b04.confirm_surface(uuid4(), request, {})
await asyncio.sleep(0) # 백그라운드 체인 한 번 돌림
return answer
return asyncio.run(run())
def test_같은_서피스_다시_확정은_다시_셈_없음(b04_world) -> None:
_confirm(7, 2.0)
assert b04_world["confirm"] == [] and b04_world["chain"] == []
assert b04_world["contour"] == [2.0]
def test_다른_서피스_확정은_전처리_체인(b04_world) -> None:
_confirm(8)
assert b04_world["confirm"] == [(8, "ptd")]
assert b04_world["chain"] == [8]
def test_서피스_바꿈_체인_차례(tmp_path, monkeypatch) -> None:
import B03_FileInput.B03_FileInput_Repository as repo
import B03_FileInput.B03_FileInput_Service_Chain as chain
import B05_Profile.B05_Profile_Router_RouteInitial as router
import common_util.common_util_initial_snapshot as snapshot
import common_util.common_util_project_reset as reset
import common_util.common_util_storage as storage
import common_util.common_util_workflow_state as state
import config.config_db as db
log: list = []
async def purge(_c, _pid, _root, keep_preprocess=False):
log.append(("purge", keep_preprocess))
async def stages(_cursor, _pid, first_stage=1):
log.append(("stages", first_stage))
async def auto(_pid, surface_model_id=None):
log.append(("preprocess", surface_model_id))
monkeypatch.setattr(db, "get_db_pool", lambda: _Pool([]))
monkeypatch.setattr(repo, "get_project_storage_relative_path", _async("x"))
monkeypatch.setattr(storage, "resolve_stored_project_path", lambda _p: str(tmp_path))
monkeypatch.setattr(snapshot, "discard_initial_snapshot", lambda _r: log.append("snapshot"))
monkeypatch.setattr(reset, "purge_project_outputs", purge)
monkeypatch.setattr(state, "reset_stages_after_input_change", stages)
monkeypatch.setattr(chain, "run_preprocess_chain", auto)
monkeypatch.setattr(router, "schedule_precompute", lambda pid: log.append("precompute"))
asyncio.run(change.run_surface_change_chain(uuid4(), 8))
assert log == ["snapshot", ("purge", True), ("stages", 2), ("preprocess", 8)]
async def broken(*_a, **_k):
raise RuntimeError("db")
log.clear()
monkeypatch.setattr(reset, "purge_project_outputs", broken)
asyncio.run(change.run_surface_change_chain(uuid4(), 8))
assert log == ["snapshot", "precompute"] # 초기화 실패 = 체인 없음 · 추천만 새 서피스로
@pytest.mark.parametrize("name", ["run_auto_design_chain", "run_preprocess_chain"])
def test_체인_실패_길에도_미리_셈(monkeypatch, name) -> None:
import B03_FileInput.B03_FileInput_Service_Chain as chain
import B05_Profile.B05_Profile_Router_RouteInitial as router
import config.config_db as db
class _Broken:
def acquire(self):
raise RuntimeError("db down")
scheduled: list = []
monkeypatch.setattr(db, "get_db_pool", lambda: _Broken())
monkeypatch.setattr(router, "schedule_precompute", scheduled.append)
project = uuid4()
asyncio.run(getattr(chain, name)(project))
assert scheduled == [project]
def test_LAS_나중_올리기_끝에_미리_셈() -> None:
source = (ROOT / "B05_Profile/B05_Profile_Router_Lidar.py").read_text(encoding="utf-8")
work = source.split("async def work(", 1)[1].split("return lidar_upload.start_job", 1)[0]
assert "schedule_precompute(project_id)" in work
#: B05~B09 에서 확정 서피스를 바꿀 수 있는 부름 — 남은 곳은 까닭 있는 둘만(요구 605-2)
_SURFACE_WRITES = re.compile(
r"\b(confirm_surface_selection|confirm_surface_model|merge_surface_confirmation_params|"
r"switch_design_terrain|stash_las_selection|run_surface_change_chain)\("
)
_ALLOWED = {
# 지형 칩(626-1) — 사용자 답 기다림(763 ①) · 추천 [적용]은 지형을 안 넘김(아래 시험)
"B05_Profile/B05_Profile_Router_Replan.py",
# 라이다 확정값만 적어 둠 · 설계 지형 그대로(637-2)
"B05_Profile/B05_Profile_Router_Lidar.py",
# 지형 칩 = 벌 맞바꿈 · LAS 나중 올리기 뒤 전처리 다시(765-4 · 5 · 요구 605-1 · 606-1)
"B05_Profile/B05_Profile_Router_TerrainSwitch.py",
}
def test_B05_B09_확정은_서피스를_안_바꿈() -> None:
found = set()
for folder in (
"B05_Profile",
"B06_Section",
"B07_DesignDetail",
"B08_Quantity",
"B09_Estimation",
):
for path in (ROOT / folder).glob("*.py"):
if _SURFACE_WRITES.search(path.read_text(encoding="utf-8")):
found.add(path.relative_to(ROOT).as_posix())
assert found <= _ALLOWED, found - _ALLOWED
replan = (ROOT / "B05_Profile/B05_Profile_Router_Replan.py").read_text(encoding="utf-8")
save = replan.split("async def _save_route(", 1)[1].split("\nasync def ", 1)[0]
assert "request.terrain" not in save # 추천 [적용] = 선만 · 설계 지형 그대로(605-2)