mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
feat(vegas): keep live games in the ticker by default
display.vegas_scroll.live_in_ticker now defaults to true: through a live game the marquee keeps running and the live scoreboard takes extra turns in it -- its cards updating in place while they scroll -- instead of the ticker giving way to the full-screen scoreboard. The new default would reach nobody on its own: every existing config holds an explicit false copied from the template (there was no control for it), and the template merge only adds missing keys. ConfigManager therefore turns a stored false on once, with a backup, and records live_in_ticker_migrated so a false chosen afterwards stays. The marker is never in the template. A "Keep live games in the ticker" checkbox under Vegas mode sets it. Tests that pin the full-screen takeover now say live_in_ticker=false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,20 @@ class TestJsonPartialSaves:
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert saved['config']['display']['vegas_scroll']['live_refresh'] is True
|
||||
|
||||
def test_live_games_can_be_kept_in_the_ticker_or_not(self, api_v3_client, saved):
|
||||
resp = _post_json(api_v3_client, {'vegas_live_in_ticker': False})
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert saved['config']['display']['vegas_scroll']['live_in_ticker'] is False
|
||||
# An unticked box is absent from a submitted form: that is false too.
|
||||
resp = _post_json(api_v3_client, {'__form_section': 'display',
|
||||
'vegas_scroll_speed': '50',
|
||||
'vegas_live_in_ticker': 'on'})
|
||||
assert saved['config']['display']['vegas_scroll']['live_in_ticker'] is True
|
||||
resp = _post_json(api_v3_client, {'__form_section': 'display',
|
||||
'vegas_scroll_speed': '50'})
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert saved['config']['display']['vegas_scroll']['live_in_ticker'] is False
|
||||
|
||||
def test_double_sided_axis_only_keeps_enabled(self, api_v3_client, saved):
|
||||
resp = _post_json(api_v3_client, {'double_sided_axis': 'horizontal'})
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""live_in_ticker's new default reaches existing installs, once (src/config_manager.py).
|
||||
|
||||
3.8.0 makes display.vegas_scroll.live_in_ticker true. Every existing config
|
||||
holds an explicit false copied from the template, which the template merge
|
||||
never touches (it only adds missing keys), so ConfigManager turns that false
|
||||
on once and marks the config. A false chosen after that -- the new checkbox,
|
||||
or by hand -- must stay false.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from src.config_manager import ConfigManager # noqa: E402
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
MARKER = ConfigManager.LIVE_IN_TICKER_MARKER
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def files(tmp_path):
|
||||
template = {"display": {"brightness": 90,
|
||||
"vegas_scroll": {"enabled": False, "live_in_ticker": True}}}
|
||||
paths = {name: tmp_path / f"{name}.json" for name in ("config", "secrets", "template")}
|
||||
paths["template"].write_text(json.dumps(template))
|
||||
paths["secrets"].write_text("{}")
|
||||
return paths
|
||||
|
||||
|
||||
def _load(files, config):
|
||||
files["config"].write_text(json.dumps(config))
|
||||
manager = ConfigManager(config_path=str(files["config"]),
|
||||
secrets_path=str(files["secrets"]))
|
||||
manager.template_path = str(files["template"])
|
||||
manager.load_config()
|
||||
return manager, json.loads(files["config"].read_text())
|
||||
|
||||
|
||||
def _vegas(saved):
|
||||
return saved["display"]["vegas_scroll"]
|
||||
|
||||
|
||||
def test_the_old_default_is_turned_on_once_and_marked(files):
|
||||
old = {"display": {"brightness": 90,
|
||||
"vegas_scroll": {"enabled": True, "live_in_ticker": False}}}
|
||||
manager, saved = _load(files, old)
|
||||
assert _vegas(saved)["live_in_ticker"] is True
|
||||
assert _vegas(saved)[MARKER] is True
|
||||
assert _vegas(manager.config)["live_in_ticker"] is True
|
||||
# The config as it was is kept beside it.
|
||||
backup = json.loads(Path(f"{files['config']}.backup").read_text())
|
||||
assert _vegas(backup)["live_in_ticker"] is False
|
||||
|
||||
|
||||
def test_false_chosen_after_the_migration_stays_false(files):
|
||||
chosen = {"display": {"brightness": 90,
|
||||
"vegas_scroll": {"enabled": True, "live_in_ticker": False,
|
||||
MARKER: True}}}
|
||||
_manager, saved = _load(files, chosen)
|
||||
assert _vegas(saved)["live_in_ticker"] is False
|
||||
|
||||
|
||||
def test_a_marked_config_is_not_written_again(files):
|
||||
marked = {"display": {"brightness": 90,
|
||||
"vegas_scroll": {"enabled": True, "live_in_ticker": True,
|
||||
MARKER: True}}}
|
||||
files["config"].write_text(json.dumps(marked))
|
||||
before = files["config"].stat().st_mtime_ns
|
||||
manager = ConfigManager(config_path=str(files["config"]),
|
||||
secrets_path=str(files["secrets"]))
|
||||
manager.template_path = str(files["template"])
|
||||
manager.load_config()
|
||||
assert files["config"].stat().st_mtime_ns == before
|
||||
assert not Path(f"{files['config']}.backup").exists()
|
||||
|
||||
|
||||
def test_a_config_without_vegas_settings_gets_the_new_default(files):
|
||||
_manager, saved = _load(files, {"display": {"brightness": 90}})
|
||||
assert _vegas(saved)["live_in_ticker"] is True
|
||||
assert _vegas(saved)[MARKER] is True
|
||||
|
||||
|
||||
def test_true_already_is_only_marked(files):
|
||||
on = {"display": {"brightness": 90,
|
||||
"vegas_scroll": {"enabled": True, "live_in_ticker": True}}}
|
||||
_manager, saved = _load(files, on)
|
||||
assert _vegas(saved)["live_in_ticker"] is True and _vegas(saved)[MARKER] is True
|
||||
|
||||
|
||||
def test_a_second_load_keeps_a_false_set_in_between(files):
|
||||
old = {"display": {"brightness": 90,
|
||||
"vegas_scroll": {"enabled": True, "live_in_ticker": False}}}
|
||||
_manager, saved = _load(files, old)
|
||||
_vegas(saved)["live_in_ticker"] = False # the checkbox, unticked
|
||||
_manager, saved = _load(files, saved)
|
||||
assert _vegas(saved)["live_in_ticker"] is False
|
||||
|
||||
|
||||
def test_the_shipped_template_has_the_new_default_and_never_the_marker():
|
||||
template = json.loads((REPO / "config" / "config.template.json").read_text(encoding="utf-8"))
|
||||
vegas = template["display"]["vegas_scroll"]
|
||||
assert vegas["live_in_ticker"] is True
|
||||
assert MARKER not in vegas
|
||||
@@ -67,10 +67,13 @@ class TestConfigWhileStopped:
|
||||
|
||||
|
||||
class TestLivePriorityThrottle:
|
||||
"""The full-screen takeover's live scan: live_in_ticker off, no longer the default."""
|
||||
|
||||
def test_scan_runs_at_most_once_per_interval(self, monkeypatch):
|
||||
now = [1000.0]
|
||||
monkeypatch.setattr(coordinator_module.time, 'monotonic', lambda: now[0])
|
||||
c = _coordinator()
|
||||
c.vegas_config.live_in_ticker = False
|
||||
c._live_priority_check = MagicMock(return_value=None)
|
||||
|
||||
for _ in range(10):
|
||||
@@ -84,6 +87,7 @@ class TestLivePriorityThrottle:
|
||||
def test_live_content_still_pauses_vegas(self, monkeypatch):
|
||||
monkeypatch.setattr(coordinator_module.time, 'monotonic', lambda: 1000.0)
|
||||
c = _coordinator()
|
||||
c.vegas_config.live_in_ticker = False
|
||||
c._live_priority_check = MagicMock(return_value='nfl_live')
|
||||
c.pause = MagicMock()
|
||||
assert c.run_frame() is False
|
||||
|
||||
@@ -58,11 +58,16 @@ def test_an_iteration_does_not_poll_every_plugin_for_its_mode(caplog):
|
||||
|
||||
|
||||
def _live_coordinator(live):
|
||||
"""A coordinator running the real run_frame(), with a switchable live check."""
|
||||
"""A coordinator running the real run_frame(), with a switchable live check.
|
||||
|
||||
live_in_ticker off: these pin the full-screen takeover, which is no longer
|
||||
the default.
|
||||
"""
|
||||
coord = _coordinator({})
|
||||
del coord.run_frame # the real one: it is what refuses frames while paused
|
||||
coord.vegas_config = VegasModeConfig.from_config({'display': {'vegas_scroll': {
|
||||
'enabled': True, 'max_cycle_duration': 0, 'continuous_scroll': True}}})
|
||||
'enabled': True, 'max_cycle_duration': 0, 'continuous_scroll': True,
|
||||
'live_in_ticker': False}}})
|
||||
coord.render_pipeline.has_deferred.return_value = False
|
||||
coord.render_pipeline.needs_extension.return_value = False
|
||||
coord.render_pipeline.render_frame.return_value = True
|
||||
|
||||
@@ -5,10 +5,10 @@ live content did not appear in it at all, because the display controller
|
||||
refused to run the ticker while anything was live. With a dozen plugins
|
||||
enabled that left a live score either absent or minutes stale.
|
||||
|
||||
Two things change, both off by default. `live_in_ticker` keeps the marquee
|
||||
running instead of yielding to a full-screen takeover, and the rotation is
|
||||
expanded by Smooth Weighted Round-Robin so a weighted plugin gets several
|
||||
slots per cycle, spaced through it rather than clumped.
|
||||
Two things change. `live_in_ticker` keeps the marquee running instead of
|
||||
yielding to a full-screen takeover (on by default since 3.8.0), and the
|
||||
rotation is expanded by Smooth Weighted Round-Robin so a weighted plugin gets
|
||||
several slots per cycle, spaced through it rather than clumped.
|
||||
|
||||
Weights are per plugin, not per game: a scoreboard showing four live games
|
||||
still occupies one slot at a time and rotates its own games within it.
|
||||
@@ -288,9 +288,9 @@ class TestTheSchedule:
|
||||
|
||||
|
||||
class TestConfigParsing:
|
||||
def test_defaults_preserve_todays_behaviour(self):
|
||||
def test_live_games_stay_in_the_ticker_by_default(self):
|
||||
cfg = VegasModeConfig.from_config({})
|
||||
assert cfg.live_in_ticker is False
|
||||
assert cfg.live_in_ticker is True
|
||||
assert cfg.live_weight == 3 and cfg.favorite_live_weight == 5
|
||||
|
||||
@pytest.mark.parametrize("given,expected", [(0, 1), (-4, 1), (99, 10), (4, 4)])
|
||||
|
||||
Reference in New Issue
Block a user