Four ways a selection produced a confident number about a different area than the operator drew: Flood hazard divided the inundated cells by every cell in the drawn rectangle, including cells the VMM raster does not model at all. A selection reaching past the modelled extent therefore reported a diluted risk share, turning missing data into an implied absence of risk. Terrain, bathymetry and thematic raster already divided by valid cells; flood hazard was the outlier. It now reports the three populations separately, states model coverage next to the drawn area, and returns a null fraction rather than a zero when nothing was modelled. geometry_mask selects a cell when its centre falls inside the geometry, so a rectangle smaller than one cell — or one landing between four centres — selected nothing and the analysis returned zeros indistinguishable on screen from "we looked and there is nothing here". On a 100 m population raster a 40 m rectangle over a city block reported no inhabitants. Selection now falls back to the touched cells and says that it did, since the answer then covers more ground than was requested. rasterio.mask applies the same centre rule when cropping, so that call is widened too; the cells that count are still decided by the centre rule wherever it selects anything. The object count treated any feature touching the selection as whole, while intersection_area clipped it — two headline numbers on one panel describing different populations. The count stays whole-feature, which is what "objecten" means to an operator, but now reports how many the edge cuts and is marked an estimate when it does. The area_weighted_sum branch reuses that same count instead of issuing its own near-identical query. Partitioned selection de-duplicated the count on source_feature_id but returned the raw rows, so a building on a municipal boundary was counted once and drawn twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
549 lines
20 KiB
Python
549 lines
20 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from geoalchemy2.shape import from_shape, to_shape
|
|
from shapely.geometry import Polygon, box
|
|
|
|
from app.core.errors import AppError
|
|
from app.models import Dataset, DatasetVersion
|
|
from app.schemas.dataset import DatasetTemporalUpdate
|
|
from app.schemas.temporal import TemporalComparisonRequest, TemporalObjectChanges
|
|
from app.services.dataset_service import DatasetService
|
|
from app.services.temporal_analysis_service import TemporalAnalysisService
|
|
from app.services.vector_feature_service import VectorFeatureService
|
|
|
|
|
|
ROOT = Path(__file__).parents[2]
|
|
|
|
|
|
class ScalarQuery:
|
|
def __init__(self, value: float):
|
|
self.value = value
|
|
|
|
def filter(self, *args): # noqa: ANN002, ARG002
|
|
return self
|
|
|
|
def scalar(self):
|
|
return self.value
|
|
|
|
|
|
class ScalarSession:
|
|
def __init__(self, value: float):
|
|
self.value = value
|
|
|
|
def query(self, *args): # noqa: ANN002, ARG002
|
|
return ScalarQuery(self.value)
|
|
|
|
|
|
class SequenceScalarSession:
|
|
def __init__(self, values: list[float]):
|
|
self.values = iter(values)
|
|
|
|
def query(self, *args): # noqa: ANN002, ARG002
|
|
return ScalarQuery(next(self.values))
|
|
|
|
|
|
class FeatureRowsQuery:
|
|
def __init__(self, rows: list[object]):
|
|
self.rows = rows
|
|
self.row_limit: int | None = None
|
|
|
|
def filter(self, *args): # noqa: ANN002, ARG002
|
|
return self
|
|
|
|
def order_by(self, *args): # noqa: ANN002, ARG002
|
|
return self
|
|
|
|
def limit(self, value: int):
|
|
self.row_limit = value
|
|
return self
|
|
|
|
def all(self):
|
|
return self.rows[: self.row_limit]
|
|
|
|
|
|
class SequentialFeatureSession:
|
|
def __init__(self, row_sets: list[list[object]]):
|
|
self.row_sets = iter(row_sets)
|
|
|
|
def query(self, _model):
|
|
return FeatureRowsQuery(next(self.row_sets))
|
|
|
|
|
|
class VersionQuery:
|
|
def __init__(self, latest: DatasetVersion | None):
|
|
self.latest = latest
|
|
|
|
def filter(self, *args): # noqa: ANN002, ARG002
|
|
return self
|
|
|
|
def order_by(self, *args): # noqa: ANN002, ARG002
|
|
return self
|
|
|
|
def first(self):
|
|
return self.latest
|
|
|
|
|
|
class TemporalUpdateSession:
|
|
def __init__(self, dataset: Dataset, latest: DatasetVersion | None):
|
|
self.dataset = dataset
|
|
self.latest = latest
|
|
self.added: list[object] = []
|
|
|
|
def get(self, model, item_id): # noqa: ANN001
|
|
return self.dataset if model is Dataset and item_id == self.dataset.id else None
|
|
|
|
def query(self, model): # noqa: ANN001
|
|
assert model is DatasetVersion
|
|
return VersionQuery(self.latest)
|
|
|
|
def add(self, item): # noqa: ANN001
|
|
self.added.append(item)
|
|
|
|
def commit(self):
|
|
return None
|
|
|
|
def refresh(self, _item):
|
|
return None
|
|
|
|
|
|
def temporal_dataset(*, project_id, observed_year: int, metric_method: str = "feature_count") -> Dataset:
|
|
return Dataset(
|
|
id=uuid4(),
|
|
project_id=project_id,
|
|
name=f"snapshot-{observed_year}.geojson",
|
|
dataset_type="vector",
|
|
source="official",
|
|
dataset_role="reference",
|
|
temporal_series_key="official:test:mol",
|
|
observed_at=datetime(observed_year, 1, 1, tzinfo=timezone.utc),
|
|
source_version=str(observed_year),
|
|
source_metadata={
|
|
"selection_aggregation": {
|
|
"method": metric_method,
|
|
"label": "Objecten",
|
|
"unit": "objecten",
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
def test_temporal_series_keeps_only_latest_snapshot_per_observation_date() -> None:
|
|
project_id = uuid4()
|
|
old = temporal_dataset(project_id=project_id, observed_year=2025)
|
|
old.imported_at = datetime(2026, 7, 19, tzinfo=timezone.utc)
|
|
latest = temporal_dataset(project_id=project_id, observed_year=2025)
|
|
latest.imported_at = datetime(2026, 7, 21, tzinfo=timezone.utc)
|
|
earlier = temporal_dataset(project_id=project_id, observed_year=2022)
|
|
earlier.imported_at = datetime(2026, 7, 21, tzinfo=timezone.utc)
|
|
|
|
canonical = TemporalAnalysisService._canonical_observation_snapshots([old, latest, earlier])
|
|
|
|
assert [dataset.id for dataset in canonical] == [earlier.id, latest.id]
|
|
|
|
|
|
def governed_grb_dataset(*, project_id, observed_day: int) -> Dataset:
|
|
dataset = temporal_dataset(project_id=project_id, observed_year=2026, metric_method="intersection_area")
|
|
dataset.observed_at = datetime(2026, 7, observed_day, tzinfo=timezone.utc)
|
|
dataset.source_version = f"2026-07-{observed_day:02d}"
|
|
dataset.source_name = "grb"
|
|
dataset.reference_layer_name = "buildings"
|
|
dataset.temporal_series_key = "grb:buildings:kempen-transport-region"
|
|
dataset.source_metadata = {
|
|
"authority_level": "authoritative",
|
|
"collection": "GRB/GBG",
|
|
"coverage_scope": "kempen-transport-region",
|
|
"scope_type": "transport_region",
|
|
"member_count": 28,
|
|
"partition_count": 28,
|
|
"partition_strategy": "municipality_bbox_maximum_boundary_intersection",
|
|
"selection_aggregation": {
|
|
"method": "intersection_area",
|
|
"label": "Bebouwde grondoppervlakte",
|
|
"unit": "ha",
|
|
},
|
|
}
|
|
dataset.provenance_metadata = {
|
|
"operator_tool": "provision_regional_grb_buildings.py",
|
|
"reference_truncated": False,
|
|
"manifest_path": "/storage/operator/grb/manifest.json",
|
|
"source_url": "https://geo.api.vlaanderen.be/GRB/ogc/features/v1/collections/GBG/items",
|
|
"artifact_sha256": "a" * 64,
|
|
"partition_checksums": {f"{index:05d}": "b" * 64 for index in range(28)},
|
|
}
|
|
if observed_day > 14:
|
|
dataset.source_metadata["geometry_clipped_to_area"] = True
|
|
return dataset
|
|
|
|
|
|
def persisted_feature(dataset_id, source_feature_id: str | None, polygon: Polygon):
|
|
return SimpleNamespace(
|
|
id=uuid4(),
|
|
dataset_id=dataset_id,
|
|
source_feature_id=source_feature_id,
|
|
properties_json={},
|
|
geometry=from_shape(polygon, srid=4326),
|
|
)
|
|
|
|
|
|
def test_temporal_migration_and_models_align() -> None:
|
|
migration = (ROOT / "backend/alembic/versions/202607140001_temporal_dataset_foundation.py").read_text(encoding="utf-8")
|
|
for field in (
|
|
"temporal_series_key",
|
|
"observed_at",
|
|
"valid_from",
|
|
"valid_to",
|
|
"temporal_granularity",
|
|
"source_version",
|
|
):
|
|
assert field in migration
|
|
assert hasattr(Dataset, field)
|
|
assert "ix_vector_features_dataset_source_feature" in migration
|
|
assert 'down_revision = "202606120900"' in migration
|
|
|
|
|
|
def test_temporal_metadata_requires_an_explicit_series_and_observation_date() -> None:
|
|
with pytest.raises(AppError, match="observed_at is required"):
|
|
DatasetService._validate_temporal_metadata(
|
|
temporal_series_key="official:test:mol",
|
|
observed_at=None,
|
|
valid_from=None,
|
|
valid_to=None,
|
|
temporal_granularity="year",
|
|
source_version="2024",
|
|
)
|
|
with pytest.raises(AppError, match="valid_to must be"):
|
|
DatasetService._validate_temporal_metadata(
|
|
temporal_series_key="official:test:mol",
|
|
observed_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
|
|
valid_from=datetime(2024, 12, 31, tzinfo=timezone.utc),
|
|
valid_to=datetime(2024, 1, 1, tzinfo=timezone.utc),
|
|
temporal_granularity="year",
|
|
source_version="2024",
|
|
)
|
|
|
|
|
|
def test_temporal_metadata_update_appends_provenance_version_and_is_idempotent() -> None:
|
|
project_id = uuid4()
|
|
dataset = temporal_dataset(project_id=project_id, observed_year=2024)
|
|
dataset.status = "ready"
|
|
dataset.metadata_json = {}
|
|
latest = DatasetVersion(
|
|
dataset_id=dataset.id,
|
|
version=3,
|
|
observed_at=dataset.observed_at,
|
|
source_version="2024",
|
|
)
|
|
session = TemporalUpdateSession(dataset, latest)
|
|
payload = DatasetTemporalUpdate(
|
|
temporal_series_key="official:test:mol",
|
|
observed_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
|
|
temporal_granularity="year",
|
|
source_version="2025",
|
|
)
|
|
|
|
updated = DatasetService.update_temporal_metadata(session, dataset.id, payload)
|
|
|
|
assert updated.observed_at == payload.observed_at
|
|
assert latest.version == 3
|
|
assert latest.observed_at == datetime(2024, 1, 1, tzinfo=timezone.utc)
|
|
assert len(session.added) == 2
|
|
appended = session.added[1]
|
|
assert isinstance(appended, DatasetVersion)
|
|
assert appended.version == 4
|
|
assert appended.observed_at == payload.observed_at
|
|
|
|
session.added.clear()
|
|
DatasetService.update_temporal_metadata(session, dataset.id, payload)
|
|
assert session.added == []
|
|
|
|
|
|
def test_selection_area_aggregation_returns_hectares_without_loading_all_features() -> None:
|
|
project_id = uuid4()
|
|
dataset = temporal_dataset(project_id=project_id, observed_year=1969, metric_method="intersection_area")
|
|
dataset.source_metadata["selection_aggregation"].update({"label": "Oppervlakte", "unit": "ha"})
|
|
result = VectorFeatureService.summarize_features_by_bbox(
|
|
ScalarSession(125_000.0),
|
|
dataset=dataset,
|
|
bbox={"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3, "crs": "EPSG:4326"},
|
|
total_feature_count=40,
|
|
)
|
|
assert result["metric_value"] == 12.5
|
|
assert result["metric_unit"] == "ha"
|
|
assert result["feature_count"] == 40
|
|
|
|
|
|
def test_population_area_weighting_is_exact_for_full_features_and_estimated_for_partial_features() -> None:
|
|
dataset = temporal_dataset(project_id=uuid4(), observed_year=2025, metric_method="area_weighted_sum")
|
|
dataset.source_metadata["selection_aggregation"].update(
|
|
{
|
|
"property": "population_total",
|
|
"label": "Inwoners",
|
|
"unit": "inwoners",
|
|
"warning": "Partial-sector estimate",
|
|
"warning_only_when_estimate": True,
|
|
}
|
|
)
|
|
bbox = {"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3, "crs": "EPSG:4326"}
|
|
|
|
full = VectorFeatureService.summarize_features_by_bbox(
|
|
SequenceScalarSession([49, 38_675.0]),
|
|
dataset=dataset,
|
|
bbox=bbox,
|
|
total_feature_count=49,
|
|
)
|
|
partial = VectorFeatureService.summarize_features_by_bbox(
|
|
SequenceScalarSession([1, 1_250.5]),
|
|
dataset=dataset,
|
|
bbox=bbox,
|
|
total_feature_count=3,
|
|
)
|
|
|
|
assert full["metric_value"] == 38_675.0
|
|
assert full["is_estimate"] is False
|
|
assert full["warning"] is None
|
|
assert partial["metric_value"] == 1_250.5
|
|
assert partial["is_estimate"] is True
|
|
assert partial["warning"] == "Partial-sector estimate"
|
|
|
|
|
|
def test_temporal_compare_returns_delta_and_canonical_change_payload(monkeypatch) -> None:
|
|
project_id = uuid4()
|
|
earlier = temporal_dataset(project_id=project_id, observed_year=2021)
|
|
later = temporal_dataset(project_id=project_id, observed_year=2024)
|
|
|
|
def get_dataset(_db, _project_id, dataset_id, _label):
|
|
return earlier if dataset_id == earlier.id else later
|
|
|
|
def summarize(_db, *, dataset, bbox): # noqa: ARG001
|
|
value = 100.0 if dataset.id == earlier.id else 115.0
|
|
return {
|
|
"metric_label": "Inwoners",
|
|
"metric_value": value,
|
|
"metric_unit": "inwoners",
|
|
"aggregation_method": "area_weighted_sum",
|
|
"feature_count": 10,
|
|
"is_estimate": True,
|
|
"warning": "Areal weighting",
|
|
}
|
|
|
|
monkeypatch.setattr(TemporalAnalysisService, "_get_temporal_dataset", staticmethod(get_dataset))
|
|
monkeypatch.setattr(VectorFeatureService, "summarize_features_by_bbox", staticmethod(summarize))
|
|
monkeypatch.setattr(
|
|
TemporalAnalysisService,
|
|
"_compare_identity_features",
|
|
staticmethod(
|
|
lambda *args, **kwargs: (
|
|
TemporalObjectChanges(available=True, added_count=1, removed_count=0, modified_count=2, unchanged_count=7),
|
|
{"type": "FeatureCollection", "features": []},
|
|
[],
|
|
)
|
|
),
|
|
)
|
|
result = TemporalAnalysisService.compare(
|
|
SimpleNamespace(),
|
|
project_id=project_id,
|
|
payload=TemporalComparisonRequest(
|
|
earlier_dataset_id=earlier.id,
|
|
later_dataset_id=later.id,
|
|
bbox={"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3},
|
|
),
|
|
)
|
|
assert result.metric.absolute_change == 15.0
|
|
assert result.metric.percent_change == 15.0
|
|
assert result.metric.is_estimate is True
|
|
assert result.object_changes.modified_count == 2
|
|
assert result.geojson["type"] == "FeatureCollection"
|
|
|
|
|
|
def test_temporal_comparison_clips_cross_boundary_bbox_to_selected_area(monkeypatch) -> None:
|
|
project_id = uuid4()
|
|
area_id = uuid4()
|
|
earlier = temporal_dataset(project_id=project_id, observed_year=2021)
|
|
later = temporal_dataset(project_id=project_id, observed_year=2024)
|
|
area_shape = box(5.0, 51.0, 5.2, 51.2)
|
|
area = SimpleNamespace(
|
|
id=area_id,
|
|
project_id=project_id,
|
|
geometry=from_shape(area_shape, srid=4326),
|
|
)
|
|
captured_geometries = []
|
|
identity_capture = {}
|
|
|
|
monkeypatch.setattr(
|
|
TemporalAnalysisService,
|
|
"_get_temporal_dataset",
|
|
staticmethod(lambda _db, _project_id, dataset_id, _label: earlier if dataset_id == earlier.id else later),
|
|
)
|
|
monkeypatch.setattr(
|
|
TemporalAnalysisService,
|
|
"_get_selection_area",
|
|
staticmethod(lambda _db, _project_id, requested_area_id: area if requested_area_id == area_id else None),
|
|
)
|
|
|
|
def summarize(_db, *, dataset, bbox, selection_geometry, full_dataset_area): # noqa: ARG001
|
|
captured_geometries.append(selection_geometry)
|
|
return {
|
|
"metric_label": "Oppervlakte",
|
|
"metric_value": 10.0 if dataset.id == earlier.id else 12.0,
|
|
"metric_unit": "ha",
|
|
"aggregation_method": "intersection_area",
|
|
"feature_count": 1,
|
|
"is_estimate": False,
|
|
"warning": None,
|
|
}
|
|
|
|
def compare_identity(*_args, **kwargs):
|
|
identity_capture.update(kwargs)
|
|
return (
|
|
TemporalObjectChanges(available=False),
|
|
{"type": "FeatureCollection", "features": []},
|
|
[],
|
|
)
|
|
|
|
monkeypatch.setattr(VectorFeatureService, "summarize_features_by_bbox", staticmethod(summarize))
|
|
monkeypatch.setattr(TemporalAnalysisService, "_compare_identity_features", staticmethod(compare_identity))
|
|
|
|
result = TemporalAnalysisService.compare(
|
|
SimpleNamespace(),
|
|
project_id=project_id,
|
|
payload=TemporalComparisonRequest(
|
|
earlier_dataset_id=earlier.id,
|
|
later_dataset_id=later.id,
|
|
area_id=area_id,
|
|
bbox={"min_x": 4.9, "min_y": 51.1, "max_x": 5.1, "max_y": 51.3},
|
|
),
|
|
)
|
|
|
|
expected = box(5.0, 51.1, 5.1, 51.2)
|
|
assert result.metric.absolute_change == 2.0
|
|
assert all(to_shape(geometry).equals(expected) for geometry in captured_geometries)
|
|
assert to_shape(identity_capture["selection_geometry"]).equals(expected)
|
|
assert identity_capture["earlier_full_dataset_area"] is False
|
|
assert identity_capture["later_full_dataset_area"] is False
|
|
|
|
|
|
def test_unstable_temporal_identity_returns_clear_end_user_warning() -> None:
|
|
project_id = uuid4()
|
|
earlier = temporal_dataset(project_id=project_id, observed_year=2021)
|
|
later = temporal_dataset(project_id=project_id, observed_year=2025)
|
|
earlier.source_metadata["identity_stable"] = False
|
|
later.source_metadata["identity_stable"] = False
|
|
|
|
changes, geojson, warnings = TemporalAnalysisService._compare_identity_features(
|
|
SimpleNamespace(),
|
|
earlier=earlier,
|
|
later=later,
|
|
bbox={"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3},
|
|
preview_limit=100,
|
|
)
|
|
|
|
assert changes.available is False
|
|
assert geojson == {"type": "FeatureCollection", "features": []}
|
|
assert warnings == ["Wijzigingen van individuele objecten kunnen voor deze bron niet betrouwbaar worden gevolgd."]
|
|
|
|
|
|
def test_governed_legacy_grb_snapshots_use_verified_official_feature_identity() -> None:
|
|
project_id = uuid4()
|
|
earlier = governed_grb_dataset(project_id=project_id, observed_day=14)
|
|
later = governed_grb_dataset(project_id=project_id, observed_day=15)
|
|
original = Polygon([(5.1, 51.1), (5.101, 51.1), (5.101, 51.101), (5.1, 51.101)])
|
|
changed = Polygon([(5.1, 51.1), (5.102, 51.1), (5.102, 51.101), (5.1, 51.101)])
|
|
added = Polygon([(5.11, 51.11), (5.111, 51.11), (5.111, 51.111), (5.11, 51.111)])
|
|
session = SequentialFeatureSession(
|
|
[
|
|
[persisted_feature(earlier.id, "GBG.1", original)],
|
|
[persisted_feature(later.id, "GBG.1", changed), persisted_feature(later.id, "GBG.2", added)],
|
|
]
|
|
)
|
|
|
|
changes, geojson, warnings = TemporalAnalysisService._compare_identity_features(
|
|
session,
|
|
earlier=earlier,
|
|
later=later,
|
|
bbox={"min_x": 5.0, "min_y": 51.0, "max_x": 5.2, "max_y": 51.2},
|
|
preview_limit=100,
|
|
)
|
|
|
|
assert changes.available is True
|
|
assert changes.added_count == 1
|
|
assert changes.removed_count == 0
|
|
assert changes.modified_count == 1
|
|
assert changes.unchanged_count == 0
|
|
assert {feature["properties"]["change_type"] for feature in geojson["features"]} == {"added", "modified"}
|
|
assert warnings == []
|
|
|
|
|
|
def test_governed_grb_object_history_fails_closed_for_unverified_identity() -> None:
|
|
project_id = uuid4()
|
|
earlier = governed_grb_dataset(project_id=project_id, observed_day=14)
|
|
later = governed_grb_dataset(project_id=project_id, observed_day=15)
|
|
polygon = Polygon([(5.1, 51.1), (5.101, 51.1), (5.101, 51.101), (5.1, 51.101)])
|
|
fallback_hash = "a" * 64
|
|
session = SequentialFeatureSession(
|
|
[
|
|
[persisted_feature(earlier.id, fallback_hash, polygon)],
|
|
[persisted_feature(later.id, fallback_hash, polygon)],
|
|
]
|
|
)
|
|
|
|
changes, geojson, warnings = TemporalAnalysisService._compare_identity_features(
|
|
session,
|
|
earlier=earlier,
|
|
later=later,
|
|
bbox={"min_x": 5.0, "min_y": 51.0, "max_x": 5.2, "max_y": 51.2},
|
|
preview_limit=100,
|
|
)
|
|
|
|
assert changes.available is False
|
|
assert geojson["features"] == []
|
|
assert warnings == ["De geselecteerde objecten bevatten geen volledig verifieerbare stabiele bronidentiteit."]
|
|
|
|
|
|
def test_legacy_grb_identity_requires_complete_partition_evidence() -> None:
|
|
dataset = governed_grb_dataset(project_id=uuid4(), observed_day=14)
|
|
dataset.provenance_metadata["partition_checksums"] = {"13025": "b" * 64}
|
|
|
|
assert TemporalAnalysisService._identity_contract(dataset) is None
|
|
|
|
|
|
def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
|
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
|
temporal_api = (ROOT / "frontend/src/services/api/temporal.ts").read_text(encoding="utf-8")
|
|
population = (ROOT / "scripts/provision_mol_population_history.py").read_text(encoding="utf-8")
|
|
landuse = (ROOT / "scripts/provision_mol_historical_landuse.py").read_text(encoding="utf-8")
|
|
dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8")
|
|
|
|
assert "Laatste toestand" in workspace
|
|
assert "Evolutie" in workspace
|
|
assert "Vergelijk periode" in workspace
|
|
assert "temporalRangeLabel" in workspace
|
|
assert "Dagelijkse GRB-edities tonen wijzigingen in de officiële registratie" in workspace
|
|
assert "/temporal/compare" in temporal_api
|
|
assert "Statbel" in population and "area_weighted_sum" in population
|
|
assert '"identity_stable": False' in population
|
|
assert "HistLandgebruik" in landuse and "intersection_area" in landuse
|
|
assert "<wfs:GetFeature" in landuse and "session.post(" in landuse
|
|
assert 'lambda value: value.startswith("weg"), "weg*"' in landuse
|
|
assert "provision_mol_population_history.py" in dockerfile
|
|
assert "provision_mol_historical_landuse.py" in dockerfile
|
|
assert "fake" not in population.lower()
|
|
|
|
|
|
def test_tower_deploy_waits_for_startup_migration_before_live_smoke() -> None:
|
|
for relative_path in ("scripts/deploy_tower.ps1", "scripts/deploy_tower.sh"):
|
|
script = (ROOT / relative_path).read_text(encoding="utf-8")
|
|
assert "bash deploy/unraid/deploy-release.sh" in script
|
|
|
|
release_script = (ROOT / "deploy/unraid/deploy-release.sh").read_text(encoding="utf-8")
|
|
wait_position = release_script.index("wait_for_geointel_health")
|
|
invocation_position = release_script.index("\n wait_for_geointel_health", wait_position)
|
|
smoke_position = release_script.index("LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh")
|
|
assert "docker inspect --format" in release_script
|
|
assert invocation_position < smoke_position
|