SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,155 @@
|
||||
"""HTTP-layer tests for backend/main.py (FastAPI TestClient + real EPANET DLL).
|
||||
|
||||
Run from the repo root: .venv/Scripts/python -m pytest backend/tests -q
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from backend import main
|
||||
from backend.main import app
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
PUBLIC = REPO / "public"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
with TestClient(app, raise_server_exceptions=False) as c:
|
||||
yield c
|
||||
|
||||
|
||||
def post(client, name: str, data: bytes):
|
||||
return client.post("/api/analyze", files={"file": (name, data, "text/plain")})
|
||||
|
||||
|
||||
SIMPLE = b"""[JUNCTIONS]
|
||||
J1 10 5
|
||||
J2 12 3
|
||||
[RESERVOIRS]
|
||||
R1 50
|
||||
[PIPES]
|
||||
P1 R1 J1 1000 200 100
|
||||
P2 J1 J2 1000 150 100
|
||||
[OPTIONS]
|
||||
Units LPS
|
||||
[END]
|
||||
"""
|
||||
|
||||
|
||||
def test_health(client):
|
||||
r = client.get("/api/health")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"status": "ok"}
|
||||
|
||||
|
||||
def test_net1_runs(client):
|
||||
r = post(client, "Net1.inp", (PUBLIC / "Net1.inp").read_bytes())
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert len(body["nodes"]) == 11 and len(body["links"]) == 13
|
||||
assert body["units"] == {"flow": "GPM", "pressure": "psi"}
|
||||
assert body["reportStep"] > 0
|
||||
assert body["engineWarnings"] == []
|
||||
|
||||
|
||||
def test_wrong_extension_rejected(client):
|
||||
assert post(client, "a.txt", SIMPLE).status_code == 400
|
||||
|
||||
|
||||
def test_oversized_upload_rejected(client):
|
||||
r = post(client, "big.inp", b"x" * (main.MAX_UPLOAD_BYTES + 1))
|
||||
assert r.status_code == 413
|
||||
|
||||
|
||||
def test_early_size_check_not_bypassed_by_bad_host(client):
|
||||
# request.url is rebuilt from Host; the early 413 must not depend on it.
|
||||
big = str(main.MAX_UPLOAD_BYTES + 10 * 1024 * 1024)
|
||||
for host in ("localhost", "bad host:abc", "evil.example/../x"):
|
||||
r = client.post("/api/analyze", headers={"content-length": big, "host": host}, content=b"")
|
||||
assert r.status_code == 413, (host, r.status_code)
|
||||
|
||||
|
||||
def test_invalid_inp_returns_json_with_root_cause(client):
|
||||
# Previously: EN_open failure leaked the .rpt handle, cleanup raised
|
||||
# PermissionError on Windows, and the client got plain-text 500.
|
||||
r = post(client, "bad.inp", b"[JUNCTIONS]\n J1 abc\n[END]\n")
|
||||
assert r.status_code == 500
|
||||
assert r.headers["content-type"].startswith("application/json")
|
||||
detail = r.json()["detail"]
|
||||
assert "Error 200" in detail
|
||||
assert "Error 202" in detail # root cause pulled from the report file
|
||||
|
||||
|
||||
def test_warning_codes_collected_even_with_messages_no(client):
|
||||
# J1 sits above the reservoir head -> negative pressure (warning 6).
|
||||
inp = SIMPLE.replace(b" J1 10 5", b" J1 100 5").replace(
|
||||
b"[OPTIONS]", b"[REPORT]\n Messages No\n[OPTIONS]"
|
||||
)
|
||||
r = post(client, "neg.inp", inp)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert "WARNING" not in body["reportText"]
|
||||
assert any(w["code"] == 6 for w in body["engineWarnings"])
|
||||
|
||||
|
||||
def test_step_amplification_rejected_fast(client):
|
||||
inp = SIMPLE.replace(
|
||||
b"[OPTIONS]", b"[TIMES]\n Duration 10000:00\n Hydraulic Timestep 0:00:01\n[OPTIONS]"
|
||||
)
|
||||
r = post(client, "dos.inp", inp)
|
||||
assert r.status_code == 413
|
||||
assert "Simulation too large" in r.json()["detail"]
|
||||
|
||||
|
||||
def test_columnar_format_carries_identical_values(client):
|
||||
import base64
|
||||
from array import array
|
||||
|
||||
data = (PUBLIC / "Net1.inp").read_bytes() # pump + chlorine: every series kind
|
||||
obj = client.post("/api/analyze", files={"file": ("Net1.inp", data, "text/plain")}).json()
|
||||
col = client.post("/api/analyze?format=columnar", files={"file": ("Net1.inp", data, "text/plain")}).json()
|
||||
assert col["format"] == "spipe-columnar-1"
|
||||
steps = len(obj["timeSteps"])
|
||||
for side, keys in (("nodes", ("demand", "head", "pressure", "quality")), ("links", ("flow", "velocity", "headloss", "quality"))):
|
||||
ids = col[side]["ids"]
|
||||
assert ids == list(obj[side])
|
||||
for key in keys:
|
||||
flat = array("d", base64.b64decode(col[side]["series"][key]))
|
||||
for i, element_id in enumerate(ids):
|
||||
assert list(flat[i * steps:(i + 1) * steps]) == obj[side][element_id]["timeSeries"][key]
|
||||
pump = col["links"]["pumps"]["9"]
|
||||
assert pump["energySeries"] == obj["links"]["9"]["timeSeries"]["energy"]
|
||||
assert pump["totalKwh"] == obj["links"]["9"]["totalKwh"]
|
||||
|
||||
|
||||
def test_overlong_utf8_id_is_engine_error(client):
|
||||
# 12 Korean chars = 36 UTF-8 bytes > EPANET MAXID (31 bytes) -> Error 252.
|
||||
long_id = "상리배수지유출관로제일번".encode("utf-8")
|
||||
inp = SIMPLE.replace(b" J2 12 3", b" " + long_id + b" 12 3").replace(b"J1 J2", b"J1 " + long_id)
|
||||
r = post(client, "longid.inp", inp)
|
||||
assert r.status_code == 500
|
||||
assert "252" in r.json()["detail"]
|
||||
|
||||
|
||||
NET1 = (PUBLIC / "Net1.inp").read_bytes()
|
||||
|
||||
|
||||
def post_job(client, name, inp=SIMPLE, params="{}"):
|
||||
return client.post(f"/api/jobs/{name}", files={"file": ("n.inp", inp, "text/plain")}, data={"params": params})
|
||||
|
||||
|
||||
def test_job_summary_runs(client):
|
||||
r = post_job(client, "summary", NET1)
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["junctions"] == 9
|
||||
assert len(body["times"]) == len(body["minPressure"]) == 25
|
||||
assert all(lo <= hi for lo, hi in zip(body["minPressure"], body["maxPressure"]))
|
||||
|
||||
|
||||
def test_job_unknown_and_bad_params(client):
|
||||
assert post_job(client, "nope").status_code == 400
|
||||
assert post_job(client, "summary", params="[1]").status_code == 400
|
||||
assert post_job(client, "summary", params="not json").status_code == 400
|
||||
Reference in new issue
Block a user