"""347-2 「지금 노선으로 셈」 서버 길 · 347-3 관 자리 후보. - 늦은 차례: 계산 중 새 차례가 오면 단계 사이에서 그만둠(superseded) · 작업본에 안 씀. - 메모리 격자로 세부 유역(파일 안 읽음) · 노선 초안 → 그린 노선. - 관 자리 후보: 최대 간격(100 m)을 넘는 구간에만 · 기존 관 자리엔 안 냄 · 관 목록엔 안 넣음. - [확정](recompute) = 낡았으면 배수유역부터 다시 셈 · [저장] = 안 셈. """ import asyncio import math from pathlib import Path from types import SimpleNamespace from uuid import uuid4 import numpy as np import pytest from shapely.geometry import LineString from B04_PreProcess import B04_PreProcess_Engine_Watershed_Expand as expand from B04_PreProcess import B04_PreProcess_Router_Basins as basins from B04_PreProcess import B04_PreProcess_Router_Watershed_Live as live from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import GridSpec from common_util.common_util_drainage_context import DrainageContext from common_util.common_util_drainage_detail import ( RoadRouting, build_detail, pipes_from_chainages, suggest_pipe_positions, ) from common_util.common_util_route_geometry import RouteVertex VERTICES = [RouteVertex(0.0, 0.0, 100.0, 0.0), RouteVertex(300.0, 0.0, 90.0, 300.0)] def test_draft_vertices_follow_nodes(): draft = {"nodes": [[0, 0], [100, 0], [100, 100]], "curveOn": [False, False, False]} vertices = live.draft_vertices(draft, 30.0) assert vertices[0].x == 0 and vertices[-1].y == 100 assert math.isclose(vertices[-1].chainage_m, 200.0) def test_candidates_only_in_long_gaps(): strength = np.zeros(301) strength[150] = 50.0 # 흐름이 모이는 자리 pipes = pipes_from_chainages(VERTICES, [0.0, 300.0]) picked = suggest_pipe_positions(VERTICES, pipes, strength) assert picked and all(0 < pipe.chainage_m < 300 for pipe in picked) assert all(pipe.chainage_m == round(pipe.chainage_m) for pipe in picked) # 정수 m dense = pipes_from_chainages(VERTICES, [0.0, 90.0, 180.0, 270.0, 300.0]) assert suggest_pipe_positions(VERTICES, dense, strength) == [] def _routing() -> RoadRouting: spec = GridSpec(x_min=-5.0, y_max=5.0, cell_m=1.0, n_rows=10, n_cols=310) road_index = np.arange(5 * 310 + 5, 5 * 310 + 305, dtype=np.int32) road_slot = np.full(spec.size, -1, dtype=np.int32) road_slot[road_index] = np.arange(road_index.size) return RoadRouting( spec=spec, road_slot=road_slot, path_length=np.zeros(spec.size, np.float32), elevation=np.full(spec.size, 95.0, np.float32), road_cell_index=road_index, road_chainage=np.arange(road_index.size, dtype=np.float64), strength=np.ones(road_index.size, np.int64), ) def test_build_detail_uses_memory_routing(tmp_path): missing = tmp_path / "없는 폴더" # 파일을 읽으면 None 이 됨 detail = build_detail(missing, VERTICES, [0.0, 300.0], _routing(), []) assert detail is not None and detail.pipe_candidates assert build_detail(missing, VERTICES, [0.0, 300.0]) is None def _patch_live(monkeypatch, preview_fn): async def prepare(project_id): return {"contours": [], "streams": [], "contour_key": "k"} async def load(project_id): return DrainageContext(stored_path="x", project_root=Path("."), vertices=VERTICES), "" monkeypatch.setattr(live, "_prepare", prepare) monkeypatch.setattr(live, "load_drainage_context", load) monkeypatch.setattr(live, "drainage_dir", lambda stored: Path(".")) monkeypatch.setattr(live, "preview_stages", preview_fn) calls = SimpleNamespace(build=0) def build(*args): calls.build += 1 return None, [] monkeypatch.setattr(live, "_build", build) return calls def test_newer_generation_stops_between_stages(monkeypatch): pid = uuid4() def preview(vertices, contours, streams, store, should_stop): live._LATEST_GENERATION[str(pid)] = 8 # 계산 중 새 조작 → 새 요청이 차례를 올림 if should_stop(): raise expand.AnalysisStopped calls = _patch_live(monkeypatch, preview) result = asyncio.run(live.post_drainage_live(pid, {"generation": 7, "points": []})) assert result == live.SUPERSEDED and calls.build == 0 def test_newer_generation_after_preview_skips_split(monkeypatch): pid = uuid4() def preview(vertices, contours, streams, store, should_stop): assert not should_stop() live._LATEST_GENERATION[str(pid)] = 3 return SimpleNamespace(routing=object()) calls = _patch_live(monkeypatch, preview) result = asyncio.run(live.post_drainage_live(pid, {"generation": 2})) assert result == live.SUPERSEDED and calls.build == 0 def test_engine_stops_in_expansion_round(): contours = [ { "geometry": { "type": "LineString", "coordinates": [ [150 + r * math.cos(t), -150 + r * math.sin(t)] for t in np.linspace(0, 2 * math.pi, 121) ], }, "properties": {"elevation": 300 - r}, } for r in range(10, 200, 10) ] spec = GridSpec(x_min=100.0, y_max=-100.0, cell_m=1.0, n_rows=100, n_cols=100) domain = np.zeros((100, 100), bool) domain[20:80, 20:80] = True route = LineString([(110.0, -150.0), (190.0, -150.0)]) with pytest.raises(expand.AnalysisStopped): expand.expand_by_red_boundary( spec, domain, contours, route, [], None, should_stop=lambda: True ) @pytest.mark.parametrize( "recompute, stale, expected", [(True, True, 1), (True, False, 0), (False, True, 0)] ) def test_confirm_recomputes_only_when_stale(monkeypatch, recompute, stale, expected): import B04_PreProcess.B04_PreProcess_Router_Watershed as watershed calls = [] async def is_stale(project_id): return stale async def region(project_id, refresh=False): calls.append(refresh) return {"status": "success"} async def resolve(*args, **kwargs): return basins.JSONResponse(status_code=404, content={}) monkeypatch.setattr(basins, "_analysis_stale", is_stale) monkeypatch.setattr(watershed, "get_primary_region", region) monkeypatch.setattr(basins, "_resolve", resolve) asyncio.run(basins.put_pipe_points(uuid4(), {"points": []}, None, recompute)) assert len(calls) == expected and all(calls)