diff --git a/.claude/sweep-test-coverage-state.csv b/.claude/sweep-test-coverage-state.csv index ed3a5d395..48d28a4b0 100644 --- a/.claude/sweep-test-coverage-state.csv +++ b/.claude/sweep-test-coverage-state.csv @@ -1,3 +1,3 @@ module,last_inspected,issue,severity_max,categories_found,notes -geotiff,2026-05-11,,MEDIUM,1;4,"Sweep 3 (2026-05-11 r3): added test_vrt_backend_coverage_2026_05_11.py closing read_vrt(gpu=True) + read_vrt(gpu=True, chunks=N) (Cat 1, dask+cupy backend), read_vrt(dtype=) safe-widening and float->int validation (Cat 4), read_vrt(name=) override (Cat 4), and open_geotiff(BytesIO, gpu=True) / open_geotiff(BytesIO, chunks=N) error-path coverage (Cat 4). 11 tests, all passing on GPU host. No HIGH gaps remain." +geotiff,2026-05-11,,MEDIUM,1;4,"Sweep 4 (2026-05-11 r4): added test_kwarg_coverage_2026_05_11_r4.py closing read_geotiff_gpu(name=) + read_geotiff_gpu(chunks=, name=) (Cat 4, dask+cupy backend), read_geotiff_dask(name=) (Cat 4), read_geotiff_gpu(max_pixels=) accept/reject + chunks+max_pixels reject (Cat 4), and open_geotiff(chunks=, name=) / open_geotiff(gpu=True, name=) / open_geotiff(gpu=True, chunks=, name=) / open_geotiff(gpu=True, max_pixels=) dispatch coverage. 12 tests, all passing on GPU host. Pass 3 (r3) added test_vrt_backend_coverage_2026_05_11.py covering read_vrt gpu/chunks/dtype/name (11 tests). No HIGH gaps remain." reproject,2026-05-10,,HIGH,1;4;5,"Added 39 tests: LiteCRS direct coverage, itrf_transform behaviour/roundtrip/array, itrf_frames, geoid_height numerical correctness + raster happy-path, vertical helpers (ellipsoidal<->orthometric/depth), reproject() lat/lon and latitude/longitude dim propagation. Note: _merge_arrays_cupy is imported but unused (no cupy merge dispatch in merge()); flagged as feature gap not test gap." diff --git a/xrspatial/geotiff/tests/test_kwarg_coverage_2026_05_11_r4.py b/xrspatial/geotiff/tests/test_kwarg_coverage_2026_05_11_r4.py new file mode 100644 index 000000000..1535421e6 --- /dev/null +++ b/xrspatial/geotiff/tests/test_kwarg_coverage_2026_05_11_r4.py @@ -0,0 +1,177 @@ +"""Parameter coverage for ``read_geotiff_gpu`` / ``read_geotiff_dask``. + +The ``name=`` and ``max_pixels=`` kwargs flow through ``open_geotiff``'s +dispatch into the GPU and dask backends. The eager numpy path tests +both kwargs directly (e.g. ``test_cog::test_open_geotiff_custom_name``, +``test_security`` for ``max_pixels``). The dask backend covers +``max_pixels`` in ``test_backend_kwarg_parity_1561``. The remaining +gaps that this sweep (test coverage gap sweep 2026-05-11, pass 4) +closes are: + +* ``read_geotiff_gpu(name=...)`` -- direct test on the GPU eager path + and the dask+GPU path. +* ``read_geotiff_dask(name=...)`` -- direct test on the dask-on-CPU + path. +* ``read_geotiff_gpu(max_pixels=...)`` -- both the accept and reject + branches; the GPU pipeline calls ``_check_dimensions`` twice (once + for the full raster, once per tile) and neither call had regression + coverage. +* ``open_geotiff(chunks=..., name=...)`` / + ``open_geotiff(gpu=True, name=...)`` / + ``open_geotiff(gpu=True, chunks=..., name=...)`` -- the dispatcher + forwards ``name=`` through three distinct branches and a silent + drop would only show up in user code. + +Adding these closes the MEDIUM Cat 4 (parameter coverage) gap that +was open after pass 3. +""" +from __future__ import annotations + +import importlib.util + +import numpy as np +import pytest + +from xrspatial.geotiff import ( + open_geotiff, + read_geotiff_dask, + read_geotiff_gpu, + to_geotiff, +) + + +def _gpu_available() -> bool: + if importlib.util.find_spec("cupy") is None: + return False + try: + import cupy + return bool(cupy.cuda.is_available()) + except Exception: + return False + + +_HAS_GPU = _gpu_available() +_gpu_only = pytest.mark.skipif(not _HAS_GPU, reason="cupy + CUDA required") + + +@pytest.fixture +def small_tiff_path(tmp_path): + arr = np.arange(64, dtype=np.float32).reshape(8, 8) + p = tmp_path / "small.tif" + to_geotiff(arr, str(p), tile_size=4) + return str(p), arr + + +# --------------------------------------------------------------------------- +# read_geotiff_dask(name=...) -- direct +# --------------------------------------------------------------------------- + + +def test_read_geotiff_dask_name_kwarg_sets_name(small_tiff_path): + path, arr = small_tiff_path + da = read_geotiff_dask(path, chunks=4, name="custom_dask") + assert da.name == "custom_dask" + np.testing.assert_array_equal(da.values, arr) + + +def test_read_geotiff_dask_default_name_from_path(small_tiff_path): + path, _ = small_tiff_path + da = read_geotiff_dask(path, chunks=4) + # Default name is filename stem when no override is supplied. + assert da.name == "small" + + +# --------------------------------------------------------------------------- +# read_geotiff_gpu(name=...) -- direct +# --------------------------------------------------------------------------- + + +@_gpu_only +def test_read_geotiff_gpu_name_kwarg_sets_name(small_tiff_path): + path, arr = small_tiff_path + da = read_geotiff_gpu(path, name="custom_gpu") + assert da.name == "custom_gpu" + np.testing.assert_array_equal(da.data.get(), arr) + + +@_gpu_only +def test_read_geotiff_gpu_default_name_from_path(small_tiff_path): + path, _ = small_tiff_path + da = read_geotiff_gpu(path) + assert da.name == "small" + + +@_gpu_only +def test_read_geotiff_gpu_chunks_name_kwarg_sets_name(small_tiff_path): + path, arr = small_tiff_path + da = read_geotiff_gpu(path, chunks=4, name="custom_dask_gpu") + assert da.name == "custom_dask_gpu" + np.testing.assert_array_equal(da.data.compute().get(), arr) + + +# --------------------------------------------------------------------------- +# read_geotiff_gpu(max_pixels=...) -- accept + reject +# --------------------------------------------------------------------------- + + +@_gpu_only +def test_read_geotiff_gpu_max_pixels_accepts_within_budget(small_tiff_path): + path, arr = small_tiff_path + # 8 * 8 = 64 pixels. 100 leaves room. + da = read_geotiff_gpu(path, max_pixels=100) + np.testing.assert_array_equal(da.data.get(), arr) + + +@_gpu_only +def test_read_geotiff_gpu_max_pixels_rejects_oversized(small_tiff_path): + path, _ = small_tiff_path + with pytest.raises(ValueError, match="safety limit|exceeds max_pixels"): + read_geotiff_gpu(path, max_pixels=10) + + +@_gpu_only +def test_read_geotiff_gpu_chunks_max_pixels_rejects_oversized(small_tiff_path): + """Dask+GPU path also enforces ``max_pixels``.""" + path, _ = small_tiff_path + with pytest.raises(ValueError, match="safety limit|exceeds max_pixels"): + read_geotiff_gpu(path, chunks=4, max_pixels=10) + + +# --------------------------------------------------------------------------- +# open_geotiff dispatch: name= flows through every backend branch +# --------------------------------------------------------------------------- + + +def test_open_geotiff_chunks_name_flows_through(small_tiff_path): + path, arr = small_tiff_path + da = open_geotiff(path, chunks=4, name="dispatch_dask") + assert da.name == "dispatch_dask" + np.testing.assert_array_equal(da.values, arr) + + +@_gpu_only +def test_open_geotiff_gpu_name_flows_through(small_tiff_path): + path, arr = small_tiff_path + da = open_geotiff(path, gpu=True, name="dispatch_gpu") + assert da.name == "dispatch_gpu" + np.testing.assert_array_equal(da.data.get(), arr) + + +@_gpu_only +def test_open_geotiff_gpu_chunks_name_flows_through(small_tiff_path): + path, arr = small_tiff_path + da = open_geotiff(path, gpu=True, chunks=4, name="dispatch_dask_gpu") + assert da.name == "dispatch_dask_gpu" + np.testing.assert_array_equal(da.data.compute().get(), arr) + + +# --------------------------------------------------------------------------- +# open_geotiff dispatch: max_pixels reject flows through GPU branch +# --------------------------------------------------------------------------- + + +@_gpu_only +def test_open_geotiff_gpu_max_pixels_rejects(small_tiff_path): + path, _ = small_tiff_path + with pytest.raises(ValueError, match="safety limit|exceeds max_pixels"): + open_geotiff(path, gpu=True, max_pixels=10)