Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/sweep-test-coverage-state.csv
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ rasterize,2026-06-18,2614;3102;3105;3296;3383,HIGH,4,"Pass 7 (2026-06-18, deep-s
reproject,2026-06-09,2618;3050;3100;3101;3141,MEDIUM,1,"CI follow-up same day: first CI run of the threaded streaming branch hard-crashed macos-arm64 py3.14 (SIGABRT in numba call_cfunc, two ThreadPoolExecutor threads concurrently inside try_numba_transform/tmerc_inverse) -- the projection kernels are @njit(parallel=True) and numba's workqueue threading layer aborts on concurrent entry; filed source bug #3141. Test fix: threaded parity test now uses transform_precision=0 (per-thread pyproj Transformer, no numba), the NaN multi-tile test and 3-D xfail forced serial (max_memory=1) so the numba fast path stays covered without concurrent entry. windows-3.14 failure was fail-fast collateral (its suite fully passed). Pass 2026-06-09 (deep-sweep test-coverage): delta re-sweep one day after the 2026-06-08 pass; module modified today by #3077 (datum-probe warning silencing) and #3081 (merge output-size guard backend-aware) -- both landed WITH their own tests (TestDatumProbeNoProjWarning; TestSecurityGuards merge-guard trio incl. the monkeypatched in-memory raise), so the delta added no gap; the guard branching is is_dask-only, so cupy eager shares the tested numpy branch (no per-backend guard test needed). Found one MEDIUM Cat 1 gap every prior pass missed: the 5th dispatch branch of reproject() -- the streaming fallback (_reproject_streaming / _process_tile_batch / _parse_max_memory, taken when source >512MB and dask is not importable) -- had zero coverage anywhere; _parse_max_memory only runs on that branch so the existing max_memory kwarg tests never reached it. Filed #3101, added test_reproject_streaming_3101.py (15 tests: parity vs in-memory numpy for threaded / serial(max_memory=1) / single-tile / nearest+NaN, plus 10 _parse_max_memory unit cases). Probe surfaced source bug #3100: streaming assembly allocates a 2-D output buffer but 3-D sources yield (h,w,b) tiles -> ValueError broadcast in both assembly loops; pinned with strict xfail, source fix left to #3100 (test-only PR, source untouched). CPU-only path so no GPU tests needed (CUDA host; file ran 14 passed + 1 xfailed). LOW carried (documented, not fixed): reproject(name=) / merge(name=) override values untested (only merge name fallback covered); non-square-cellsize successful anisotropic run; dask.bag distributed branch of _reproject_streaming still unexercised (needs a live distributed client). || PREVIOUS: Pass 2026-06-08 (deep-sweep test-coverage): #3050 closes the one live gap found this pass. reproject()'s dask+cupy backend was parity-tested only with resampling='cubic' (TestCupyPyprojFallbackParity::test_projected_to_projected_dask_cupy_match); nearest/bilinear were covered on numpy (end-to-end) and eager cupy (parametrized test_projected_to_projected_numpy_cupy_match) but never on the dask+cupy chunk-assembly path. Parametrized that test over ['nearest','bilinear','cubic']; all 3 RUN+PASS on a CUDA host. Cat 4 MEDIUM (resampling-mode parameter coverage on the dask+cupy backend). Test-only, source untouched. Re-confirmed _merge.merge() has NO genuine cupy/dask+cupy backend (_merge_inmemory/_merge_dask use _merge_arrays_numpy + raster.values; _merge_arrays_cupy is imported but never dispatched = dead code, not a test gap) matching the prior pass's observation. reproject() otherwise saturated across all 4 backends, NaN/Inf/all-NaN, degenerate shapes, metadata, vertical, bounds_policy, integer nodata. LOW (documented, not filed): dask+cupy resampling-mode parity is the only per-mode-per-backend cell that had been missing. || PREVIOUS: Pass 2026-05-29: reproject already has a deep suite (369 tests in test_reproject.py + coverage/gate files) covering all 4 backends, NaN/Inf/all-NaN/all-Inf, 1x1/2x2, metadata, vertical shift, bounds_policy x backends, integer nodata x backends. Gaps found: Cat 3 HIGH single-row (1xN) and single-col (Nx1) strip rasters never tested (hit size<2 branch of _validate_regular_axis + degenerate resampling axis); Cat 3 MEDIUM constant-value/zero-gradient raster never reprojected. Added TestDegenerateShapeReproject (12 tests): 1xN+Nx1 strips x numpy/dask/cupy/dask+cupy, constant raster numpy value-preservation + cross-backend parity. All 12 executed and passed on a CUDA host. Test-only, no source change (#2618). LOW (documented only): _merge._merge_arrays_cupy imported but never called by merge() (host-bounces via _merge_arrays_numpy) - dead-code source observation not a test gap; non-square cellsize reproject only covered via resolution-tuple validation errors not a successful anisotropic run."
resample,2026-05-29,2547;2615,HIGH,1;2;3;5,"Pass 2 (2026-05-29): added test_resample_cupy_agg_fallback_2615.py (6 tests, all passing on CUDA host). Closes Cat 1 MEDIUM backend-coverage gap: the cupy eager aggregate CPU fallback for average/min/max at a NON-integer downsample factor (_run_cupy fy==int(fy) branch in resample.py ~L957-973) was never exercised; existing TestCuPyParity used 12x12 scale 0.5 (integer factor 2 -> GPU reshape path) and only median/mode hit the host fallback. New tests use 10x10 scale 0.3 (factor 3.33) for average/min/max parity vs numpy plus a NaN-masked variant. Issue #2615. Module is otherwise very thoroughly covered (test_resample.py + 3 supplementary files); no remaining HIGH gaps found. Pass 1 (2026-05-27): added test_resample_coverage_2026_05_27.py with 70 tests (68 passing, 2 skipped). Closes Cat 3 HIGH Nx1 single-column gap across numpy/cupy/dask+numpy/dask+cupy x 8 methods (nearest/bilinear/cubic/average/min/max/median/mode) plus Nx1 upsample-nearest parity and Nx1 cross-backend aggregate parity. Closes Cat 2 MEDIUM NaN-parity gap on cupy and dask+cupy (existing TestCuPyParity/TestDaskCuPyParity used random data without NaN; the weight-mask gate and spline-prepad had no GPU NaN coverage). Closes Cat 3 MEDIUM all-equal-value raster across 8 methods (downsample) and 3 interp methods (upsample) plus a constant-with-NaN aggregate variant. Closes Cat 5 MEDIUM non-default dim-name propagation: lat/lon, latitude/longitude, and (channel, lat, lon) 3D round-trip without being renamed to y/x; per-dim attrs (units) preserved. Closes Cat 3 MEDIUM empty-raster behaviour pin: 0-row and 0-col rasters raise (currently IndexError) -- contract covered. Filed source-bug issue #2547: cubic on dask backends fails for Nx1 / arrays smaller than depth=16; the 2 skipped tests in this file gate on that fix landing. Source untouched."
slope,2026-05-29,2697,MEDIUM,3,"PR #2703: added degenerate-shape tests (1x1/1xN/Nx1) for all 4 planar backends + geodesic; no live bug, pins all-NaN+shape contract. CUDA host: cupy/dask+cupy ran. Backend/NaN/param/metadata coverage already complete."
templates,2026-06-26,3540,MEDIUM,3;4,"Deep-sweep test-coverage on a CUDA host (cuda available). Backend matrix already complete: from_template x4 backends (numpy/dask+numpy/cupy/dask+cupy) + dask alias + bad-backend all tested and green; preserve path also covers dask+cupy. Cat 2 N/A (procedural generator, no raster input; fill=NaN and fill=0 tested). Cat 5 covered (attrs/dims/coords + dask-vs-eager attrs equality asserted). Found two MEDIUM coverage gaps, no source bug. Cat 3: the max(1,...) width/height floor (single-pixel + Nx1/1xN strip) was untested -- probed live, (1,1) and (1,N) build correctly. Cat 4: the _normalize_resolution wrong-length tuple ValueError was the one validation error path with no test (all siblings tested). Added test-only test_single_pixel_grid, test_strip_grid, test_resolution_tuple_wrong_length; all RAN and PASSED on the CUDA host (66 passed, 0 skipped). LOW (documented, no test): dask+cupy block test does not assert value/coord parity with numpy; chunks param only exercised at default 'auto'. PR #3540 opened with the three tests. Standalone issue creation was blocked by the auto-mode classifier; humanized issue draft saved to scratchpad."
templates,2026-06-30,3580,MEDIUM,1;4,"Deep-sweep test-coverage re-run on a CUDA host (cuda available). Module is already heavily tested (281 tests): 4-backend matrix (numpy/dask+numpy/cupy/dask+cupy) + dask alias + bad-backend all green; preserve area/shape across backends; single-pixel + Nx1/1xN strips; cell-cap and chunk-count guards; padding/tiling helpers; country/region/city resolution + aliases; CF metadata + no-pyproj fallback. Cat 2 N/A (procedural generator, no raster input). Found two MEDIUM parameter-coverage gaps, no source bug. Cat 4: chunks=tuple only exercised via internal _estimate_n_chunks, never end-to-end through from_template (int/'auto' were); the dask chunk-count guard message on the explicit height/width path was untested (only the resolution-path message and the eager cell-cap message named the knob). Added test-only test_chunks_tuple_through_public_api and test_explicit_shape_chunk_count_message_names_height_width; both RAN and PASSED on the CUDA host (281 passed). LOW (documented, no test): non-NaN fill is only asserted on eager numpy (fill=0); probed live and works on all 4 backends but cross-backend fill value parity is not asserted. PR #3580 opened with the two tests."
viewshed,2026-05-29,2693,HIGH,1;2;5,"Pass 1 (2026-05-29): added 4 new test groups to test_viewshed.py (13 new tests + 1 xfail, all passing/xfailing on a CUDA+RTX host). Closes Cat 1 HIGH backend-coverage gap: the dask+cupy dispatch path in _viewshed_dask (Tier B) and _viewshed_windowed (max_distance) was registered but never invoked by any test -- added test_viewshed_dask_cupy_flat (analytical-angle parity, atol 0.03) and test_viewshed_dask_cupy_max_distance (windowed GPU run; observer cell 180, corners INVISIBLE). Both use non-zero flat terrain (1.3) because the RTX mesh builder rejects an all-zero raster (#1378). Closes Cat 5 HIGH metadata-preservation gap: only the numpy test_viewshed called general_output_checks; the cupy/dask/dask+cupy and max_distance paths never asserted attrs/coords/dims/array-type preservation. Added parametrised test_viewshed_metadata_preserved over {numpy,cupy,dask+numpy,dask+cupy} x {full, max_distance=2.0}: asserts attrs==, dims==, shape==, x/y coords allclose; runs general_output_checks (full type parity) for all backends except dask+cupy. Closes Cat 2 HIGH NaN-input gap and surfaced source bug #2693: viewshed on a numpy raster crashes with ValueError 'node not found' from _delete_from_tree when a NaN cell sits at certain positions (e.g. (2,4) in a 5x5 with observer at (2,2)), while NaN at (1,1)/(0,0)/(4,4) runs fine. Added test_viewshed_nan_input_supported_positions (parametrised working positions, asserts observer=180 and NaN cell is INVISIBLE/NaN) plus test_viewshed_nan_input_crashing_position (xfail strict, raises, links #2693). Noted but NOT fixed (source change out of scope for test sweep): the dask+cupy backend does not preserve the cupy backing -- _viewshed_dask computes then rewraps via da.from_array(result_np), so the output computes to numpy not cupy; general_output_checks is skipped for dask+cupy for that reason (candidate for the metadata/backend-parity sweep). LOW (documented only): non-square cell sizes; 1x1 and 1xN geometry covered behaviourally by probing (run without error). Test-only PR; viewshed.py untouched."
visibility,2026-06-10,3192,HIGH,1;2;4,"cupy cumulative_viewshed/visibility_frequency broken (numpy count + cupy viewshed) -> issue #3192 (dup #3193), fix in flight in #3205 with its own cupy parity tests, xfail pins dropped to avoid an XPASS race; added cupy _extract_transect+line_of_sight parity, NaN LOS, Fresnel-blocked branch; dask+metadata already covered"
zonal,2026-06-10,,HIGH,1,"deep-sweep test-coverage on CUDA host (cupy + dask+cupy live). Cat1 HIGH: regions() cupy/dask+cupy backends (_regions_cupy/_regions_dask_cupy via cupyx.scipy.ndimage.label) had ZERO test coverage -- every test_regions_* was ['numpy','dask+numpy'] only. Added test_regions_gpu_matches_numpy (cupy + dask+cupy, cell-by-cell parity vs numpy + general_output_checks). Cat1 MEDIUM: crosstab() 2D count and percentage aggs were ['numpy','dask+numpy'] only; extended test_count_crosstab_2d and test_percentage_crosstab_2d to all 4 backends (_crosstab_cupy/_crosstab_dask_cupy now exercised for count AND percentage; previously only count via the cat_ids #2560 test). All new/modified tests RAN and PASSED on GPU; full test_zonal.py 185 passed. No source bugs surfaced -- test-only change, no rockout PR needed beyond test additions. hypsometric_integral already fully covered in test_hypsometric_integral.py (4 backends, NaN/flat/single-cell/all-NaN/metadata). NOT gaps. LOW (documented, not fixed): trim()/crop() exercise cupy via _crop_backends_2561 but trim() has no cupy/dask+cupy parametrized parity test (trim source supports cupy); stats() return_type='xarray.DataArray' rejected on non-numpy so no GPU gap there."
28 changes: 28 additions & 0 deletions xrspatial/tests/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,19 @@ def test_explicit_chunks_bypass_default_tiling():
assert agg.data.chunks[1][0] == 512


