mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
142 lines
5.1 KiB
Python
142 lines
5.1 KiB
Python
|
|
"""Unit tests for the shared monitoring-area module (v0.10.2).
|
||
|
|
|
||
|
|
Covers the four exports:
|
||
|
|
- ``MonitoringArea.as_box`` (shapely box construction)
|
||
|
|
- ``build_geom_json`` (all five Geo shapes the archive sees)
|
||
|
|
- ``classify_geom`` (the five verdicts)
|
||
|
|
- ``load_monitoring_area`` (DB read; None on missing-row / NULL-column)
|
||
|
|
|
||
|
|
The classify_geom + as_box assertions are the v0.9.12 archive bbox tests
|
||
|
|
lifted out of ``test_archive_bbox_filter.py`` and expanded with edge cases.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from unittest.mock import AsyncMock, MagicMock
|
||
|
|
from shapely.geometry import Polygon, box as shapely_box
|
||
|
|
|
||
|
|
from central.monitoring_area import (
|
||
|
|
MonitoringArea,
|
||
|
|
build_geom_json,
|
||
|
|
classify_geom,
|
||
|
|
load_monitoring_area,
|
||
|
|
)
|
||
|
|
|
||
|
|
IDAHO = MonitoringArea(north=44.5, south=41.8, east=-111.0, west=-117.5)
|
||
|
|
|
||
|
|
|
||
|
|
def _pt(lon, lat):
|
||
|
|
return json.dumps({"type": "Point", "coordinates": [lon, lat]})
|
||
|
|
|
||
|
|
|
||
|
|
class TestMonitoringAreaAsBox:
|
||
|
|
def test_as_box_returns_shapely_polygon(self):
|
||
|
|
b = IDAHO.as_box()
|
||
|
|
assert isinstance(b, Polygon)
|
||
|
|
|
||
|
|
def test_as_box_corners_match_west_south_east_north(self):
|
||
|
|
# shapely box(minx, miny, maxx, maxy) -> envelope (west, south, east, north)
|
||
|
|
expected = shapely_box(-117.5, 41.8, -111.0, 44.5)
|
||
|
|
assert IDAHO.as_box().equals(expected)
|
||
|
|
|
||
|
|
|
||
|
|
class TestBuildGeomJson:
|
||
|
|
def test_none_input_returns_none(self):
|
||
|
|
assert build_geom_json(None) is None
|
||
|
|
|
||
|
|
def test_empty_dict_returns_none(self):
|
||
|
|
assert build_geom_json({}) is None
|
||
|
|
|
||
|
|
def test_full_geometry_wins_over_bbox(self):
|
||
|
|
# If a real geometry is present it MUST be used verbatim (this is the
|
||
|
|
# v0.9.8 wfigs/tomtom invariant -- the map needs the real shape).
|
||
|
|
geom = {"type": "LineString", "coordinates": [[-114, 43], [-115, 44]]}
|
||
|
|
out = build_geom_json({
|
||
|
|
"geometry": geom,
|
||
|
|
"bbox": [-115, 43, -114, 44],
|
||
|
|
"centroid": [-114.5, 43.5],
|
||
|
|
})
|
||
|
|
assert json.loads(out) == geom
|
||
|
|
|
||
|
|
def test_bbox_rendered_as_closed_polygon(self):
|
||
|
|
out = build_geom_json({"bbox": [-117, 42, -111, 44]})
|
||
|
|
parsed = json.loads(out)
|
||
|
|
assert parsed["type"] == "Polygon"
|
||
|
|
coords = parsed["coordinates"][0]
|
||
|
|
# 5 points: 4 corners + closing duplicate
|
||
|
|
assert len(coords) == 5
|
||
|
|
assert coords[0] == coords[-1]
|
||
|
|
assert coords[0] == [-117, 42]
|
||
|
|
|
||
|
|
def test_centroid_rendered_as_point(self):
|
||
|
|
out = build_geom_json({"centroid": [-114.5, 43.5]})
|
||
|
|
assert json.loads(out) == {"type": "Point", "coordinates": [-114.5, 43.5]}
|
||
|
|
|
||
|
|
def test_partial_bbox_falls_through(self):
|
||
|
|
# An invalid 3-element bbox should not produce a 3-corner polygon;
|
||
|
|
# caller is expected to fall through to centroid or return None.
|
||
|
|
assert build_geom_json({"bbox": [-117, 42, -111]}) is None
|
||
|
|
|
||
|
|
def test_centroid_wins_when_bbox_invalid(self):
|
||
|
|
out = build_geom_json({"bbox": [-117, 42, -111], "centroid": [-114, 43]})
|
||
|
|
assert json.loads(out) == {"type": "Point", "coordinates": [-114, 43]}
|
||
|
|
|
||
|
|
|
||
|
|
class TestClassifyGeom:
|
||
|
|
def test_null_geom_always_kept(self):
|
||
|
|
assert classify_geom(None, IDAHO) == "null-geom"
|
||
|
|
|
||
|
|
def test_null_geom_kept_even_without_area(self):
|
||
|
|
assert classify_geom(None, None) == "null-geom"
|
||
|
|
|
||
|
|
def test_no_area_keeps_everything(self):
|
||
|
|
assert classify_geom(_pt(-114.0, 43.5), None) == "no-area"
|
||
|
|
|
||
|
|
def test_in_bounds_kept(self):
|
||
|
|
assert classify_geom(_pt(-114.0, 43.5), IDAHO) == "in-bounds"
|
||
|
|
|
||
|
|
def test_out_of_bounds_dropped(self):
|
||
|
|
assert classify_geom(_pt(-74.0, 40.7), IDAHO) == "out-of-bounds"
|
||
|
|
|
||
|
|
def test_border_straddling_polygon_kept(self):
|
||
|
|
# Spans the western edge (west=-117.5): partly out, partly in -> kept.
|
||
|
|
poly = json.dumps({
|
||
|
|
"type": "Polygon",
|
||
|
|
"coordinates": [[[-119, 42], [-116, 42], [-116, 43], [-119, 43], [-119, 42]]],
|
||
|
|
})
|
||
|
|
assert classify_geom(poly, IDAHO) == "in-bounds"
|
||
|
|
|
||
|
|
def test_point_exactly_on_border_kept(self):
|
||
|
|
assert classify_geom(_pt(-117.5, 43.0), IDAHO) == "in-bounds"
|
||
|
|
|
||
|
|
def test_unparseable_geom_keeps_failopen(self):
|
||
|
|
assert classify_geom("{not valid json", IDAHO) == "invalid-geom"
|
||
|
|
|
||
|
|
def test_unknown_geom_type_keeps_failopen(self):
|
||
|
|
assert classify_geom(json.dumps({"type": "Nonsense"}), IDAHO) == "invalid-geom"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
class TestLoadMonitoringArea:
|
||
|
|
async def test_returns_area_when_all_columns_set(self):
|
||
|
|
conn = MagicMock()
|
||
|
|
conn.fetchrow = AsyncMock(return_value={
|
||
|
|
"monitor_north": 44.5, "monitor_south": 41.8,
|
||
|
|
"monitor_east": -111.0, "monitor_west": -117.5,
|
||
|
|
})
|
||
|
|
area = await load_monitoring_area(conn)
|
||
|
|
assert area == IDAHO
|
||
|
|
|
||
|
|
async def test_returns_none_when_no_row(self):
|
||
|
|
conn = MagicMock()
|
||
|
|
conn.fetchrow = AsyncMock(return_value=None)
|
||
|
|
assert await load_monitoring_area(conn) is None
|
||
|
|
|
||
|
|
async def test_returns_none_when_any_column_null(self):
|
||
|
|
conn = MagicMock()
|
||
|
|
conn.fetchrow = AsyncMock(return_value={
|
||
|
|
"monitor_north": 44.5, "monitor_south": None,
|
||
|
|
"monitor_east": -111.0, "monitor_west": -117.5,
|
||
|
|
})
|
||
|
|
assert await load_monitoring_area(conn) is None
|