mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
A game that goes live takes over within about a second (_check_live_takeover in the frame loops and the dwell sleep, throttled to 1 s, never during on-demand, scheduled-off, live_in_ticker or an already-live screen); an interrupted Vegas iteration switches straight to the game; has_live_content() is asked once per plugin per scan. Goldens: live_priority, vegas. Merged with #712: after an interrupted Vegas iteration the WiFi-notice check runs before the live switch (WiFi outranks live). Adds test/test_run_loop_wifi_and_live.py, pinning that a notice and a game arriving during the same screen (1 Hz, 125 Hz, Vegas) show the notice first, then the game, and neither while scheduled off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
6.7 KiB
Python
158 lines
6.7 KiB
Python
"""A WiFi notice and a live game that both want the panel at once.
|
|
|
|
The two preempt the current screen independently (_wifi_notice_pending and
|
|
_check_live_takeover, both polled from the frame loops and the dwell sleep),
|
|
so these pin down how they combine. The documented priority is follower,
|
|
on-demand, WiFi, live, Vegas, rotation: the notice shows first, then the
|
|
game, with no rotation screen in between, and the scheduled-off panel shows
|
|
neither. Runs the real run() loop on the fake clock of
|
|
test/_run_loop_harness.py. Each trace row is
|
|
[start, mode, duration, exit_reason, frames, force_clear].
|
|
"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("EMULATOR", "true")
|
|
|
|
from test._run_loop_harness import FakePlugin, RunLoopHarness # noqa: E402
|
|
|
|
|
|
def _run(tmp_path, horizon, build):
|
|
harness = RunLoopHarness(tmp_path, horizon=horizon)
|
|
build(harness)
|
|
return harness.run()
|
|
|
|
|
|
def _sports(h, live, **kwargs):
|
|
h.add_plugin(FakePlugin("sports", ["sports_live"], duration=20, live=live,
|
|
live_priority=True, **kwargs))
|
|
|
|
|
|
def _notice_then_game(rows, after, posted, expires):
|
|
"""Check the rows from index `after` on: notice, then game, nothing else.
|
|
|
|
The notice is up within about a second of being posted and stays up
|
|
until it expires (it may be redrawn across pass boundaries, so it can
|
|
span several rows). The game follows it directly.
|
|
"""
|
|
wifi = []
|
|
i = after
|
|
while rows[i][1] == "<wifi>":
|
|
wifi.append(rows[i])
|
|
i += 1
|
|
assert wifi, rows
|
|
assert posted <= wifi[0][0] <= posted + 1.25
|
|
# Continuous: each notice row starts where the one before it ended.
|
|
for prev, cur in zip(wifi, wifi[1:]):
|
|
assert cur[0] == pytest.approx(prev[0] + prev[2])
|
|
assert wifi[-1][0] + wifi[-1][2] >= expires
|
|
game = rows[i]
|
|
assert game[1] == "sports_live"
|
|
assert game[0] == pytest.approx(wifi[-1][0] + wifi[-1][2])
|
|
return i
|
|
|
|
|
|
@pytest.mark.parametrize("live_at, wifi_at", [(10.2, 10.4), (10.4, 10.2)],
|
|
ids=["game-first", "notice-first"])
|
|
def test_both_during_a_static_screen(tmp_path, live_at, wifi_at):
|
|
def build(h):
|
|
h.add_plugin(FakePlugin("clock", ["clock"], duration=30))
|
|
h.add_plugin(FakePlugin("weather", ["weather"], duration=20))
|
|
_sports(h, (live_at, 60))
|
|
h.wifi_message(wifi_at, "Connected to HomeNet", duration=5)
|
|
trace = _run(tmp_path, 90, build)
|
|
rows = trace["screens"]
|
|
|
|
# The 1 Hz loop's next check after both arrive ends clock's screen.
|
|
assert rows[0][1] == "clock" and rows[0][2] <= 11.0
|
|
_notice_then_game(rows, 1, wifi_at, wifi_at + 5)
|
|
# The game is never on the panel before the notice.
|
|
first_game = next(row for row in rows if row[1] == "sports_live")
|
|
first_wifi = next(row for row in rows if row[1] == "<wifi>")
|
|
assert first_wifi[0] < first_game[0]
|
|
# Once the game ends, the rotation resumes at the screen it cut short.
|
|
after_game = next(row for row in rows if row[0] >= 60 and row[1] != "sports_live")
|
|
assert after_game[1] == "clock"
|
|
|
|
|
|
def test_both_at_once_during_a_scrolling_screen(tmp_path):
|
|
def build(h):
|
|
h.add_plugin(FakePlugin("ticker", ["ticker"], duration=30, enable_scrolling=True))
|
|
h.add_plugin(FakePlugin("weather", ["weather"], duration=20))
|
|
_sports(h, (10.0, 60))
|
|
h.wifi_message(10.0, "AP mode on", duration=4)
|
|
rows = _run(tmp_path, 90, build)["screens"]
|
|
|
|
assert rows[0][1] == "ticker" and rows[0][0] + rows[0][2] <= 11.0
|
|
_notice_then_game(rows, 1, 10.0, 14.0)
|
|
assert "weather" not in [row[1] for row in rows if row[0] < 60]
|
|
|
|
|
|
def test_vegas_yields_to_both_with_no_rotation_screen(tmp_path):
|
|
def build(h):
|
|
h.add_plugin(FakePlugin("clock", ["clock"], duration=20))
|
|
_sports(h, (40, 70), content=lambda t, mode: 40 <= t < 70)
|
|
h.enable_vegas(cycle=30)
|
|
h.wifi_message(40, "Connected to HomeNet", duration=3)
|
|
trace = _run(tmp_path, 110, build)
|
|
rows = trace["screens"]
|
|
|
|
yielded = next(i for i, row in enumerate(rows)
|
|
if row[1] == "<vegas>" and row[3] in ("vegas-live", "vegas-interrupt"))
|
|
# Vegas's live check (4 Hz) can see the game before the notice file's
|
|
# 1 Hz stat sees the notice; then the game is up for at most a second
|
|
# before the notice preempts it.
|
|
i = yielded + 1
|
|
if rows[i][1] == "sports_live":
|
|
assert rows[i][2] <= 1.0 and rows[i][3] == "wifi"
|
|
i += 1
|
|
i = _notice_then_game(rows, i, 40, 43)
|
|
# Neither the rotation nor the ticker runs while the game is live.
|
|
during = [row[1] for row in rows[yielded + 1:] if row[0] < 70]
|
|
assert set(during) <= {"sports_live", "<wifi>"}
|
|
assert rows[-1][1] == "<vegas>"
|
|
|
|
|
|
def test_vegas_stopped_for_a_game_shows_a_known_notice_first(tmp_path):
|
|
# The notice is already posted when Vegas stops for the game: each Vegas
|
|
# frame runs its live check before its interrupt check, so the game can
|
|
# be what stops it. Without the interrupt check the notice is only
|
|
# learned after the yield, which pins the order the yield path checks
|
|
# them in: the notice first.
|
|
def build(h):
|
|
h.add_plugin(FakePlugin("clock", ["clock"], duration=20))
|
|
_sports(h, (40, 70), content=lambda t, mode: 40 <= t < 70)
|
|
vegas = h.enable_vegas(cycle=30)
|
|
vegas.set_interrupt_checker(lambda: False)
|
|
h.wifi_message(39.5, "Connected to HomeNet", duration=4)
|
|
rows = _run(tmp_path, 110, build)["screens"]
|
|
|
|
yielded = next(i for i, row in enumerate(rows) if row[3] == "vegas-live")
|
|
assert 40.0 <= rows[yielded][0] + rows[yielded][2] <= 40.3
|
|
_notice_then_game(rows, yielded + 1, 40.0, 43.5)
|
|
|
|
|
|
def test_scheduled_off_shows_neither(tmp_path):
|
|
# The harness clock starts at 22:59:30: off from 23:00 (t=30) to 23:05
|
|
# (t=330). The notice and the game both arrive at t=120, well inside it
|
|
# (whichever way the end minute is counted).
|
|
def build(h):
|
|
h.config["schedule"] = {"enabled": True, "start_time": "23:05", "end_time": "23:00"}
|
|
h.add_plugin(FakePlugin("clock", ["clock"], duration=20))
|
|
h.add_plugin(FakePlugin("weather", ["weather"], duration=20))
|
|
_sports(h, (120, 400))
|
|
h.wifi_message(120, "AP mode on", duration=30)
|
|
trace = _run(tmp_path, 380, build)
|
|
rows = trace["screens"]
|
|
|
|
off = next(i for i, row in enumerate(rows) if row[1] == "<off>")
|
|
assert rows[off][0] <= 90.0
|
|
assert rows[off][0] + rows[off][2] == 330.0 and rows[off][3] == "schedule-on"
|
|
assert not any(row[1] == "<wifi>" for row in rows)
|
|
live_events = [e for e in trace["events"] if e[1] == "live"]
|
|
assert live_events and all(e[0] >= 330.0 for e in live_events)
|
|
# The game, still live when the panel comes back, is what shows.
|
|
assert rows[off + 1][1] == "sports_live"
|