@dask_array_available
def test_chunks_tuple_through_public_api():
import dask.array as da
# chunks may be a (chunk_y, chunk_x) tuple (a documented form); the public
# path must honor it verbatim and keep the resolution exact, the same as the
# int form. Only the int and 'auto' forms were exercised end-to-end before;
# the tuple form was checked only against the internal _estimate_n_chunks.
agg = from_template("conus", resolution=1000, chunks=(300, 400))
assert isinstance(agg.data, da.Array)
assert agg.data.chunksize == (300, 400)
assert agg.attrs["res"] == (1000.0, 1000.0)


def test_single_pixel_grid():
# a resolution coarser than the whole study-area box clamps width and height
# to the max(1, ...) floor, giving a 1x1 grid that still obeys the contract.
Expand Down Expand Up @@ -1075,6 +1088,21 @@ def test_oversized_explicit_shape_cap_message_names_height_width():
assert "resolution" not in str(exc.value)


@dask_array_available
def test_explicit_shape_chunk_count_message_names_height_width():
# The dask chunk-count guard on the height/width path must name the knob the
# caller actually set -- height/width -- not the derived resolution. chunks=
# promotes the eager default to dask and skips the cell cap, so this hits the
# chunk-count branch (not the cell-cap branch the message-naming test above
# covers). Only the resolution-path chunk-count message was tested before.
with pytest.raises(ValueError, match="chunk limit") as exc:
from_template("conus", height=2_000_000, width=2_000_000, chunks=512)
assert "height=2000000" in str(exc.value)
assert "width=2000000" in str(exc.value)
assert "smaller height/width" in str(exc.value)
assert "resolution" not in str(exc.value)


@dask_array_available
def test_explicit_height_width_with_preserve():
# height/width compose with preserve=: the exact shape is anchored at the
Expand Down
Loading