162 lines
6.5 KiB
Python
162 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import sys
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from shapely.geometry import Polygon, shape
|
|
|
|
from app.models import VectorFeature
|
|
from app.services.vector_feature_service import VectorFeatureService
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def load_provisioner():
|
|
script_path = ROOT / "scripts" / "provision_mol_municipality_workspace.py"
|
|
spec = importlib.util.spec_from_file_location("mol_municipality_provisioner", script_path)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def feature(feature_id: str, coordinates: list[list[list[float]]]):
|
|
return {
|
|
"type": "Feature",
|
|
"id": feature_id,
|
|
"geometry": {"type": "Polygon", "coordinates": coordinates},
|
|
"properties": {"UIDN": feature_id},
|
|
}
|
|
|
|
|
|
def test_mol_provisioner_uses_official_identity_and_exact_boundary_clipping() -> None:
|
|
module = load_provisioner()
|
|
boundary = Polygon([(5.0, 51.0), (5.2, 51.0), (5.2, 51.2), (5.0, 51.2), (5.0, 51.0)])
|
|
inside = feature("GBG.inside", [[[5.05, 51.05], [5.1, 51.05], [5.1, 51.1], [5.05, 51.1], [5.05, 51.05]]])
|
|
crossing = feature("GBG.crossing", [[[5.18, 51.08], [5.22, 51.08], [5.22, 51.12], [5.18, 51.12], [5.18, 51.08]]])
|
|
outside = feature("GBG.outside", [[[5.3, 51.3], [5.31, 51.3], [5.31, 51.31], [5.3, 51.31], [5.3, 51.3]]])
|
|
pages = [
|
|
(
|
|
{"type": "FeatureCollection", "features": [inside, crossing, outside, inside]},
|
|
"https://geo.api.vlaanderen.be/GRB/page-1",
|
|
)
|
|
]
|
|
|
|
buildings, summary = module.build_municipality_buildings(pages, boundary, max_features=10)
|
|
|
|
assert module.MUNICIPALITY_NIS_CODE == "13025"
|
|
assert module.PROJECT_NAME == "Mol Municipality Workbench"
|
|
assert module.GEOJSON_CRS == {"type": "name", "properties": {"name": "EPSG:4326"}}
|
|
assert len(buildings) == 2
|
|
assert summary["bbox_feature_count"] == 3
|
|
assert summary["outside_boundary_count"] == 1
|
|
assert summary["clipped_at_boundary_count"] == 1
|
|
assert summary["reference_truncated"] is False
|
|
assert all(shape(item["geometry"]).within(boundary) for item in buildings)
|
|
assert buildings[0]["properties"]["coverage_scope"] == "municipality"
|
|
assert buildings[0]["properties"]["source_name"] == "grb"
|
|
assert buildings[0]["properties"]["reference_layer_name"] == "buildings"
|
|
assert buildings[1]["properties"]["clipped_to_municipality"] is True
|
|
|
|
|
|
def test_mol_provisioner_refuses_a_truncated_municipality_dataset() -> None:
|
|
module = load_provisioner()
|
|
boundary = Polygon([(5.0, 51.0), (5.2, 51.0), (5.2, 51.2), (5.0, 51.2), (5.0, 51.0)])
|
|
pages = [
|
|
(
|
|
{
|
|
"type": "FeatureCollection",
|
|
"features": [
|
|
feature("GBG.1", [[[5.01, 51.01], [5.02, 51.01], [5.02, 51.02], [5.01, 51.02], [5.01, 51.01]]]),
|
|
feature("GBG.2", [[[5.03, 51.03], [5.04, 51.03], [5.04, 51.04], [5.03, 51.04], [5.03, 51.03]]]),
|
|
],
|
|
},
|
|
"https://geo.api.vlaanderen.be/GRB/page-1",
|
|
)
|
|
]
|
|
|
|
with pytest.raises(RuntimeError, match="refusing a truncated municipality dataset"):
|
|
module.build_municipality_buildings(pages, boundary, max_features=1)
|
|
|
|
|
|
def test_mol_source_session_retries_only_safe_get_requests() -> None:
|
|
module = load_provisioner()
|
|
|
|
with module.build_source_session() as session:
|
|
retry = session.get_adapter("https://").max_retries
|
|
|
|
assert retry.total == 5
|
|
assert retry.allowed_methods == frozenset({"GET"})
|
|
assert set(retry.status_forcelist) == {429, 500, 502, 503, 504}
|
|
|
|
|
|
def test_large_vector_persistence_flushes_once_without_per_feature_refresh() -> None:
|
|
class FakeSession:
|
|
def __init__(self) -> None:
|
|
self.added = []
|
|
self.flushes = 0
|
|
self.commits = 0
|
|
self.refreshes = 0
|
|
|
|
def add(self, item) -> None:
|
|
self.added.append(item)
|
|
|
|
def flush(self) -> None:
|
|
self.flushes += 1
|
|
|
|
def commit(self) -> None:
|
|
self.commits += 1
|
|
|
|
def refresh(self, _item) -> None:
|
|
self.refreshes += 1
|
|
|
|
payload = {
|
|
"type": "FeatureCollection",
|
|
"features": [
|
|
{
|
|
"type": "Feature",
|
|
"id": f"GBG.{index}",
|
|
"properties": {"layer_type": "building"},
|
|
"geometry": {
|
|
"type": "Polygon",
|
|
"coordinates": [[[5.0, 51.0], [5.001, 51.0], [5.001, 51.001], [5.0, 51.001], [5.0, 51.0]]],
|
|
},
|
|
}
|
|
for index in range(250)
|
|
],
|
|
}
|
|
db = FakeSession()
|
|
|
|
persisted = VectorFeatureService.persist_geojson_features(db, uuid4(), payload, feature_class="buildings")
|
|
|
|
assert len(persisted) == 250
|
|
assert all(isinstance(item, VectorFeature) for item in persisted)
|
|
assert db.flushes == 1
|
|
assert db.commits == 1
|
|
assert db.refreshes == 0
|
|
|
|
|
|
def test_municipality_workspace_is_wired_into_runtime_and_frontend_priority() -> None:
|
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
|
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
|
focus = (ROOT / "frontend" / "src" / "config" / "primaryFocus.ts").read_text(encoding="utf-8")
|
|
project_hook = (ROOT / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8")
|
|
dataset_hook = (ROOT / "frontend" / "src" / "hooks" / "useDatasetWorkflow.ts").read_text(encoding="utf-8")
|
|
map_source = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
|
|
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
|
|
|
assert "py_compile scripts/provision_mol_municipality_workspace.py" in readiness
|
|
assert "COPY scripts/provision_mol_municipality_workspace.py" in dockerfile
|
|
assert "PRIMARY_FOCUS_MUNICIPALITY_PROJECT_NAME = 'Mol Municipality Workbench'" in focus
|
|
assert "items.find(isPrimaryFocusMunicipalityProject)" in project_hook
|
|
assert "datasets.find(isPrimaryFocusMunicipalityBoundaryDataset)" in dataset_hook
|
|
assert "featureCollectionBounds(featureCollection)" in map_source
|
|
assert "useMemo(() => getFeatureCollectionBBox(mapFeatureCollection)" in map_workspace
|
|
assert "Math.min(...xs)" not in map_source
|