From 0f91ff88d1a244513dd327bbf9fd61d07df2a14c Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Tue, 30 Jun 2026 12:08:23 -0700 Subject: [PATCH 1/2] Test from_template tuple chunks and explicit-shape chunk-cap message Add two test-coverage tests for already-correct from_template behavior: - chunks passed as a (chunk_y, chunk_x) tuple through the public API. The tuple form is documented but was only checked against the internal _estimate_n_chunks helper, never end-to-end. Assert it is honored verbatim and the resolution stays exact. - the dask chunk-count guard on the height/width path. Its message must name height/width (the knob the caller set), not the derived resolution. Only the resolution-path chunk-count message and the eager cell-cap message were covered before. Test-only; no source change. --- xrspatial/tests/test_templates.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/xrspatial/tests/test_templates.py b/xrspatial/tests/test_templates.py index af6bce1c8..9fe49e459 100644 --- a/xrspatial/tests/test_templates.py +++ b/xrspatial/tests/test_templates.py @@ -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. @@ -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 From 638fd57957352710faccda14ea00813baadd2ebe Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Tue, 30 Jun 2026 12:09:22 -0700 Subject: [PATCH 2/2] sweep-test-coverage: record templates re-run (PR #3580) --- .claude/sweep-test-coverage-state.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/sweep-test-coverage-state.csv b/.claude/sweep-test-coverage-state.csv index e1288fd8f..a26dfa08d 100644 --- a/.claude/sweep-test-coverage-state.csv +++ b/.claude/sweep-test-coverage-state.csv @@ -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."