from __future__ import annotations import importlib.util from pathlib import Path import sys import pytest ROOT = Path(__file__).resolve().parents[2] def load_sample_preparer(): script_path = ROOT / "scripts" / "prepare_operator_real_data_samples.py" spec = importlib.util.spec_from_file_location("operator_sample_preparer", 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 test_operator_sample_registry_includes_kempen_reference_and_background_candidates() -> None: module = load_sample_preparer() expected_reference_slugs = {"geel", "mol", "turnhout", "herentals", "balen", "retie", "westerlo"} expected_background_slugs = { "postel_bos", "lommel_heide", "kasterlee_bos", "dessel_heide", "ravels_bos", "meerhout_bos", "geel_bel", "arendonk_heide", } assert expected_reference_slugs.issubset(module.SAMPLES) assert expected_background_slugs.issubset(module.SAMPLES) assert all(not module.SAMPLES[slug].allow_empty_reference for slug in expected_reference_slugs) assert all(module.SAMPLES[slug].allow_empty_reference for slug in expected_background_slugs) assert all(module.SAMPLES[slug].sample_role == "background_candidate" for slug in expected_background_slugs) def test_operator_background_candidates_are_unique_enough_for_hard_negative_training() -> None: module = load_sample_preparer() background_samples = [ sample for sample in module.SAMPLES.values() if sample.sample_role == "background_candidate" ] centers = {(round(sample.center_lon, 4), round(sample.center_lat, 4)) for sample in background_samples} half_sizes = {sample.half_size_m for sample in background_samples} assert len(background_samples) >= 8 assert len(centers) == len(background_samples) assert min(sample.center_lon for sample in background_samples) < 4.85 assert max(sample.center_lon for sample in background_samples) > 5.25 assert min(sample.center_lat for sample in background_samples) < 51.18 assert max(sample.center_lat for sample in background_samples) > 51.33 assert half_sizes == {260.0} def test_operator_sample_can_be_scaled_for_larger_training_aoi(tmp_path: Path) -> None: module = load_sample_preparer() sample = module.OperatorSample( slug="geel", display_name="Geel", center_lon=5.0, center_lat=51.0, half_size_m=250.0, ) configured = module.apply_sample_overrides(sample, width=1024, height=1024, half_size_scale=2.0) ortho_path, reference_path = module.sample_artifact_paths(configured, tmp_path) assert configured.width == 1024 assert configured.height == 1024 assert configured.half_size_m == 500.0 assert ortho_path.name == "geel_orthophoto_wms_1024.tif" assert reference_path.name == "geel_grb_gbg_buildings.geojson" def test_background_candidate_can_write_empty_reference_geojson(tmp_path: Path, monkeypatch) -> None: module = load_sample_preparer() class EmptyFeatureResponse: headers = {"content-type": "application/geo+json"} def raise_for_status(self) -> None: return None def json(self) -> dict: return {"type": "FeatureCollection", "features": []} class FakeRequests: @staticmethod def get(*args, **kwargs): return EmptyFeatureResponse() monkeypatch.setattr(module, "requests", FakeRequests) monkeypatch.setattr(module, "prepared_url", lambda url, params: f"{url}?prepared=true") sample = module.OperatorSample( slug="background", display_name="Background", center_lon=5.0, center_lat=51.0, allow_empty_reference=True, sample_role="background_candidate", ) reference_path = tmp_path / "background.geojson" source_url, feature_count = module.fetch_reference(sample, reference_path, [4.9, 50.9, 5.1, 51.1]) assert source_url.endswith("?prepared=true") assert feature_count == 0 payload = reference_path.read_text(encoding="utf-8") assert '"features": []' in payload assert '"sample_role": "background_candidate"' in payload def test_reference_sample_still_rejects_empty_grb_response(tmp_path: Path, monkeypatch) -> None: module = load_sample_preparer() class EmptyFeatureResponse: def raise_for_status(self) -> None: return None def json(self) -> dict: return {"type": "FeatureCollection", "features": []} class FakeRequests: @staticmethod def get(*args, **kwargs): return EmptyFeatureResponse() monkeypatch.setattr(module, "requests", FakeRequests) monkeypatch.setattr(module, "prepared_url", lambda url, params: f"{url}?prepared=true") sample = module.OperatorSample( slug="urban", display_name="Urban", center_lon=5.0, center_lat=51.0, ) with pytest.raises(SystemExit, match="returned no building features"): module.fetch_reference(sample, tmp_path / "urban.geojson", [4.9, 50.9, 5.1, 51.1])