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.lst_c) 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, selected_features=["ndvi", "ndwi", "lst_c", "soil_vv_db"], 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, "lst_c": None, "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"])