Files
next-tjwater-server/tests/unit/test_hydraulic_playback.py
T
jiang f5f972bbec
Generic Container CI/CD / test-build-publish (push) Failing after 34s
Server CI/CD v2 / build-test-publish-and-deploy (push) Failing after 34s
feat(api): add compact hydraulic playback endpoints
2026-11-15 23:36:29 +08:00

94 lines
3.4 KiB
Python

import asyncio
from datetime import datetime, timezone
import json
import math
import struct
import pytest
from app.services.hydraulic_playback import build_geometry, read_frames, result_factors
def fixture_geometry(longitude=121.5):
return build_geometry([
{"id": "J1", "longitude": longitude, "latitude": 30.8},
{"id": "J2", "longitude": longitude + .001, "latitude": 30.8},
], [{"id": "P1", "start_node_id": "J1", "diameter": 600,
"geometry": {"type": "LineString", "coordinates": [[longitude + .001, 30.8], [longitude, 30.8]]}}],
{"flow": "LPS", "pressure": "METERS"})
def unpack(content):
length = struct.unpack_from("<I", content)[0]
header = json.loads(content[4:4 + length])
count = (len(content) - 4 - length) // 4
return header, struct.unpack_from(f"<{count}f", content, 4 + length)
def test_geometry_version_tracks_absolute_location_and_orients_geometry_with_start_node():
geometry = fixture_geometry()
header, values = unpack(geometry.content)
assert header["nodeIds"] == ["J1", "J2"]
assert header["pipeIds"] == ["P1"]
assert header["segmentCount"] == 1
assert values[4:6] == values[:2] # reversed GIS geometry is oriented J1 -> J2
assert values[6:8] == values[2:4]
assert fixture_geometry().version == geometry.version
assert fixture_geometry(121.6).version != geometry.version
def test_unit_factors_preserve_signed_flow_and_reject_unknown_units():
assert result_factors({"flow": "LPS", "pressure": "METERS"}) == (3.6, 1, 1)
flow, pressure, velocity = result_factors({"flow": "GPM", "pressure": "PSI"})
assert flow == pytest.approx(.003785411784 * 60)
assert pressure == pytest.approx(.7032496149)
assert velocity == .3048
with pytest.raises(ValueError):
result_factors({"flow": "unknown", "pressure": "METERS"})
class Cursor:
def __init__(self, time):
self.time = time
self.rows = []
async def __aenter__(self):
return self
async def __aexit__(self, *_):
pass
async def execute(self, query, params):
assert len(params[0]) == 2
if "node_results" in query:
self.rows = [{"time": self.time, "node_id": "J1", "pressure": 25},
{"time": self.time, "node_id": "J2", "pressure": None}]
else:
self.rows = [{"time": self.time, "link_id": "P1", "flow": -10, "velocity": .7}]
def __aiter__(self):
self.iterator = iter(self.rows)
return self
async def __anext__(self):
try:
return next(self.iterator)
except StopIteration:
raise StopAsyncIteration
class Connection:
def __init__(self, time):
self.time = time
def cursor(self):
return Cursor(self.time)
def test_frames_pack_exact_sample_times_signed_values_and_missing_values_without_extrapolation():
time = datetime(2026, 7, 15, 16, tzinfo=timezone.utc)
content = asyncio.run(read_frames(Connection(time), fixture_geometry(), time, 2, 900))
header, values = unpack(content)
assert header["times"] == [1784131200000, 1784132100000]
assert header["available"] == [True, False]
assert header["validCounts"]["pressure"] == [1, 0]
assert values[0] == 25
assert math.isnan(values[1])
assert values[2] == -36
assert values[3] == pytest.approx(.7)
assert all(math.isnan(value) for value in values[4:])