mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 06:45:09 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d5a7cd692 | ||
|
|
01fea5c76d |
@@ -885,6 +885,25 @@ policies are unchanged.
|
||||
a runtime publisher that stops still goes `stale`, and a subscription that
|
||||
goes quiet still falls back to the cache. The cache path's 120 s rule is
|
||||
unchanged.
|
||||
- A plugin that pauses the Vegas scroll gets its pause when its display
|
||||
duration is not a plain number. Several plugins (clock-simple, calendar,
|
||||
countdown) return `display_duration` as it is in config.json, so a value
|
||||
saved as `"20"` or `null` (the raw config editor, a hand edit) reached the
|
||||
pause as a string or None; comparing it with the clock raised, and the
|
||||
plugin flashed up and the scroll went straight on, at every one of its
|
||||
turns. `inf` held the pause until something interrupted it, and 0, a
|
||||
negative number or NaN ended it at once. The pause now reads the duration
|
||||
as the rotation does (`finite_seconds()` in `base_plugin`): a numeric
|
||||
string counts, anything else that is not a finite number (or a
|
||||
`get_display_duration()` that raises) pauses for 30 s, and a number at or
|
||||
below zero for 15 s, with one warning per plugin.
|
||||
- Reinstalling Weather, Music, Stocks or Leaderboard from the Plugin Store
|
||||
while it is enabled asks for a display restart, as reinstalling any other
|
||||
enabled plugin does. `POST /api/v3/plugins/install` looked for the
|
||||
plugin's `enabled` flag under the store id (`weather`), but its config
|
||||
section is under the id its manifest declares (`ledmatrix-weather`), so
|
||||
`restart_required` was always false and the display kept running the
|
||||
copy it had loaded. The check now uses the installed id.
|
||||
|
||||
### Scrolling
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import os
|
||||
import inspect
|
||||
import signal
|
||||
import json
|
||||
import math
|
||||
import threading
|
||||
import types
|
||||
from collections import deque
|
||||
@@ -57,6 +56,7 @@ from src.ipc.contract import (
|
||||
PluginReloadResult,
|
||||
)
|
||||
from src.ipc.server import ControlServer, QueuedCommand, StateHub, start_control_server
|
||||
from src.plugin_system.base_plugin import finite_seconds
|
||||
from src.vegas_mode.render_pipeline import SYNC_SEND_INTERVAL
|
||||
|
||||
# Get logger with consistent configuration
|
||||
@@ -90,19 +90,6 @@ _MIN_INITIAL_UPDATE_TIMEOUT_SECONDS = 2.0
|
||||
DEFAULT_DYNAMIC_DURATION_CAP = 180.0
|
||||
|
||||
|
||||
def _finite_seconds(value: Any) -> Optional[float]:
|
||||
"""``value`` as seconds when it is a finite number or a numeric string,
|
||||
else None. A bool is not a number here, though it is an int: True would
|
||||
read as a one-second screen."""
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
seconds = float(value)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
return seconds if math.isfinite(seconds) else None
|
||||
|
||||
|
||||
class _PluginReloadJob:
|
||||
"""A ``plugin.reload`` whose slow half runs off the render thread.
|
||||
|
||||
@@ -1392,7 +1379,7 @@ class DisplayController:
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
problem = f"get_display_duration() raised {type(err).__name__}: {err}"
|
||||
else:
|
||||
seconds = _finite_seconds(value)
|
||||
seconds = finite_seconds(value)
|
||||
if seconds is not None:
|
||||
return seconds
|
||||
problem = f"display duration {value!r} is not a number"
|
||||
|
||||
@@ -11,6 +11,7 @@ Stability: Stable - maintains backward compatibility
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, Optional, List
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from src.deprecation import deprecated, warn_deprecated
|
||||
@@ -240,6 +241,26 @@ def resolve_vegas_participation(plugin: Any, plugin_id: Optional[str] = None) ->
|
||||
return legacy_vegas_participation(plugin)
|
||||
|
||||
|
||||
def finite_seconds(value: Any) -> Optional[float]:
|
||||
"""``value`` as seconds when it is a finite number or a numeric string,
|
||||
else None. A bool is not a number here, though it is an int: True would
|
||||
read as a one-second screen.
|
||||
|
||||
How the core reads a plugin's get_display_duration() -- the rotation
|
||||
(DisplayController._get_display_duration) and the Vegas static pause --
|
||||
which several plugins answer straight from config.json, so a value saved
|
||||
as "20" or null arrives as a string or None. A number at or below zero is
|
||||
returned as it is; each caller has its own rule for that.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
seconds = float(value)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
return seconds if math.isfinite(seconds) else None
|
||||
|
||||
|
||||
class BasePlugin(ABC):
|
||||
"""
|
||||
Base class that all plugins must inherit from.
|
||||
|
||||
@@ -18,10 +18,11 @@ import math
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
from typing import Optional, Dict, Any, List, Callable, TYPE_CHECKING
|
||||
from typing import Optional, Dict, Any, FrozenSet, List, Callable, TYPE_CHECKING
|
||||
|
||||
from src import display_watchdog
|
||||
from src.common import render_gate
|
||||
from src.plugin_system.base_plugin import finite_seconds
|
||||
from src.vegas_mode.config import VegasModeConfig
|
||||
from src.vegas_mode.elements import LiveEpochs
|
||||
from src.vegas_mode.plugin_adapter import PluginAdapter
|
||||
@@ -53,6 +54,14 @@ _FPS_HEARTBEAT_INTERVAL = 300.0
|
||||
#: every plugin. Game state doesn't change within a quarter second.
|
||||
_LIVE_PRIORITY_CHECK_INTERVAL = 0.25
|
||||
|
||||
#: Seconds a static pause shows a plugin whose display duration can't be
|
||||
#: used, as long as the rotation shows it: 30 when get_display_duration()
|
||||
#: raises or answers something that is not a number
|
||||
#: (DisplayController._get_display_duration), 15 when it answers a number at
|
||||
#: or below zero (DisplayController._resolve_durations).
|
||||
_UNREADABLE_DURATION = 30.0
|
||||
_NOT_POSITIVE_DURATION = 15.0
|
||||
|
||||
|
||||
def _percentile(ordered: List[float], fraction: float) -> float:
|
||||
"""Nearest-rank percentile of an already-sorted list.
|
||||
@@ -92,6 +101,9 @@ class VegasModeCoordinator:
|
||||
_live_reason: Optional[str] = None
|
||||
# Set only while Vegas has changed the GIL switch interval; read with getattr.
|
||||
_saved_switch_interval: Optional[float]
|
||||
#: Plugins already warned about a display duration the pause can't use,
|
||||
#: so a bad setting logs once, not at every turn. Replaced, not mutated.
|
||||
_duration_warned: FrozenSet[str] = frozenset()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1010,7 +1022,7 @@ class VegasModeCoordinator:
|
||||
# Wait for the plugin's display duration. Monotonic, like the
|
||||
# iteration clock: an NTP step on an RTC-less Pi would otherwise
|
||||
# end the pause at once or stretch it by the correction.
|
||||
duration = plugin.get_display_duration()
|
||||
duration = self._static_pause_duration(plugin)
|
||||
start = time.monotonic()
|
||||
|
||||
while time.monotonic() - start < duration:
|
||||
@@ -1046,6 +1058,42 @@ class VegasModeCoordinator:
|
||||
|
||||
return True
|
||||
|
||||
def _static_pause_duration(self, plugin: 'BasePlugin') -> float:
|
||||
"""Seconds a static pause shows ``plugin``: its display duration,
|
||||
read the way the rotation reads it.
|
||||
|
||||
Several plugins return their display_duration setting straight from
|
||||
config.json, so one saved as "20" or null came back as a string or
|
||||
None; comparing it with the clock raised, and the pause's broad
|
||||
except ended the pause at every one of the plugin's turns. inf
|
||||
paused until something interrupted it, and NaN, False, 0 or a
|
||||
negative number ended the pause at once. A numeric string counts
|
||||
(finite_seconds); anything else, or a raise, gets
|
||||
_UNREADABLE_DURATION, and a number at or below zero
|
||||
_NOT_POSITIVE_DURATION, logged once per plugin.
|
||||
"""
|
||||
try:
|
||||
value = plugin.get_display_duration()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
problem = f"get_display_duration() raised {type(err).__name__}: {err}"
|
||||
seconds = _UNREADABLE_DURATION
|
||||
else:
|
||||
seconds = finite_seconds(value)
|
||||
if seconds is not None and seconds > 0:
|
||||
return seconds
|
||||
if seconds is None:
|
||||
problem = f"display duration {value!r} is not a number"
|
||||
seconds = _UNREADABLE_DURATION
|
||||
else:
|
||||
problem = f"display duration {value!r} is not above zero"
|
||||
seconds = _NOT_POSITIVE_DURATION
|
||||
plugin_id = plugin.plugin_id
|
||||
if plugin_id not in self._duration_warned:
|
||||
self._duration_warned = self._duration_warned | {plugin_id}
|
||||
logger.warning("[%s] %s; its static pause lasts %.0fs (logged once)",
|
||||
plugin_id, problem, seconds)
|
||||
return seconds
|
||||
|
||||
def _end_static_pause(self) -> None:
|
||||
"""End static pause and restore scroll state."""
|
||||
should_resume_scrolling = False
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""POST /plugins/install asks for a restart by the id the plugin installed as.
|
||||
|
||||
A store install needs a display restart when config.json already enables the
|
||||
plugin (a reinstall, or a config carried over): the display loads a plugin
|
||||
when its ``enabled`` flag changes, and this flag did not. The route read the
|
||||
flag under the registry id it was given. An aliased entry installs under
|
||||
another id -- ``weather`` installs a directory whose manifest declares
|
||||
``ledmatrix-weather``, and its config section is ``ledmatrix-weather`` -- so
|
||||
reinstalling an enabled Weather never reported that a restart was needed,
|
||||
and the display kept running the old copy.
|
||||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401
|
||||
|
||||
INSTALL = "/api/v3/plugins/install"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(api_v3_module, tmp_path):
|
||||
"""The store installs registry entry ``weather`` as ``installed_id``."""
|
||||
manager = api_v3_module.api_v3.plugin_store_manager
|
||||
manager.install_plugin.return_value = True
|
||||
manager.get_registry_info.return_value = None
|
||||
manager._find_plugin_path.return_value = None
|
||||
|
||||
def installs_as(installed_id):
|
||||
path = tmp_path / installed_id
|
||||
path.mkdir()
|
||||
(path / "manifest.json").write_text(json.dumps({"id": installed_id}),
|
||||
encoding="utf-8")
|
||||
manager._find_plugin_path.side_effect = (
|
||||
lambda pid: path if pid == "weather" else None)
|
||||
|
||||
manager.installs_as = installs_as
|
||||
return manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config(api_v3_module):
|
||||
"""config.json with an ``enabled`` flag for each plugin id given."""
|
||||
def sections(enabled):
|
||||
api_v3_module.api_v3.config_manager.load_config.return_value = {
|
||||
plugin_id: {"enabled": flag} for plugin_id, flag in enabled.items()}
|
||||
return sections
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def queued(api_v3_module):
|
||||
queue = MagicMock()
|
||||
|
||||
def enqueue(operation_type, plugin_id, operation_callback=None):
|
||||
queue.callback_result = operation_callback(MagicMock())
|
||||
return "op-1"
|
||||
|
||||
queue.enqueue_operation.side_effect = enqueue
|
||||
api_v3_module.api_v3.operation_queue = queue
|
||||
return queue
|
||||
|
||||
|
||||
def _direct(client):
|
||||
return client.post(INSTALL, json={"plugin_id": "weather"}).get_json()
|
||||
|
||||
|
||||
def _queued(client, queue):
|
||||
client.post(INSTALL, json={"plugin_id": "weather"})
|
||||
return queue.callback_result
|
||||
|
||||
|
||||
class TestDirectInstall:
|
||||
def test_an_aliased_install_enabled_under_its_installed_id_asks_for_a_restart(
|
||||
self, api_v3_client, store, config):
|
||||
store.installs_as("ledmatrix-weather")
|
||||
config({"ledmatrix-weather": True})
|
||||
body = _direct(api_v3_client)
|
||||
assert body["status"] == "success"
|
||||
assert body["restart_required"] is True
|
||||
assert body["restart_message"]
|
||||
|
||||
def test_an_enabled_section_under_the_registry_id_alone_does_not(
|
||||
self, api_v3_client, store, config):
|
||||
"""The display knows the plugin as ledmatrix-weather; nothing runs
|
||||
under a section called weather."""
|
||||
store.installs_as("ledmatrix-weather")
|
||||
config({"weather": True})
|
||||
assert _direct(api_v3_client)["restart_required"] is False
|
||||
|
||||
def test_an_aliased_install_that_is_not_enabled_needs_no_restart(
|
||||
self, api_v3_client, store, config):
|
||||
store.installs_as("ledmatrix-weather")
|
||||
config({"ledmatrix-weather": False})
|
||||
assert _direct(api_v3_client)["restart_required"] is False
|
||||
|
||||
def test_an_install_under_its_own_id_is_unchanged(self, api_v3_client, store, config):
|
||||
store.installs_as("weather")
|
||||
config({"weather": True})
|
||||
assert _direct(api_v3_client)["restart_required"] is True
|
||||
|
||||
def test_an_install_that_cannot_be_found_uses_the_requested_id(
|
||||
self, api_v3_client, store, config):
|
||||
config({"weather": True})
|
||||
assert _direct(api_v3_client)["restart_required"] is True
|
||||
|
||||
|
||||
class TestQueuedInstall:
|
||||
def test_an_aliased_install_enabled_under_its_installed_id_asks_for_a_restart(
|
||||
self, api_v3_client, store, config, queued):
|
||||
store.installs_as("ledmatrix-weather")
|
||||
config({"ledmatrix-weather": True})
|
||||
result = _queued(api_v3_client, queued)
|
||||
assert result["success"] is True
|
||||
assert result["restart_required"] is True
|
||||
assert result["restart_message"]
|
||||
|
||||
def test_an_enabled_section_under_the_registry_id_alone_does_not(
|
||||
self, api_v3_client, store, config, queued):
|
||||
store.installs_as("ledmatrix-weather")
|
||||
config({"weather": True})
|
||||
assert _queued(api_v3_client, queued)["restart_required"] is False
|
||||
@@ -219,7 +219,8 @@ class TestCoordinatorStaticPause:
|
||||
def _plugin(self):
|
||||
plugin = MagicMock()
|
||||
plugin.plugin_id = 'clock'
|
||||
plugin.get_display_duration.return_value = 0
|
||||
# A moment: zero would pause 15 s, as the rotation shows it.
|
||||
plugin.get_display_duration.return_value = 0.01
|
||||
return plugin
|
||||
|
||||
def test_trigger_comes_from_the_pipeline(self):
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""A Vegas static pause lasts as long as the rotation shows the plugin.
|
||||
|
||||
The pause asked the plugin for get_display_duration() and compared the
|
||||
answer with the clock. Several plugins (clock-simple, calendar, countdown)
|
||||
return their display_duration setting as it is in config.json, so one saved
|
||||
as "20" or null -- the raw config editor, a hand edit -- reached that
|
||||
comparison as a string or None. The TypeError went to the pause's broad
|
||||
except, which ended the pause: the plugin flashed up and the scroll went on,
|
||||
at every one of its turns. inf paused until something interrupted it, and
|
||||
NaN, False, 0 or a negative number ended the pause at once.
|
||||
|
||||
The pause now reads the answer the way the rotation does since #739, with
|
||||
the same helper (base_plugin.finite_seconds): a numeric string counts;
|
||||
anything else that is not a finite number, or a raise, gets the rotation's
|
||||
30 s; a number at or below zero gets its 15 s.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
os.environ.setdefault("EMULATOR", "true")
|
||||
|
||||
import pytest
|
||||
|
||||
from src.vegas_mode import coordinator
|
||||
|
||||
NOT_NUMBERS = [None, '', 'twenty', True, False, float('nan'), float('inf'),
|
||||
'inf', '1e400', [20], {'seconds': 20}]
|
||||
NOT_ABOVE_ZERO = [0, -5, '-5', '0']
|
||||
NUMBERS = [('20', 20.0), (' 7.5 ', 7.5), (12, 12.0), (12.5, 12.5)]
|
||||
|
||||
|
||||
class FakeClock:
|
||||
"""time.monotonic/time.sleep for the pause loop: sleeping moves the clock."""
|
||||
|
||||
#: A pause still going after this long never ends (inf did that).
|
||||
LIMIT = 3600.0
|
||||
|
||||
def __init__(self):
|
||||
self.now = 0.0
|
||||
|
||||
def monotonic(self):
|
||||
return self.now
|
||||
|
||||
def sleep(self, seconds):
|
||||
self.now += seconds
|
||||
if self.now > self.LIMIT:
|
||||
raise RuntimeError("the static pause never ended")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clock(monkeypatch):
|
||||
fake = FakeClock()
|
||||
monkeypatch.setattr(coordinator, 'time', fake)
|
||||
return fake
|
||||
|
||||
|
||||
def _plugin(duration, plugin_id='clock-simple'):
|
||||
plugin = MagicMock()
|
||||
plugin.plugin_id = plugin_id
|
||||
plugin.get_display_duration.return_value = duration
|
||||
return plugin
|
||||
|
||||
|
||||
def _coord(*plugins):
|
||||
coord = coordinator.VegasModeCoordinator.__new__(coordinator.VegasModeCoordinator)
|
||||
coord.render_pipeline = MagicMock()
|
||||
coord.render_pipeline.get_scroll_position.return_value = 0
|
||||
coord.display_manager = MagicMock()
|
||||
locks = {plugin.plugin_id: threading.Lock() for plugin in plugins}
|
||||
coord.plugin_manager = SimpleNamespace(get_plugin_lock=locks.__getitem__)
|
||||
coord._state_lock = threading.Lock()
|
||||
coord._static_pause_active = False
|
||||
coord._saved_scroll_position = None
|
||||
coord._should_stop = False
|
||||
coord._live_priority_active = False
|
||||
coord._live_priority_check = None
|
||||
coord._interrupt_check = None
|
||||
coord.stats = {'static_pauses': 0}
|
||||
return coord
|
||||
|
||||
|
||||
def _pause(coord, plugin, clock):
|
||||
"""One static pause: (whether it completed, how long it lasted)."""
|
||||
start = clock.now
|
||||
completed = coord._handle_static_pause(plugin)
|
||||
return completed, clock.now - start
|
||||
|
||||
|
||||
class TestPauseLength:
|
||||
@pytest.mark.parametrize('value, seconds', NUMBERS)
|
||||
def test_numbers_and_numeric_strings_are_used(self, clock, value, seconds):
|
||||
plugin = _plugin(value)
|
||||
completed, lasted = _pause(_coord(plugin), plugin, clock)
|
||||
assert completed is True
|
||||
assert lasted == pytest.approx(seconds, abs=0.15)
|
||||
|
||||
@pytest.mark.parametrize('value', NOT_NUMBERS, ids=repr)
|
||||
def test_anything_but_a_finite_number_pauses_for_30s(self, clock, value):
|
||||
plugin = _plugin(value)
|
||||
completed, lasted = _pause(_coord(plugin), plugin, clock)
|
||||
assert completed is True
|
||||
assert lasted == pytest.approx(30.0, abs=0.15)
|
||||
plugin.display.assert_called_once_with(force_clear=True)
|
||||
|
||||
@pytest.mark.parametrize('value', NOT_ABOVE_ZERO, ids=repr)
|
||||
def test_a_number_not_above_zero_pauses_for_15s(self, clock, value):
|
||||
plugin = _plugin(value)
|
||||
completed, lasted = _pause(_coord(plugin), plugin, clock)
|
||||
assert completed is True
|
||||
assert lasted == pytest.approx(15.0, abs=0.15)
|
||||
|
||||
def test_a_raising_get_display_duration_pauses_for_30s(self, clock):
|
||||
plugin = _plugin(None)
|
||||
plugin.get_display_duration.side_effect = KeyError('display_duration')
|
||||
completed, lasted = _pause(_coord(plugin), plugin, clock)
|
||||
assert completed is True
|
||||
assert lasted == pytest.approx(30.0, abs=0.15)
|
||||
|
||||
def test_a_good_value_after_a_bad_one_is_used(self, clock):
|
||||
plugin = _plugin(None)
|
||||
coord = _coord(plugin)
|
||||
assert _pause(coord, plugin, clock)[1] == pytest.approx(30.0, abs=0.15)
|
||||
plugin.get_display_duration.return_value = 45
|
||||
assert _pause(coord, plugin, clock)[1] == pytest.approx(45.0, abs=0.15)
|
||||
|
||||
def test_the_pause_can_still_be_interrupted(self, clock):
|
||||
plugin = _plugin('twenty')
|
||||
coord = _coord(plugin)
|
||||
coord._interrupt_check = lambda: clock.now >= 5
|
||||
completed, lasted = _pause(coord, plugin, clock)
|
||||
assert completed is False
|
||||
assert lasted == pytest.approx(5.0, abs=0.15)
|
||||
|
||||
|
||||
class TestWarning:
|
||||
def test_logged_once_per_plugin(self, clock, caplog):
|
||||
clock_plugin = _plugin('twenty')
|
||||
calendar = _plugin(None, plugin_id='calendar')
|
||||
coord = _coord(clock_plugin, calendar)
|
||||
with caplog.at_level(logging.WARNING, logger='src.vegas_mode.coordinator'):
|
||||
for _ in range(3):
|
||||
for plugin in (clock_plugin, calendar):
|
||||
coord._handle_static_pause(plugin)
|
||||
warnings = [r.getMessage() for r in caplog.records
|
||||
if 'display duration' in r.getMessage()]
|
||||
assert len(warnings) == 2
|
||||
assert any('clock-simple' in m and "'twenty'" in m for m in warnings)
|
||||
assert any('calendar' in m and 'None' in m for m in warnings)
|
||||
|
||||
|
||||
class TestFiniteSeconds:
|
||||
"""The shared rule: what counts as a number of seconds."""
|
||||
|
||||
@pytest.mark.parametrize('value, seconds', NUMBERS + [(0, 0.0), ('-5', -5.0)])
|
||||
def test_numbers_and_numeric_strings(self, value, seconds):
|
||||
from src.plugin_system.base_plugin import finite_seconds
|
||||
result = finite_seconds(value)
|
||||
assert result == seconds and isinstance(result, float)
|
||||
|
||||
@pytest.mark.parametrize('value', NOT_NUMBERS + [pytest.param(10 ** 400, id='10**400')],
|
||||
ids=repr)
|
||||
def test_anything_else_is_none(self, value):
|
||||
from src.plugin_system.base_plugin import finite_seconds
|
||||
assert finite_seconds(value) is None
|
||||
|
||||
|
||||
def _rotation_seconds(plugin):
|
||||
"""How long the rotation shows ``plugin`` (no dynamic duration, no
|
||||
Rotation & Durations override): the two calls run() makes for a screen.
|
||||
"""
|
||||
from src.display_controller import DisplayController
|
||||
dc = object.__new__(DisplayController)
|
||||
dc.config = {}
|
||||
dc.plugin_modes = {'mode': plugin}
|
||||
return dc._resolve_durations(plugin, 'mode', dc._get_display_duration('mode'), False)[1]
|
||||
|
||||
|
||||
class TestSameAsTheRotation:
|
||||
"""The pause and the rotation share finite_seconds; this pins their
|
||||
fallbacks (30 s, 15 s) to each other too."""
|
||||
|
||||
@pytest.mark.parametrize('value', [value for value, _ in NUMBERS]
|
||||
+ NOT_NUMBERS + NOT_ABOVE_ZERO, ids=repr)
|
||||
def test_the_pause_lasts_as_long_as_the_rotation_shows_it(self, clock, value):
|
||||
plugin = _plugin(value)
|
||||
expected = _rotation_seconds(plugin)
|
||||
assert _pause(_coord(plugin), plugin, clock)[1] == pytest.approx(expected, abs=0.15)
|
||||
|
||||
def test_a_raise_too(self, clock):
|
||||
plugin = _plugin(None)
|
||||
plugin.get_display_duration.side_effect = KeyError('display_duration')
|
||||
expected = _rotation_seconds(plugin)
|
||||
assert _pause(_coord(plugin), plugin, clock)[1] == pytest.approx(expected, abs=0.15)
|
||||
@@ -530,11 +530,13 @@ def install_plugin():
|
||||
)
|
||||
|
||||
branch_msg = f" (branch: {branch})" if branch else ""
|
||||
# plugin_id: the id to enable it by (see _installed_plugin_id).
|
||||
# plugin_id: the id to enable it by, and the id its config
|
||||
# section is under (see _installed_plugin_id).
|
||||
installed_id = _installed_plugin_id(plugin_id)
|
||||
return {'success': True,
|
||||
'message': f'Plugin {plugin_id} installed successfully{branch_msg}',
|
||||
'plugin_id': _installed_plugin_id(plugin_id),
|
||||
**_store_restart_fields('install', _plugin_enabled_in_config(plugin_id))}
|
||||
'plugin_id': installed_id,
|
||||
**_store_restart_fields('install', _plugin_enabled_in_config(installed_id))}
|
||||
else:
|
||||
error_msg = f'Failed to install plugin {plugin_id}'
|
||||
if branch:
|
||||
@@ -588,10 +590,11 @@ def install_plugin():
|
||||
)
|
||||
|
||||
branch_msg = f" (branch: {branch})" if branch else ""
|
||||
installed_id = _installed_plugin_id(plugin_id)
|
||||
return success_response(
|
||||
message=f'Plugin installed successfully{branch_msg}',
|
||||
extra={'plugin_id': _installed_plugin_id(plugin_id),
|
||||
**_store_restart_fields('install', _plugin_enabled_in_config(plugin_id))})
|
||||
extra={'plugin_id': installed_id,
|
||||
**_store_restart_fields('install', _plugin_enabled_in_config(installed_id))})
|
||||
else:
|
||||
error_msg = f'Failed to install plugin {plugin_id}'
|
||||
if branch:
|
||||
|
||||
Reference in New Issue
Block a user