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(" 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:])