Skip to content

Commit c0f9ab8

Browse files
committed
Move VRT DstRect negative-size check before overlap continue (#1737)
Address Copilot review feedback on #1742: 1. The negative-size branch in the resample-cap guard was unreachable. A negative xSize/ySize makes dst_*1 < dst_*0 so the source was already skipped by the overlap `continue`. Reject the malformed sizes before the overlap math with a tailored error message ("negative size") rather than reusing the pixel-budget message. 2. test_max_pixels_kwarg_raises_cap and test_dstrect_at_cap_succeeds did not actually exercise the override -- they used DstRects well under the default cap and never passed max_pixels=. Restructure each to first assert rejection under a tiny max_pixels then assert success when the cap is bumped, using the same VRT across both calls. Use a 10x10 VRT raster so the output buffer stays well under _check_dimensions while the resample intermediate is what the cap actually bites on. 3. Add test_negative_dstrect_y_size_rejected for symmetric coverage.
1 parent 6100ccf commit c0f9ab8

2 files changed

Lines changed: 64 additions & 28 deletions

File tree

xrspatial/geotiff/_vrt.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,19 @@ def read_vrt(vrt_path: str, *, window=None,
579579
dr = src.dst_rect
580580
sr = src.src_rect
581581

582+
# Reject malformed negative DstRect sizes up front, before the
583+
# overlap math. A negative ``xSize`` / ``ySize`` would otherwise
584+
# produce ``dst_*1 < dst_*0`` and the source would be silently
585+
# skipped by the overlap continue below; we'd rather surface the
586+
# malformed VRT to the caller than treat it as a no-op tile.
587+
# See issue #1737.
588+
if dr.x_size < 0 or dr.y_size < 0:
589+
raise ValueError(
590+
f"VRT SimpleSource DstRect has negative size "
591+
f"(xSize={dr.x_size}, ySize={dr.y_size}); "
592+
f"DstRect sizes must be non-negative."
593+
)
594+
582595
# Destination rect in virtual raster coordinates
583596
dst_r0 = dr.y_off
584597
dst_c0 = dr.x_off
@@ -631,9 +644,9 @@ def read_vrt(vrt_path: str, *, window=None,
631644
# ``xSize`` / ``ySize`` are orders of magnitude larger than
632645
# the VRT extent. Without this guard, a single source can
633646
# demand multi-gigabyte intermediates on a tiny output
634-
# raster. See issue #1737.
635-
if (dr.x_size < 0 or dr.y_size < 0
636-
or dr.x_size > max_pixels
647+
# raster. Negative sizes are already rejected above; here
648+
# we just enforce the pixel-budget. See issue #1737.
649+
if (dr.x_size > max_pixels
637650
or dr.y_size > max_pixels
638651
or dr.x_size * dr.y_size > max_pixels):
639652
raise ValueError(

xrspatial/geotiff/tests/test_vrt_dstrect_resample_cap_1737.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -87,42 +87,65 @@ def test_legitimate_upsample_still_works():
8787

8888

8989
def test_max_pixels_kwarg_raises_cap():
90-
"""When the caller bumps ``max_pixels``, a previously-rejected DstRect
91-
is accepted (matches the contract for other read paths)."""
90+
"""A DstRect rejected under a tiny ``max_pixels`` must be accepted
91+
when the caller bumps the cap. This exercises the override contract
92+
(the same VRT goes from rejected to accepted across the two calls).
93+
94+
The output buffer is kept tiny (VRT raster 10x10) so the cap only
95+
bites on the resample intermediate, not on the ``_check_dimensions``
96+
pre-allocation guard.
97+
"""
9298
with tempfile.TemporaryDirectory() as td:
9399
_write_source(td)
94-
vrt_path = _write_vrt(td, dst_x_size=2000, dst_y_size=2000)
95-
# Default cap is 1e9, 2000*2000=4e6 well under.
96-
arr, _ = read_vrt(vrt_path)
97-
assert arr.shape == (100, 100)
100+
# 2000x2000 = 4e6 intermediate pixels. Output buffer is 10x10=100
101+
# pixels, far below both caps below. Upsample 10x10 -> 2000x2000
102+
# so ``needs_resample`` is true and the cap branch runs.
103+
vrt_path = _write_vrt(td, dst_x_size=2000, dst_y_size=2000,
104+
raster_x=10, raster_y=10)
105+
# First, the cap is too small for the intermediate: rejected.
106+
with pytest.raises(ValueError, match="resample intermediate"):
107+
read_vrt(vrt_path, max_pixels=1_000_000)
108+
# Bump the cap above 4e6: accepted.
109+
arr, _ = read_vrt(vrt_path, max_pixels=4_000_000)
110+
assert arr.shape == (10, 10)
98111

99112

100113
def test_dstrect_at_cap_succeeds():
101-
"""Exactly at ``max_pixels`` is accepted; the cap is inclusive."""
114+
"""Exactly at ``max_pixels`` is accepted; the cap is inclusive.
115+
Together with :func:`test_max_pixels_kwarg_raises_cap`, this pins
116+
down both sides of the override boundary."""
102117
with tempfile.TemporaryDirectory() as td:
103118
_write_source(td)
104-
# max_pixels=10000 means dst 100x100 = 10000 is allowed.
105-
vrt_path = _write_vrt(td, dst_x_size=100, dst_y_size=100)
119+
# Upsample 10x10 -> 100x100 so ``needs_resample`` is true. The
120+
# VRT raster is 10x10 so the output buffer stays at 100 pixels,
121+
# well below the ``_check_dimensions`` guard; the cap below
122+
# therefore exercises the resample-intermediate branch only.
123+
vrt_path = _write_vrt(td, dst_x_size=100, dst_y_size=100,
124+
raster_x=10, raster_y=10)
125+
# Just below the cap rejects.
126+
with pytest.raises(ValueError, match="resample intermediate"):
127+
read_vrt(vrt_path, max_pixels=9_999)
128+
# At the cap succeeds.
106129
arr, _ = read_vrt(vrt_path, max_pixels=10000)
107-
assert arr.shape == (100, 100)
130+
assert arr.shape == (10, 10)
108131

109132

110133
def test_negative_dstrect_rejected():
111-
"""Negative ``xSize`` / ``ySize`` must surface as ``ValueError`` rather
112-
than degenerate into a negative-stride numpy slice."""
134+
"""Negative ``xSize`` / ``ySize`` must surface as ``ValueError``
135+
rather than be silently skipped by the overlap check. The error
136+
message must call out the malformed negative size, not the pixel
137+
budget."""
113138
with tempfile.TemporaryDirectory() as td:
114139
_write_source(td)
115140
vrt_path = _write_vrt(td, dst_x_size=-5, dst_y_size=100)
116-
# The negative size makes ``needs_resample`` true because it differs
117-
# from sr.x_size=10; the cap branch catches the negative value.
118-
# If the source dstrect doesn't overlap the window the source is
119-
# skipped silently (returns the empty fill array) and no resample
120-
# runs - that's also OK; we accept either behaviour.
121-
try:
122-
arr, _ = read_vrt(vrt_path)
123-
# If it did succeed, the array must still be the VRT extent.
124-
assert arr.shape == (100, 100)
125-
except ValueError as e:
126-
# The cap message identifies the resample intermediate, which is
127-
# what we want to surface here.
128-
assert "resample intermediate" in str(e)
141+
with pytest.raises(ValueError, match="negative size"):
142+
read_vrt(vrt_path)
143+
144+
145+
def test_negative_dstrect_y_size_rejected():
146+
"""Negative ``ySize`` is also rejected with the same tailored error."""
147+
with tempfile.TemporaryDirectory() as td:
148+
_write_source(td)
149+
vrt_path = _write_vrt(td, dst_x_size=100, dst_y_size=-5)
150+
with pytest.raises(ValueError, match="negative size"):
151+
read_vrt(vrt_path)

0 commit comments

Comments
 (0)