2026-05-10 22:49:07 +03:30
|
|
|
from datetime import date
|
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
|
|
|
|
from django.test import TestCase
|
|
|
|
|
|
|
|
|
|
from location_data.models import AnalysisGridCell, AnalysisGridObservation, RemoteSensingRun, SoilLocation
|
|
|
|
|
from location_data.tasks import _upsert_grid_observations, run_remote_sensing_analysis
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RemoteSensingTaskDiagnosticsTests(TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.boundary = {
|
|
|
|
|
"type": "Polygon",
|
|
|
|
|
"coordinates": [
|
|
|
|
|
[
|
|
|
|
|
[51.3890, 35.6890],
|
|
|
|
|
[51.3900, 35.6890],
|
|
|
|
|
[51.3900, 35.6900],
|
|
|
|
|
[51.3890, 35.6900],
|
|
|
|
|
[51.3890, 35.6890],
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
self.location = SoilLocation.objects.create(
|
|
|
|
|
latitude="35.689200",
|
|
|
|
|
longitude="51.389000",
|
|
|
|
|
farm_boundary=self.boundary,
|
|
|
|
|
)
|
|
|
|
|
self.run = RemoteSensingRun.objects.create(
|
|
|
|
|
soil_location=self.location,
|
|
|
|
|
block_code="",
|
|
|
|
|
chunk_size_sqm=900,
|
|
|
|
|
temporal_start=date(2026, 4, 9),
|
|
|
|
|
temporal_end=date(2026, 5, 9),
|
|
|
|
|
status=RemoteSensingRun.STATUS_PENDING,
|
|
|
|
|
metadata={},
|
|
|
|
|
)
|
|
|
|
|
self.cell = AnalysisGridCell.objects.create(
|
|
|
|
|
soil_location=self.location,
|
|
|
|
|
block_code="",
|
|
|
|
|
cell_code="cell-1",
|
|
|
|
|
chunk_size_sqm=900,
|
|
|
|
|
geometry=self.boundary,
|
|
|
|
|
centroid_lat="35.689200",
|
|
|
|
|
centroid_lon="51.389200",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_upsert_logs_and_stores_diagnostics_for_empty_observations(self):
|
|
|
|
|
metric_payload = {
|
|
|
|
|
"results": {},
|
|
|
|
|
"metadata": {
|
|
|
|
|
"backend": "openeo",
|
|
|
|
|
"backend_url": "https://openeofed.dataspace.copernicus.eu",
|
|
|
|
|
"collections_used": ["SENTINEL2_L2A"],
|
|
|
|
|
"job_refs": {"ndvi": "job-1"},
|
|
|
|
|
"failed_metrics": [],
|
|
|
|
|
"payload_diagnostics": {
|
|
|
|
|
"ndvi": {
|
|
|
|
|
"returned_cell_count": 0,
|
|
|
|
|
"payload_keys_sample": [],
|
|
|
|
|
"available_features": ["mean"],
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with self.assertLogs("location_data.tasks", level="WARNING") as captured:
|
|
|
|
|
summary = _upsert_grid_observations(
|
|
|
|
|
cells=[self.cell],
|
|
|
|
|
run=self.run,
|
|
|
|
|
temporal_start=date(2026, 4, 9),
|
|
|
|
|
temporal_end=date(2026, 5, 9),
|
|
|
|
|
metric_payload=metric_payload,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
log_output = "\n".join(captured.output)
|
|
|
|
|
self.assertIn("Persisting empty observation for cell=cell-1, run_id=", log_output)
|
|
|
|
|
self.assertIn("No payload cells matched DB cell_codes for run_id=", log_output)
|
|
|
|
|
self.assertIn("All persisted observations are empty for run_id=", log_output)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(summary["total_observation_count"], 1)
|
|
|
|
|
self.assertEqual(summary["usable_observation_count"], 0)
|
|
|
|
|
self.assertEqual(summary["fully_null_observation_count"], 1)
|
|
|
|
|
self.assertEqual(summary["matched_cell_count"], 0)
|
|
|
|
|
self.assertEqual(summary["payload_keys_sample"], [])
|
|
|
|
|
self.assertEqual(summary["available_features"], ["mean"])
|
|
|
|
|
|
|
|
|
|
observation = AnalysisGridObservation.objects.get(cell=self.cell)
|
|
|
|
|
self.assertIsNone(observation.ndvi)
|
|
|
|
|
self.assertIsNone(observation.ndwi)
|
|
|
|
|
self.assertIsNone(observation.soil_vv)
|
|
|
|
|
self.assertIsNone(observation.soil_vv_db)
|
|
|
|
|
|
|
|
|
|
self.run.refresh_from_db()
|
|
|
|
|
diagnostics = self.run.metadata["diagnostics"]["empty_observations"]
|
|
|
|
|
self.assertEqual(diagnostics["job_ref"], {"ndvi": "job-1"})
|
|
|
|
|
self.assertEqual(diagnostics["total_cells"], 1)
|
|
|
|
|
self.assertEqual(diagnostics["matched_cells"], 0)
|
|
|
|
|
self.assertEqual(diagnostics["payload_keys_sample"], [])
|
|
|
|
|
self.assertEqual(diagnostics["available_features"], ["mean"])
|
|
|
|
|
|
|
|
|
|
def test_run_remote_sensing_analysis_refetches_when_cached_observations_are_empty(self):
|
|
|
|
|
AnalysisGridObservation.objects.create(
|
|
|
|
|
cell=self.cell,
|
|
|
|
|
run=self.run,
|
|
|
|
|
temporal_start=date(2026, 4, 9),
|
|
|
|
|
temporal_end=date(2026, 5, 9),
|
|
|
|
|
metadata={},
|
|
|
|
|
)
|
|
|
|
|
subdivision_result = Mock(
|
|
|
|
|
id=99,
|
|
|
|
|
cluster_count=1,
|
2026-05-11 00:36:02 +03:30
|
|
|
selected_features=["ndvi", "ndwi", "soil_vv_db"],
|
2026-05-10 22:49:07 +03:30
|
|
|
metadata={"used_cell_count": 1, "skipped_cell_count": 0, "kmeans_params": {}},
|
|
|
|
|
skipped_cell_codes=[],
|
|
|
|
|
)
|
|
|
|
|
remote_payload = {
|
|
|
|
|
"results": {
|
|
|
|
|
"cell-1": {
|
|
|
|
|
"ndvi": 0.52,
|
|
|
|
|
"ndwi": 0.21,
|
|
|
|
|
"soil_vv": 10.0,
|
|
|
|
|
"soil_vv_db": 10.0,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"metadata": {
|
|
|
|
|
"backend": "openeo",
|
|
|
|
|
"backend_url": "https://openeofed.dataspace.copernicus.eu",
|
|
|
|
|
"collections_used": ["SENTINEL2_L2A", "SENTINEL1_GRD"],
|
|
|
|
|
"job_refs": {"ndvi": "job-1"},
|
|
|
|
|
"failed_metrics": [],
|
|
|
|
|
"payload_diagnostics": {
|
|
|
|
|
"ndvi": {
|
|
|
|
|
"returned_cell_count": 1,
|
|
|
|
|
"payload_keys_sample": ["0"],
|
|
|
|
|
"available_features": ["mean"],
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
|
"location_data.tasks.create_or_get_analysis_grid_cells",
|
|
|
|
|
return_value={
|
|
|
|
|
"created": False,
|
|
|
|
|
"block_code": "",
|
|
|
|
|
"total_count": 1,
|
|
|
|
|
"created_count": 0,
|
|
|
|
|
"chunk_size_sqm": 900,
|
|
|
|
|
"existing_count": 1,
|
|
|
|
|
},
|
|
|
|
|
), patch(
|
|
|
|
|
"location_data.tasks.compute_remote_sensing_metrics",
|
|
|
|
|
return_value=remote_payload,
|
|
|
|
|
) as compute_mock, patch(
|
|
|
|
|
"location_data.tasks._ensure_subdivision_result",
|
|
|
|
|
return_value=subdivision_result,
|
|
|
|
|
):
|
|
|
|
|
summary = run_remote_sensing_analysis(
|
|
|
|
|
soil_location_id=self.location.id,
|
|
|
|
|
block_code="",
|
|
|
|
|
temporal_start=date(2026, 4, 9),
|
|
|
|
|
temporal_end=date(2026, 5, 9),
|
|
|
|
|
run_id=self.run.id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
compute_mock.assert_called_once()
|
|
|
|
|
self.assertEqual(summary["source"], "openeo")
|
|
|
|
|
self.assertEqual(summary["processed_cell_count"], 1)
|
|
|
|
|
|
|
|
|
|
observation = AnalysisGridObservation.objects.get(cell=self.cell)
|
|
|
|
|
self.assertEqual(observation.ndvi, 0.52)
|
|
|
|
|
self.assertEqual(observation.ndwi, 0.21)
|
|
|
|
|
self.assertEqual(observation.soil_vv, 10.0)
|
|
|
|
|
self.assertEqual(observation.soil_vv_db, 10.0)
|
|
|
|
|
|
|
|
|
|
self.run.refresh_from_db()
|
|
|
|
|
cached_details = self.run.metadata["stage_details"]["using_cached_observations"]
|
|
|
|
|
self.assertEqual(cached_details["source"], "database")
|
|
|
|
|
self.assertFalse(cached_details["usable"])
|
|
|
|
|
self.assertTrue(cached_details["refetching"])
|