Files
Aislo/resources/tester/test_533_3_e04_router.py
T

269 lines
9.5 KiB
Python

"""E04 3D 해석 길(PLAN 533-3) — 지문 같으면 안 셈 · force 면 다시 · 끊기 · 진행률 · 저장본 · 공유 403.
./venv/Scripts/python.exe -m pytest resources/tester/test_533_3_e04_router.py -q
"""
import asyncio
import json
import threading
import time
import numpy as np
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from common_util import common_util_auth as auth
from E04_Stab3D import E04_Stab3D_Router as router_mod
from E04_Stab3D import E04_Stab3D_Run as run
PID = "4b1f6f0c-0c52-4a49-9d2b-6c0c3f3e0a11"
STATIONS = [
{"chainage_m": 20.0 * i, "label": f"No.{i}", "center_x": 100.0 + 20.0 * i, "center_y": 200.0}
for i in range(11)
]
def _fake_engine(root, stations, ground, settings, on_progress, should_stop):
"""엔진 자리 — 길 시험은 셈 내용이 아니라 흐름(지문 · 끊기 · 저장)을 봄(엔진 = test_533_3_e04_engine)."""
on_progress(50, "가짜")
f = np.array([[1.4, 1.2, np.nan], [1.1, 0.9, 1.6]])
return {
"grid": {"x0": 100.0, "y0": 200.0, "dx": 1.0, "nx": 3, "ny": 2},
"z": np.full(f.shape, 500.0),
"f": f,
"v": np.where(np.isfinite(f), 120.0, np.nan),
"m": np.where(np.isfinite(f), 0.0, np.nan),
"best": {
"x": 101.0, "y": 201.0, "z": 505.0, "r": 6.0, "ratio": 1.0, "azimuth_deg": 0.0,
"f": 0.9, "volume": 120.0, "method": 0,
"hovland": 0.85, "bishop": 0.9, "janbu_corrected": 0.95, "truncated_ratio": 0.0,
"floor": [],
},
"trials": 7,
"reference_fs": 1.5,
} # fmt: skip
@pytest.fixture
def project(tmp_path, monkeypatch):
src = tmp_path / "E01_StabRegister" / "source"
models = src / "B04_PreProcess" / "models"
models.mkdir(parents=True)
(src / "copy_manifest.json").write_text(json.dumps({"source_fingerprint": "a" * 40}))
xs = np.arange(0.0, 400.0, 2.0)
ys = np.arange(100.0, 300.0, 2.0)
xx, yy = np.meshgrid(xs, ys)
z = 500.0 + 0.4 * (yy - 200.0) + 0.002 * (xx - 200.0) ** 2 # 노선 옆 비탈
np.savez(models / "dtm_classification.npz", x=xs, y=ys, z=z, valid_mask=np.ones_like(z, bool))
monkeypatch.setattr(run.source, "stations", lambda root: STATIONS)
async def fake_root(project_id):
return tmp_path
monkeypatch.setattr(run, "run_engine", _fake_engine)
monkeypatch.setattr(router_mod, "_project_root", fake_root)
router_mod._JOBS.clear()
return tmp_path
@pytest.fixture
def client(project):
app = FastAPI()
app.include_router(router_mod.router)
app.dependency_overrides[auth.require_company] = lambda: {"role": "SYSTEM_ADMIN"}
with TestClient(app) as c:
yield c
def _wait(client, done=lambda p: not p["running"], seconds=10.0):
end = time.time() + seconds
while time.time() < end:
body = client.get(f"/api/projects/{PID}/stab/progress3d").json()
if done(body):
return body
time.sleep(0.05)
raise AssertionError("진행 기다림 초과")
BODY = {"start_m": 0, "end_m": 100, "search": {"box_half_width_m": 20, "cell_m": 2}}
def test_auto_sections():
sections = run.auto_sections(STATIONS)
assert sections == [{"start_m": 0.0, "end_m": 100.0}, {"start_m": 100.0, "end_m": 200.0}]
short = run.auto_sections(STATIONS[:8]) # 0~140 — 꼬리 40 m 는 앞에 붙임
assert short == [{"start_m": 0.0, "end_m": 140.0}]
def test_analyze_cached_force_and_result(client, project, monkeypatch):
calls = []
real = run.run_engine
def counted(*args, **kwargs):
calls.append(1)
return real(*args, **kwargs)
monkeypatch.setattr(run, "run_engine", counted)
first = client.post(f"/api/projects/{PID}/stab/analyze3d", json=BODY).json()
assert first == {"status": "accepted", "key": "0.00_100.00"}
done = _wait(client)
assert done["error"] is None and done["percent"] == 100
assert done["summary"]["cells"] == 5 and done["summary"]["verdict"] == "ng"
assert done["summary"]["by_method"] == {"hovland": 0.85, "bishop": 0.9, "janbu_corrected": 0.95}
# 같은 입력 = 셈하지 않음
again = client.post(f"/api/projects/{PID}/stab/analyze3d", json=BODY).json()
assert again["status"] == "cached" and len(calls) == 1
# force = 다시 셈
assert (
client.post(f"/api/projects/{PID}/stab/analyze3d", json={**BODY, "force": True}).json()[
"status"
]
== "accepted"
)
_wait(client)
assert len(calls) == 2
# 탐색 조건이 바뀌면 지문이 달라 셈
changed = {**BODY, "search": {**BODY["search"], "dr_m": 0.5}}
assert (
client.post(f"/api/projects/{PID}/stab/analyze3d", json=changed).json()["status"]
== "accepted"
)
_wait(client)
assert len(calls) == 3
doc = client.get(f"/api/projects/{PID}/stab/result3d", params={"key": "0.00_100.00"}).json()
grid = doc["grid"]
assert len(doc["f"]) == len(doc["z"]) == len(doc["v"]) == grid["nx"] * grid["ny"]
f = np.array([np.nan if v is None else v for v in doc["f"]])
best = doc["best"]
assert f[best["iy"] * grid["nx"] + best["ix"]] == pytest.approx(np.nanmin(f), abs=1e-3)
assert doc["summary"]["f_min"] == pytest.approx(np.nanmin(f), abs=1e-3)
assert doc["stale"] is False
over = client.get(f"/api/projects/{PID}/stab/result3d").json()
assert [s["key"] for s in over["saved"]] == ["0.00_100.00"] and over["editable"] is True
assert len(over["line"]) == len(STATIONS) and over["sections"][0]["end_m"] == 100.0
# E02 지반을 바꾸면 저장본이 「옛 것」
(project / "E02_StabGround").mkdir()
(project / "E02_StabGround" / "ground.json").write_text(json.dumps({"road_surcharge_kpa": 12}))
assert client.get(f"/api/projects/{PID}/stab/result3d").json()["saved"][0]["stale"] is True
assert (
client.post(f"/api/projects/{PID}/stab/analyze3d", json=changed).json()["status"]
== "accepted"
)
_wait(client)
def test_cancel_and_restart(client, monkeypatch):
started = threading.Event()
def slow(root, stations, ground, settings, on_progress, should_stop):
started.set()
for i in range(500):
if should_stop():
raise InterruptedError
on_progress(i // 5, f"{i}")
time.sleep(0.01)
raise AssertionError("끊기지 않음")
monkeypatch.setattr(run, "run_engine", slow)
assert (
client.post(f"/api/projects/{PID}/stab/analyze3d", json=BODY).json()["status"] == "accepted"
)
assert started.wait(5)
# 도는 중 또 누르면 409
assert client.post(f"/api/projects/{PID}/stab/analyze3d", json=BODY).status_code == 409
assert client.post(f"/api/projects/{PID}/stab/analyze3d/cancel").json()["stopping"] is True
body = _wait(client)
assert body["cancelled"] is True and body["error"] is None
assert (
client.get(f"/api/projects/{PID}/stab/result3d", params={"key": "0.00_100.00"}).status_code
== 404
)
# 다시 시작
monkeypatch.setattr(run, "run_engine", _fake_engine)
assert (
client.post(f"/api/projects/{PID}/stab/analyze3d", json=BODY).json()["status"] == "accepted"
)
body = _wait(client)
assert body["cancelled"] is False and body["percent"] == 100
def test_bad_requests(client):
url = f"/api/projects/{PID}/stab/analyze3d"
assert client.post(url, json={"start_m": 100, "end_m": 50}).status_code == 400
assert client.post(url, json={"start_m": 0, "end_m": 900}).status_code == 400
bad = {**BODY, "search": {"h_min_m": 10, "h_max_m": 5}}
assert client.post(url, json=bad).status_code == 400
assert (
client.get(f"/api/projects/{PID}/stab/result3d", params={"key": "../x"}).status_code == 400
)
class _Cur:
def __init__(self, rows):
self.rows, self.sql = rows, ""
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
async def execute(self, sql, args=None):
self.sql = sql
async def fetchone(self):
return next((v for k, v in self.rows.items() if k in self.sql), None)
class _Pool:
def __init__(self, rows):
self.rows = rows
def acquire(self):
pool = self
class _Conn:
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
def cursor(self, *args):
return _Cur(pool.rows)
return _Conn()
class _Req:
def __init__(self, method):
self.method = method
self.path_params = {"project_id": PID}
def test_shared_company_get_only(monkeypatch):
"""main.py 가 E04 길을 프로젝트 권한 검사에 걸었는지 + 지정받은 회사 = GET 만(POST 403)."""
import main
paths = {
"/api/projects/{project_id}/stab/analyze3d",
"/api/projects/{project_id}/stab/result3d",
}
hits = [r for r in main.app.routes if getattr(r, "path", None) in paths]
assert len(hits) == 2
for route in hits:
calls = {d.call for d in route.dependant.dependencies}
assert auth.require_project_access in calls
user = {"role": "USER", "company_id": 1}
pool = _Pool({"FROM projects": (2,), "plan_route_assignees": (1,)})
monkeypatch.setattr(auth, "get_db_pool", lambda: pool)
assert asyncio.run(auth.require_project_access(_Req("GET"), user)) is user
with pytest.raises(HTTPException) as e:
asyncio.run(auth.require_project_access(_Req("POST"), user))
assert e.value.status_code == 403