mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
- load_plugin: an on_enable() that raises unregisters the instance, so the next load retries instead of returning True "already loaded". - get_plugin_info: guard plugin.get_info(); one plugin raising no longer breaks /api/v3/plugins/installed. - plugin_state.json and the operation history are written with atomic_write_text under their lock. - plugin_loader: module-level lock serialises pip installs across the parallel startup loaders. - store_manager._install_via_download: extract dir cleanup moved to finally. - Test doubles: draw_image() warns (DeprecationWarning; the real DisplayManager has none), MockDisplayManager.draw_text accepts the real signature's optional params, VisualTestDisplayManager logs draw errors at WARNING. - Docs/comments: compatibility.py method name, PluginState.LOADED meaning, brittle schema count, why _report_skip_once uses setdefault. - Remove unused PluginOperationQueue.get_active_operations(). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
58 lines
2.1 KiB
Python
58 lines
2.1 KiB
Python
"""plugin_state.json and the operation history file are replaced atomically.
|
|
|
|
Both were written with a plain ``open(path, 'w')`` + ``json.dump`` outside
|
|
their lock. The open truncates first, so a value json can't encode (or a
|
|
crash, or a second Flask thread saving at the same moment) left a partial
|
|
file, and the next load dropped every saved state. They now serialise first
|
|
and go through a temp file + rename while holding the lock.
|
|
"""
|
|
|
|
import json
|
|
import threading
|
|
|
|
from src.plugin_system.operation_history import OperationHistory
|
|
from src.plugin_system.state_manager import PluginStateManager
|
|
|
|
|
|
def test_state_file_survives_a_failed_save(tmp_path):
|
|
state_file = tmp_path / "plugin_state.json"
|
|
mgr = PluginStateManager(state_file=str(state_file))
|
|
mgr.set_plugin_enabled("clock", True)
|
|
before = json.loads(state_file.read_text())
|
|
|
|
# Not JSON-serialisable: the save fails (and is logged, not raised).
|
|
mgr.update_plugin_state("clock", {"metadata": {"bad": object()}})
|
|
|
|
assert json.loads(state_file.read_text()) == before
|
|
assert [p.name for p in tmp_path.iterdir()] == ["plugin_state.json"]
|
|
|
|
|
|
def test_state_file_is_valid_after_concurrent_saves(tmp_path):
|
|
state_file = tmp_path / "plugin_state.json"
|
|
mgr = PluginStateManager(state_file=str(state_file))
|
|
|
|
def worker(n):
|
|
for i in range(15):
|
|
mgr.set_plugin_enabled(f"plugin-{n}", i % 2 == 0)
|
|
|
|
threads = [threading.Thread(target=worker, args=(n,)) for n in range(6)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
data = json.loads(state_file.read_text())
|
|
assert set(data["states"]) == {f"plugin-{n}" for n in range(6)}
|
|
|
|
|
|
def test_history_file_survives_a_failed_save(tmp_path):
|
|
history_file = tmp_path / "operation_history.json"
|
|
history = OperationHistory(history_file=str(history_file))
|
|
history.record_operation("install", plugin_id="clock")
|
|
before = json.loads(history_file.read_text())
|
|
|
|
history.record_operation("update", plugin_id="clock", details={"bad": object()})
|
|
|
|
assert json.loads(history_file.read_text()) == before
|
|
assert [p.name for p in tmp_path.iterdir()] == ["operation_history.json"]
|