mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 23:05:10 +00:00
test: stop the suite reinstalling plugins into the real plugin-repos/ (#679)
Any test that imported web_interface.app and sent a request fired the app's startup reconciliation, which runs against the checkout's real config.json and plugin-repos/ and reinstalls every configured-but-missing plugin from the live store. A full Windows run left basketball-scoreboard, calendar, football-scoreboard, leaderboard and ledmatrix-stocks untracked in plugin-repos/ (not gitignored) from that daemon thread. test/conftest.py now installs an import hook that sets the app's run-once _reconciliation_started latch as the module finishes executing, so lazy imports, module-level imports and reloads all start disarmed. StateReconciliation's own tests are unaffected. A regression test pins that a request to the imported app launches no reconciliation thread. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+50
-1
@@ -16,7 +16,54 @@ if str(project_root) not in sys.path:
|
|||||||
sys.path.insert(0, str(project_root))
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
|
||||||
|
class _DisarmStartupReconciliation:
|
||||||
|
"""Import hook: every ``web_interface.app`` this process builds starts disarmed.
|
||||||
|
|
||||||
|
app.py wires itself to the checkout's real config/config.json and
|
||||||
|
plugin-repos/ at import, and its before_request hook launches startup
|
||||||
|
reconciliation on the first request any test sends. Reconciliation
|
||||||
|
reinstalls every configured plugin missing on disk from the live store,
|
||||||
|
so a full run downloaded basketball-scoreboard, calendar,
|
||||||
|
football-scoreboard, leaderboard and ledmatrix-stocks into the real
|
||||||
|
plugin-repos/ (not gitignored), minutes in, from a daemon thread no test
|
||||||
|
waits on. Setting ``_reconciliation_started`` is the app's own run-once
|
||||||
|
latch; doing it as the module finishes executing covers fixtures that
|
||||||
|
import the app lazily and send a request at once, and ``importlib.reload``.
|
||||||
|
StateReconciliation itself stays fully testable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_MODULE = "web_interface.app"
|
||||||
|
|
||||||
|
def find_spec(self, fullname, path, target=None):
|
||||||
|
if fullname != self._MODULE:
|
||||||
|
return None
|
||||||
|
import importlib.machinery
|
||||||
|
spec = importlib.machinery.PathFinder.find_spec(fullname, path, target)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
return spec
|
||||||
|
exec_module = spec.loader.exec_module
|
||||||
|
|
||||||
|
def exec_disarmed(module):
|
||||||
|
exec_module(module)
|
||||||
|
module._reconciliation_started = True
|
||||||
|
|
||||||
|
spec.loader.exec_module = exec_disarmed
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
_DISARM_HOOK = _DisarmStartupReconciliation()
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
|
sys.meta_path.insert(0, _DISARM_HOOK)
|
||||||
|
app_module = sys.modules.get(_DisarmStartupReconciliation._MODULE)
|
||||||
|
if app_module is not None:
|
||||||
|
app_module._reconciliation_started = True
|
||||||
|
|
||||||
|
_point_emulator_at_raw_adapter(config)
|
||||||
|
|
||||||
|
|
||||||
|
def _point_emulator_at_raw_adapter(config):
|
||||||
"""Point the emulator at a per-process config that binds no socket.
|
"""Point the emulator at a per-process config that binds no socket.
|
||||||
|
|
||||||
Six test modules set EMULATOR=true and build a real DisplayManager. The
|
Six test modules set EMULATOR=true and build a real DisplayManager. The
|
||||||
@@ -63,7 +110,9 @@ def pytest_configure(config):
|
|||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config):
|
def pytest_unconfigure(config):
|
||||||
"""Remove the throwaway emulator config written by pytest_configure."""
|
"""Undo pytest_configure: the import hook and the throwaway emulator config."""
|
||||||
|
if _DISARM_HOOK in sys.meta_path:
|
||||||
|
sys.meta_path.remove(_DISARM_HOOK)
|
||||||
tmp_dir = getattr(config, "_ledmatrix_emulator_tmp", None)
|
tmp_dir = getattr(config, "_ledmatrix_emulator_tmp", None)
|
||||||
if tmp_dir is not None:
|
if tmp_dir is not None:
|
||||||
import shutil
|
import shutil
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
No test may start the real app's startup reconciliation.
|
||||||
|
|
||||||
|
web_interface/app.py reads the checkout's real config/config.json and
|
||||||
|
plugin-repos/ at import, and its first request launches a reconciliation
|
||||||
|
thread that reinstalls every configured-but-missing plugin from the live
|
||||||
|
store. A full suite run on a dev checkout used to leave whole plugins
|
||||||
|
untracked in plugin-repos/ that way. test/conftest.py disarms the run-once
|
||||||
|
latch on every import of the module; this pins that.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_request_to_the_imported_app_launches_no_reconciliation():
|
||||||
|
import web_interface.app as web_app
|
||||||
|
|
||||||
|
assert web_app._reconciliation_started is True
|
||||||
|
|
||||||
|
with patch.object(web_app, "threading", MagicMock()) as threading_mock:
|
||||||
|
web_app.app.test_client().get("/favicon.ico")
|
||||||
|
|
||||||
|
threading_mock.Thread.assert_not_called()
|
||||||
Reference in New Issue
Block a user