mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
* chore(deprecation): remove the 35 APIs deprecated for 3.8.0 The usage scan (docs/DEPRECATIONS_3.8.md, regenerated 2026-10-01 and committed here) finds no call or override of any of them in the 46 monorepo plugins or the 8 third-party plugins plugins.json lists; the only core callers were other deprecated methods removed alongside. - CacheManager: 13 methods, plus the private helpers only has_data_changed used (_has_*_changed, _is_market_open). - DisplayManager: 7 methods, plus WEATHER_COLORS and the private _draw_sun/_cloud/_rain/_snow/_storm helpers only the icon methods used. - FontManager: 14 methods, plus size_tokens, _save_overrides and _clear_plugin_font_cache. font_overrides and _load_overrides stay: resolve_font() still applies config/font_overrides.json. performance_stats stays: get_font() keeps it and tests read it. - PluginManager.get_enabled_plugins. test_deprecation.py pins only the two 3.9.0 markers now; the scanner tests run against a stand-in core instead of the real markers. The memory-tier tests read stats through log_memory_cache_stats() and the component, and the test of the removed _clear_plugin_font_cache goes. Docs drop the removed methods' reference entries; the Deprecated APIs table becomes "Removed in 3.8.0". CHANGELOG gains a Removed section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(deprecation): drop the test harness's copies of the removed icon methods VisualTestDisplayManager still drew weather icons that DisplayManager no longer has, so a plugin's visual tests could pass on calls that raise AttributeError on the real display. Its draw_sun/draw_cloud/draw_rain/ draw_snow/draw_weather_icon/draw_text_with_icons, WEATHER_COLORS and the private helpers go, with the tests that exercised them. The CHANGELOG's Deprecations entries no longer say nothing is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
"""CacheManager's memory tier is MemoryCache's, not a copy of it.
|
|
|
|
CacheManager used to re-implement MemoryCache.cleanup() line for line and
|
|
read the component's private dicts and lock through aliases bound at
|
|
construction. Those aliases went stale the moment the component was replaced
|
|
(tests do exactly that), and the listing of the cache *directory* held the
|
|
memory tier's lock for the whole scan.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from src.cache.memory_cache import MemoryCache
|
|
from src.cache_manager import CacheManager
|
|
|
|
|
|
@pytest.fixture
|
|
def cm(tmp_path):
|
|
with patch('src.cache_manager.CacheManager._get_writable_cache_dir',
|
|
return_value=str(tmp_path)):
|
|
manager = CacheManager()
|
|
# The disk sweep thread stats this directory too; keep it out of the
|
|
# os.stat spies below.
|
|
manager.stop_cleanup_thread()
|
|
yield manager
|
|
|
|
|
|
def test_cleanup_and_stats_follow_a_replaced_component(cm):
|
|
"""Replace the component the way test_cache_ttl_honoured does; cleanup and
|
|
stats must act on the new one, not on dicts captured at construction."""
|
|
cm._memory_cache_component = MemoryCache(max_size=7, cleanup_interval=11.0)
|
|
cm._memory_cache_component.set("fresh", {"v": 1})
|
|
cm._memory_cache_component.set("stale", {"v": 2})
|
|
cm._memory_cache_component._timestamps["stale"] = time.time() - 4000
|
|
|
|
assert cm._cleanup_memory_cache(force=True) == 1
|
|
assert cm._memory_cache_component.get("stale") is None
|
|
assert cm._memory_cache_component.get("fresh") == {"v": 1}
|
|
|
|
with patch.object(cm.logger, "info") as info:
|
|
cm.log_memory_cache_stats()
|
|
assert "Size: 1/7 (14.3%)" in info.call_args[0][0]
|
|
|
|
|
|
def test_periodic_cleanup_is_throttled_and_records_its_run(cm):
|
|
mem = cm._memory_cache_component
|
|
mem.set("stale", {"v": 1})
|
|
mem._timestamps["stale"] = time.time() - 4000
|
|
|
|
# Within the interval: nothing runs, even through the get path.
|
|
assert cm._cleanup_memory_cache() == 0
|
|
assert mem.size() == 1
|
|
|
|
mem._last_cleanup = time.time() - mem._cleanup_interval - 1
|
|
before = time.time()
|
|
cm.get_cached_data("missing") # triggers the periodic sweep
|
|
assert mem.size() == 0
|
|
assert mem.get_stats()["last_cleanup"] >= before
|
|
|
|
|
|
def test_memory_stats_log_reads_the_live_tier(cm):
|
|
cm.set("k", {"v": 1})
|
|
with patch.object(cm.logger, "info") as info:
|
|
cm.log_memory_cache_stats()
|
|
message = info.call_args[0][0]
|
|
assert f"Size: 1/{cm._memory_cache_component.max_size()}" in message
|
|
assert "Last cleanup:" in message
|
|
|
|
|
|
def test_listing_the_cache_dir_does_not_hold_the_memory_lock(cm, tmp_path):
|
|
"""8,864 files on a real rig: every get/set used to wait out the scan."""
|
|
for name in ("a", "b"):
|
|
(tmp_path / f"{name}.json").write_text("{}")
|
|
(tmp_path / "notes.txt").write_text("x")
|
|
|
|
lock = cm._memory_cache_component._lock
|
|
held_during_stat = []
|
|
real_stat = os.stat
|
|
|
|
def spying_stat(path, *args, **kwargs):
|
|
held_during_stat.append(lock.locked())
|
|
return real_stat(path, *args, **kwargs)
|
|
|
|
with patch('src.cache_manager.os.stat', side_effect=spying_stat):
|
|
files = cm.list_cache_files()
|
|
|
|
assert held_during_stat and not any(held_during_stat)
|
|
assert sorted(f["key"] for f in files) == ["a", "b"]
|
|
|
|
|
|
def test_listing_skips_a_file_deleted_mid_scan(cm, tmp_path):
|
|
for name in ("a", "b"):
|
|
(tmp_path / f"{name}.json").write_text("{}")
|
|
real_stat = os.stat
|
|
|
|
def vanishing_stat(path, *args, **kwargs):
|
|
if str(path).endswith("a.json"):
|
|
raise FileNotFoundError(path)
|
|
return real_stat(path, *args, **kwargs)
|
|
|
|
with patch('src.cache_manager.os.stat', side_effect=vanishing_stat):
|
|
files = cm.list_cache_files()
|
|
|
|
assert [f["key"] for f in files] == ["b"]
|
|
|
|
|
|
def test_listing_is_newest_first(cm, tmp_path):
|
|
now = time.time()
|
|
for i, name in enumerate(("old", "mid", "new")):
|
|
p = tmp_path / f"{name}.json"
|
|
p.write_text("{}")
|
|
os.utime(p, (now - 300 + i * 100, now - 300 + i * 100))
|
|
assert [f["key"] for f in cm.list_cache_files()] == ["new", "mid", "old"]
|