From af49be9290b1727b435f9b21f568ba639b07c0af Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Thu, 2 Jul 2026 11:45:58 -0400 Subject: [PATCH] bump: validate agg, count, and spread inputs bump() only validated width and height. Bad values for the agg template, count, and spread surfaced numpy/dispatcher errors from deep in the call or were silently accepted: - agg as a plain ndarray -> 'Unsupported Array Type' from the backend dispatcher; a 3D/1D DataArray -> 'too many values to unpack' from the bare h, w = agg.shape unpack. Neither named agg. - count negative/float -> raw np.empty errors. - spread negative/float -> accepted silently, dropping the spreading the caller asked for despite the docstring saying int. Add _validate_raster(agg, ndim=2, numeric=False) and _validate_scalar for count (>=0) and spread (>=0), matching the existing width/height checks. bump only reads agg's shape/backend so dtype stays unchecked. spread=0 and count=0 remain valid. Verified across numpy, cupy, dask, and dask+cupy. error-handling sweep 2026-07-02 --- .claude/sweep-error-handling-state.csv | 2 + xrspatial/bump.py | 9 +++++ xrspatial/tests/test_bump.py | 51 ++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 .claude/sweep-error-handling-state.csv diff --git a/.claude/sweep-error-handling-state.csv b/.claude/sweep-error-handling-state.csv new file mode 100644 index 000000000..82cc38cba --- /dev/null +++ b/.claude/sweep-error-handling-state.csv @@ -0,0 +1,2 @@ +module,last_inspected,issue,severity_max,categories_found,notes +bump,2026-07-02,,HIGH,1;2;3;4,"bump() agg template unvalidated: plain ndarray -> ArrayTypeFunctionMapping 'Unsupported Array Type'; 3D/1D DataArray -> 'too many values to unpack'. count/spread unvalidated (internal numpy errors / silent). Added _validate_raster(agg,ndim=2) + _validate_scalar for count,spread. all 4 backends verified (CUDA present)." diff --git a/xrspatial/bump.py b/xrspatial/bump.py index c7518b461..bea1f7f08 100644 --- a/xrspatial/bump.py +++ b/xrspatial/bump.py @@ -17,6 +17,7 @@ class cupy(object): from xrspatial.utils import ( ArrayTypeFunctionMapping, + _validate_raster, _validate_scalar, has_cuda_and_cupy, is_cupy_array, @@ -331,7 +332,15 @@ def heights(locations, src, src_range, height = 20): Description: Example Bump Map units: km """ + _validate_scalar(spread, func_name='bump', name='spread', + dtype=int, min_val=0) + if count is not None: + _validate_scalar(count, func_name='bump', name='count', + dtype=int, min_val=0) + if agg is not None: + _validate_raster(agg, func_name='bump', name='agg', ndim=2, + numeric=False) h, w = agg.shape else: _validate_scalar(width, func_name='bump', name='width', diff --git a/xrspatial/tests/test_bump.py b/xrspatial/tests/test_bump.py index bb9857a32..221b98eaa 100644 --- a/xrspatial/tests/test_bump.py +++ b/xrspatial/tests/test_bump.py @@ -309,3 +309,54 @@ def test_bump_dask_bypasses_raster_guard(): result = bump(agg=agg, count=10, spread=0) assert result.shape == (100_000, 100_000) assert isinstance(result.data, da.Array) + + +# --- Input-validation regression tests --- + +def test_bump_agg_must_be_dataarray(): + """A plain ndarray template raises a clear TypeError naming `agg`, + not an inscrutable 'Unsupported Array Type' from the dispatcher.""" + import pytest + + with pytest.raises(TypeError, match=r"bump\(\): `agg` must be an " + r"xarray\.DataArray"): + bump(agg=np.zeros((10, 10))) + + +def test_bump_agg_must_be_2d(): + """A 3D or 1D template raises a clear ValueError naming `agg` and the + 2D requirement, not 'too many values to unpack'.""" + import pytest + + with pytest.raises(ValueError, match=r"bump\(\): `agg` must be 2D"): + bump(agg=xr.DataArray(np.zeros((3, 10, 10)), dims=['b', 'y', 'x'])) + with pytest.raises(ValueError, match=r"bump\(\): `agg` must be 2D"): + bump(agg=xr.DataArray(np.zeros(10), dims=['x'])) + + +def test_bump_count_validated(): + """`count` gets the same clean validation as width/height instead of + surfacing raw numpy errors.""" + import pytest + + with pytest.raises(ValueError, match=r"bump\(\): `count` must be >= 0"): + bump(width=10, height=10, count=-5) + with pytest.raises(TypeError, match=r"bump\(\): `count` must be int"): + bump(width=10, height=10, count=5.0) + + +def test_bump_spread_validated(): + """`spread` is documented as int; negative and non-int values raise + instead of being silently ignored.""" + import pytest + + with pytest.raises(ValueError, match=r"bump\(\): `spread` must be >= 0"): + bump(width=10, height=10, spread=-3) + with pytest.raises(TypeError, match=r"bump\(\): `spread` must be int"): + bump(width=10, height=10, spread=2.5) + + +def test_bump_spread_zero_still_allowed(): + """spread=0 (single-pixel bumps) must remain valid.""" + result = bump(width=10, height=10, count=5, spread=0) + assert result.shape == (10, 10)