Fix IndexError in find_blank_cut when the cut lands on the image edge

A cut position after the last column is legitimate — _crop_to_budget asks for
min(start + budget, img.width), which equals the width whenever the remaining
strip is shorter than the budget. find_blank_cut clamped target to width but
then walked leftwards starting at target itself, so ink[width] raised
IndexError.

Caught on hardware: it killed the ledmatrix-stocks fetch, and because
_fetch_plugin_content catches broadly that surfaced as the plugin silently
contributing nothing for the cycle.

Only reachable on the second or later pass of the rotating window over a single
oversized image, which is why the existing tests missed it — they all exercised
the first pass, where start is 0 and start + budget is comfortably inside the
image. Added TestRotationAcrossMultipleCycles, which walks the window round
several times and asserts content is never lost, plus direct coverage of
find_blank_cut at and beyond the image edge.

Both bounds now stop at width - 1 so neither direction can index past the end.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-29 09:42:51 -04:00
co-authored by Claude
parent 62919a13e3
commit 105c6df019
3 changed files with 97 additions and 4 deletions
+41
View File
@@ -928,3 +928,44 @@ class TestBudgetUsesMeasuredGaps:
p = RenderPipeline(VegasModeConfig(lead_in_width=0, **cfg), DM(), FakeStream())
assert p._join_plugin_rows(selected).width <= DISPLAY_W
class TestRotationAcrossMultipleCycles:
"""
The single-image crop advances a window across cycles. The second and later
passes are where start + budget can land exactly on the image width, which
crashed find_blank_cut in the field and lost that plugin's content for the
cycle. First-pass-only tests never reach it.
"""
def test_window_advances_over_many_cycles_without_error(self):
# Mirrors the field case: 1840px stocks strip, 1536px budget, so the
# second pass starts at 1536 and start + budget == 3072 -> clamped to
# the 1840 width, i.e. target == img.width.
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=3.0)
strip = canvas([(0, 1840)], width=1840)
widths = []
for _ in range(8):
adapter.invalidate_cache('ledmatrix-stocks')
images = adapter.get_content(NativePlugin([strip]), 'ledmatrix-stocks')
assert images, "content must never be lost mid-rotation"
widths.append(images[0].width)
assert all(w > 0 for w in widths)
def test_offset_wraps_back_to_zero_at_the_end(self):
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=3.0)
strip = canvas([(0, 1840)], width=1840)
seen_reset = False
for _ in range(6):
adapter.invalidate_cache('s')
adapter.get_content(NativePlugin([strip]), 's')
if adapter._item_offsets.get('s', 0) == 0:
seen_reset = True
assert seen_reset, "window should wrap round rather than stall at the end"
def test_multi_row_rotation_never_returns_empty(self):
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=1.0)
rows = [canvas([(0, 200)], width=200) for _ in range(7)]
for _ in range(10):
adapter.invalidate_cache('rows')
assert adapter.get_content(NativePlugin(rows), 'rows')