Files
LEDMatrix/test/test_vegas_live_property.py
T
ChuckandClaude Opus 5.5 7e066174d9 chore: remove dead code, deprecate unused plugin APIs (over-engineering audit)
Whole-tree audit. Every symbol was checked against core, the plugin
monorepo and all eight third-party plugins in plugins.json first.

- Deprecate (removal 3.10.0) plugin-facing methods nothing calls:
  LogoDownloader bulk download, ConfigManager backup/secret wrappers,
  APIHelper extras, BackgroundDataService poll API, PluginManager /
  PluginStateManager info readers, and a few CacheManager, FontManager,
  BaseOddsManager, DynamicTeamResolver methods and PluginTestCase.
  plugin_api_usage.py learns their receiver names; DEPRECATIONS doc
  regenerated.
- Remove core-internal dead code: CacheMetrics, Vegas status/stats
  plumbing, sync "new cycle" message (followers ignore unknown types),
  unused operation types, test-only PluginCatalog readers, IPC to_dict
  and ping, _parse_form_value, CacheStrategyProtocol, ErrorAggregator
  callbacks, duplicate web response helpers.
- Web UI: drop never-mounted json-file-manager.js, the example widget,
  utils/error_handler.js, four uncalled PluginAPI methods, and 29
  escapeHtml shims (call window.LEDEscape directly). Public globals,
  BaseWidget and widget names unchanged.
- Remove six one-off scripts (owner decision) and the unused markupsafe
  and pytest-mock pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:27:40 -04:00

154 lines
5.3 KiB
Python

"""Live patches change live elements' columns and nothing else, whatever happens.
Two pipelines are built from the same content and put through the same random
sequence of scrolling, extending and trimming. One of them also has random
patches applied. After every step, the only columns allowed to differ between
the two are those of live elements, and each of those must show exactly the
last patch applied to it (or its original pixels).
"""
import collections
import random
import sys
from pathlib import Path
import numpy as np
import pytest
from PIL import Image, ImageDraw
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src.vegas_mode import elements # noqa: E402
from src.vegas_mode.config import VegasModeConfig # noqa: E402
from src.vegas_mode.elements import ElementMeta, LivePatch # noqa: E402
from src.vegas_mode.render_pipeline import RenderPipeline # noqa: E402
W, H = 96, 24
def _live(key, width, seed):
rng = np.random.default_rng(seed)
pixels = rng.integers(20, 255, (H, width, 3), dtype=np.uint8)
image = Image.frombytes("RGB", (width, H), pixels.tobytes())
pinned, array = elements.pin_element(image, 8)
return elements.tag(pinned, ElementMeta("p", key, 0, elements.pixel_digest(array), 0.0, 0.0))
def _plain(width, seed):
image = Image.new("RGB", (width, H))
ImageDraw.Draw(image).rectangle([0, 0, width - 1, H - 1],
outline=(seed * 37 % 255, 90, 200))
return image
def _groups(rng, n):
groups = []
for g in range(n):
group = []
for p in range(rng.randint(1, 3)):
rows = []
for r in range(rng.randint(1, 3)):
key = f"g{g}p{p}r{r}"
if rng.random() < 0.6:
rows.append(_live(key, rng.randint(10, 50), rng.randint(0, 10**6)))
else:
rows.append(_plain(rng.randint(10, 50), rng.randint(0, 255)))
group.append((f"pl{g}{p}", rows))
groups.append(group)
return groups
class _Stream:
def __init__(self, groups):
self.groups = groups
self.plugin_manager = type("PM", (), {"plugins": {}})()
self.plugin_adapter = None
self.i = 0
def get_grouped_content_for_composition(self):
return self.groups[0]
def take_next_group(self, count=None, offscreen_only=False):
self.i += 1
return self.groups[self.i] if self.i < len(self.groups) else []
class _DM:
width, height = W, H
def __init__(self):
self.image = Image.new("RGB", (W, H))
def set_scrolling_state(self, *a):
pass
def update_display(self):
pass
def _pipeline(groups):
p = RenderPipeline(VegasModeConfig(continuous_scroll=True, lead_in_width=0,
separator_width=12), _DM(), _Stream(groups))
start_prefetch = p.start_prefetch
def prefetch_now():
# Done before the next extension, so both twins append the same
# groups the same way (prepared ahead) whatever the thread timing.
start_prefetch()
if p._prefetch_thread is not None:
p._prefetch_thread.join(5)
p.start_prefetch = prefetch_now
assert p.compose_scroll_content()
return p
@pytest.mark.parametrize("seed", range(12))
def test_patches_touch_only_live_columns(seed):
rng = random.Random(seed)
groups = _groups(rng, 25)
patched, reference = _pipeline(groups), _pipeline(groups)
expected = {} # seq -> pixels the patched strip must show
counter = collections.Counter()
for _step in range(60):
action = rng.random()
if action < 0.35:
advance = rng.randint(5, 60)
for p in (patched, reference):
p.scroll_helper.scroll_position += advance
elif action < 0.6:
for p in (patched, reference):
if p.scroll_helper.remaining_unscrolled() < 4 * W:
p.extend_scroll_content()
counter["extend"] += 1
else:
records = patched.live_records()
if not records:
continue
record = rng.choice(records)
pixels = np.full((H, record.width, 3), rng.randint(0, 255), dtype=np.uint8)
pixels.setflags(write=False)
patched._live_slots[record.seq] = LivePatch(
seq=record.seq, strip_gen=patched._strip_gen, epoch=0, pixels=pixels,
digest=elements.pixel_digest(pixels), made_at=0.0)
patched._live_ready.append(record.seq)
if patched.apply_live_patches():
expected[record.seq] = pixels
counter["patch"] += 1
# The twins agree on everything but live columns.
a = patched.scroll_helper.cached_array
b = reference.scroll_helper.cached_array
assert a.shape == b.shape
assert patched._strip_origin == reference._strip_origin
live = np.zeros(a.shape[1], dtype=bool)
for record in patched.live_records():
x = record.abs_x - patched._strip_origin
lo, hi = max(0, x), max(0, x + record.width)
live[lo:hi] = True
if record.seq in expected:
assert np.array_equal(a[:, lo:hi], expected[record.seq][:, lo - x:hi - x])
assert np.array_equal(a[:, ~live], b[:, ~live])
assert counter["patch"] > 0