diff --git a/.claude/sweep-error-handling-state.csv b/.claude/sweep-error-handling-state.csv new file mode 100644 index 000000000..a5ad4dfdd --- /dev/null +++ b/.claude/sweep-error-handling-state.csv @@ -0,0 +1,2 @@ +module,last_inspected,issue,severity_max,categories_found,notes +geotiff,2026-07-02,3604,MEDIUM,2;4,"to_geotiff 0D/1D DataArray raised opaque IndexError from _coords.py coords_to_transform (dims[-2]) instead of clean 'Expected 2D or 3D' ValueError; numpy path + 4D DataArray already clean. Fixed via early ndim guard before dispatch (eager/vrt/gpu) + 3 tests; PR #3604. Read-side param validation + typed-error hierarchy + allow_rotated/allow_invalid_nodata VRT+chunked opt-in threading verified clean (CUDA available, GPU paths run). gh issue create blocked by auto-mode; PR opened. Cat 2+4." diff --git a/xrspatial/geotiff/_writers/eager.py b/xrspatial/geotiff/_writers/eager.py index 0476eef8d..2c66ba3cb 100644 --- a/xrspatial/geotiff/_writers/eager.py +++ b/xrspatial/geotiff/_writers/eager.py @@ -839,6 +839,19 @@ def _write_sidecars(): _is_vrt_path = ( isinstance(path, str) and path.lower().endswith('.vrt')) + # Reject arrays that are not 2D or 3D before any backend dispatch or + # georef resolution. ``.ndim`` is defined on DataArray, numpy, cupy, + # and dask inputs. Without this, a 0D/1D *DataArray* reaches + # ``coords_to_transform`` (which indexes ``dims[-2]``) and dies with an + # opaque ``IndexError: tuple index out of range`` pointing at + # ``_coords.py`` internals, while a 0D/1D numpy array or a 4D + # DataArray already gets the clear message below. Running the check + # here up front makes every backend (eager, VRT, GPU) reject the same + # bad shapes identically. + _ndim = getattr(data, 'ndim', None) + if _ndim is not None and _ndim not in (2, 3): + raise ValueError(f"Expected 2D or 3D array, got {_ndim}D") + # Resolve GPU dispatch up front so the JPEG opt-in warning fires # exactly once. ``_write_geotiff_gpu`` emits its own warning on the # GPU path; emitting here as well would double-warn callers of diff --git a/xrspatial/geotiff/tests/test_edge_cases.py b/xrspatial/geotiff/tests/test_edge_cases.py index 624a0282d..ac81855e2 100644 --- a/xrspatial/geotiff/tests/test_edge_cases.py +++ b/xrspatial/geotiff/tests/test_edge_cases.py @@ -40,6 +40,26 @@ def test_0d_scalar(self, tmp_path): with pytest.raises(ValueError, match="Expected 2D"): to_geotiff(arr, str(tmp_path / 'bad.tif')) + def test_1d_dataarray(self, tmp_path): + # A 1D DataArray used to reach ``coords_to_transform`` (which + # indexes ``dims[-2]``) and raise an opaque ``IndexError: tuple + # index out of range`` instead of the clear message a 1D numpy + # array or a 4D DataArray already gets. + da = xr.DataArray(np.zeros(10, dtype=np.float32), dims=('x',)) + with pytest.raises(ValueError, match="Expected 2D or 3D array, got 1D"): + to_geotiff(da, str(tmp_path / 'bad.tif')) + + def test_0d_dataarray(self, tmp_path): + da = xr.DataArray(np.float32(42.0)) + with pytest.raises(ValueError, match="Expected 2D or 3D array, got 0D"): + to_geotiff(da, str(tmp_path / 'bad.tif')) + + def test_1d_dataarray_vrt(self, tmp_path): + # The ``.vrt`` write path shares the same pre-dispatch guard. + da = xr.DataArray(np.zeros(10, dtype=np.float32), dims=('x',)) + with pytest.raises(ValueError, match="Expected 2D or 3D array, got 1D"): + to_geotiff(da, str(tmp_path / 'bad.vrt')) + def test_unsupported_compression(self, tmp_path): arr = np.zeros((4, 4), dtype=np.float32) # ``to_geotiff`` validates ``compression`` up-front. The