Files
LEDMatrix/test/test_logo_helper_download_backoff.py
ChuckandClaude Opus 5.5 7eb7a58d0c fix: web UI and src.common bugs (wifi wrong-password, plugin icon, starlark toggle, API caching, scroll/logo/font helpers) (#646)
- wifi: keep the "wrong_password:" prefix through the restore/AP fallback so
  the UI's incorrect-password prompt fires again.
- /plugins/installed returns the manifest's icon (string only).
- /starlark/apps/<id>/toggle coerces `enabled` and delegates to
  _toggle_starlark_app (disk before memory, no KeyError, "false" is false).
- /api/v3/ JSON GETs are sent Cache-Control: no-store; non-JSON keeps 5s.
- ScrollHelper.set_scrolling_image converts non-RGB input (alpha onto black);
  create/set_scrolling_image reset last_update_time like reset_scroll.
- LogoHelper backs off a failed download per path for
  MISSING_LOGO_RECHECK_SECONDS; cleared on invalidate/clear_cache.
- refresh_placeholder_timestamp saves atomically.
- FontManager.clear_cache / _clear_plugin_font_cache bump cache_generation.
- Odds manager: per-game logs to DEBUG; JSON decode error caught before
  RequestException (same cooldown).
- element_style mangled continuations; startup validator skips null plugin
  blocks and reuses the controller's discovery.
- src/common/README lists frame_timing, json_body, render_gate.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:26:05 -04:00

90 lines
2.8 KiB
Python

"""A failed logo download is not retried on every call.
A logo that is simply absent (not a stale placeholder) had nothing to back
off on: every load_logo_with_download() call retried the download, each with
a 30s timeout, for as long as the host was down or the URL was dead.
"""
import io
import logging
import time
from unittest.mock import MagicMock
import pytest
import requests
from PIL import Image
from src.common.logo_helper import MISSING_LOGO_RECHECK_SECONDS, LogoHelper
@pytest.fixture(autouse=True)
def _no_real_chmod(monkeypatch):
monkeypatch.setattr("src.common.logo_helper.ensure_directory_permissions", MagicMock())
monkeypatch.setattr("src.logo_downloader.ensure_file_permissions", MagicMock())
@pytest.fixture
def helper():
h = LogoHelper(display_width=64, display_height=32,
logger=logging.getLogger("test.logo_backoff"))
h.session.get = MagicMock(side_effect=requests.ConnectionError("down"))
return h
def _png_response():
buf = io.BytesIO()
Image.new("RGBA", (20, 20), (0, 128, 0, 255)).save(buf, "PNG")
data = buf.getvalue()
response = MagicMock()
response.__enter__.return_value = response
response.__exit__.return_value = False
response.raise_for_status = MagicMock()
response.headers = {"content-type": "image/png"}
response.iter_content = lambda *a, **k: iter([data])
return response
def _load(helper, path):
return helper.load_logo_with_download("PHI", path, "http://x/logo.png",
max_width=20, max_height=20)
def test_a_second_call_does_not_retry(helper, tmp_path):
path = tmp_path / "PHI.png"
assert _load(helper, path) is not None # placeholder
assert _load(helper, path) is not None
assert helper.session.get.call_count == 1
def test_it_retries_after_the_recheck_window(helper, tmp_path):
path = tmp_path / "PHI.png"
_load(helper, path)
helper._download_failures[str(path)] -= MISSING_LOGO_RECHECK_SECONDS + 1
_load(helper, path)
assert helper.session.get.call_count == 2
def test_other_paths_are_not_held_back(helper, tmp_path):
_load(helper, tmp_path / "PHI.png")
_load(helper, tmp_path / "DAL.png")
assert helper.session.get.call_count == 2
def test_clear_cache_forgets_failures(helper, tmp_path):
path = tmp_path / "PHI.png"
_load(helper, path)
helper.clear_cache()
_load(helper, path)
assert helper.session.get.call_count == 2
def test_invalidate_forgets_the_failure(helper, tmp_path):
path = tmp_path / "PHI.png"
_load(helper, path)
helper._invalidate_cached_logo("PHI", path)
helper.session.get = MagicMock(return_value=_png_response())
logo = _load(helper, path)
helper.session.get.assert_called_once()
assert path.exists()
assert logo.getpixel((0, 0))[:3] == (0, 128, 0)