| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- """
- Unit tests for board_settings — the host<->board settings sync layer.
- Covers:
- - Still Sands slot conversion (host dicts <-> $Sands/Slots spec string)
- - POSIX timezone derivation from TZif files
- - Adopting board $Sands/* values into host state
- - Autostart mapping to $Playlist/Autostart* commands
- - Playlist mirroring content
- """
- import pytest
- from unittest.mock import MagicMock, patch
- from modules.core import board_settings
- from modules.core.state import state
- class TestSlotConversion:
- """Host slot dicts <-> firmware '$Sands/Slots' spec string."""
- def test_daily_slot_round_trip(self):
- slots = [{"start_time": "22:00", "end_time": "06:00", "days": "daily", "custom_days": []}]
- spec = board_settings.slots_to_board(slots)
- assert spec == "22:00-06:00@daily"
- assert board_settings.board_to_slots(spec) == slots
- def test_weekdays_and_weekends(self):
- slots = [
- {"start_time": "21:00", "end_time": "08:00", "days": "weekdays", "custom_days": []},
- {"start_time": "23:30", "end_time": "09:00", "days": "weekends", "custom_days": []},
- ]
- spec = board_settings.slots_to_board(slots)
- assert spec == "21:00-08:00@weekdays,23:30-09:00@weekends"
- assert board_settings.board_to_slots(spec) == slots
- def test_custom_days_round_trip(self):
- slots = [{
- "start_time": "13:00", "end_time": "14:00", "days": "custom",
- "custom_days": ["monday", "friday"],
- }]
- spec = board_settings.slots_to_board(slots)
- assert spec == "13:00-14:00@mon+fri"
- assert board_settings.board_to_slots(spec) == slots
- def test_custom_days_sorted_in_firmware_order(self):
- slots = [{
- "start_time": "10:00", "end_time": "11:00", "days": "custom",
- "custom_days": ["saturday", "sunday", "wednesday"],
- }]
- # bit 0 = Sunday in the firmware's day order
- assert board_settings.slots_to_board(slots) == "10:00-11:00@sun+wed+sat"
- def test_empty_custom_days_falls_back_to_daily(self):
- slots = [{"start_time": "10:00", "end_time": "11:00", "days": "custom", "custom_days": []}]
- assert board_settings.slots_to_board(slots) == "10:00-11:00@daily"
- def test_parse_ignores_malformed_entries(self):
- parsed = board_settings.board_to_slots("garbage,22:00-06:00@daily,also-bad@")
- assert len(parsed) == 1
- assert parsed[0]["start_time"] == "22:00"
- def test_parse_bare_times_defaults_daily(self):
- parsed = board_settings.board_to_slots("22:00-06:00")
- assert parsed == [{"start_time": "22:00", "end_time": "06:00", "days": "daily", "custom_days": []}]
- def test_empty_specs(self):
- assert board_settings.slots_to_board([]) == ""
- assert board_settings.board_to_slots("") == []
- class TestPosixTz:
- def test_known_iana_zone(self):
- # Skip if the zoneinfo database isn't at the standard path.
- import os
- if not os.path.exists("/usr/share/zoneinfo/America/Toronto"):
- pytest.skip("no system zoneinfo db")
- tz = board_settings.posix_tz("America/Toronto")
- assert tz is not None and tz.startswith("EST5EDT")
- def test_unknown_zone_returns_none(self):
- assert board_settings.posix_tz("Not/AZone") is None
- class TestAdoptStillSands:
- @pytest.fixture(autouse=True)
- def _snapshot_state(self):
- saved = (
- state.scheduled_pause_enabled,
- state.scheduled_pause_time_slots,
- state.scheduled_pause_control_wled,
- state.scheduled_pause_finish_pattern,
- )
- yield
- (
- state.scheduled_pause_enabled,
- state.scheduled_pause_time_slots,
- state.scheduled_pause_control_wled,
- state.scheduled_pause_finish_pattern,
- ) = saved
- def test_adopts_board_values(self):
- state.scheduled_pause_enabled = False
- state.scheduled_pause_time_slots = []
- state.scheduled_pause_control_wled = False
- state.scheduled_pause_finish_pattern = False
- with patch.object(state, "save") as save:
- changed = board_settings.adopt_still_sands({
- "Sands/Enabled": "ON",
- "Sands/Slots": "21:00-08:00@daily",
- "Sands/FinishPattern": "OFF",
- "Sands/LedOff": "ON",
- })
- assert changed is True
- save.assert_called_once()
- assert state.scheduled_pause_enabled is True
- assert state.scheduled_pause_finish_pattern is False
- assert state.scheduled_pause_control_wled is True
- assert state.scheduled_pause_time_slots[0]["start_time"] == "21:00"
- def test_no_change_no_save(self):
- state.scheduled_pause_enabled = True
- state.scheduled_pause_finish_pattern = True
- state.scheduled_pause_control_wled = False
- state.scheduled_pause_time_slots = board_settings.board_to_slots("21:00-08:00@daily")
- with patch.object(state, "save") as save:
- changed = board_settings.adopt_still_sands({
- "Sands/Enabled": "ON",
- "Sands/Slots": "21:00-08:00@daily",
- "Sands/FinishPattern": "ON",
- "Sands/LedOff": "OFF",
- })
- assert changed is False
- save.assert_not_called()
- class TestApplyAutostart:
- def test_maps_fields_to_board_settings(self):
- conn = MagicMock()
- board_settings.apply_autostart({
- "playlist": "evening",
- "run_mode": "loop",
- "shuffle": True,
- "pause_seconds": 90,
- "pause_from_start": False,
- "clear_pattern": "adaptive",
- }, conn=conn)
- written = {call.args[0]: call.args[1] for call in conn.set_setting.call_args_list}
- assert written == {
- "Playlist/Autostart": "evening",
- "Playlist/AutostartMode": "loop",
- "Playlist/AutostartShuffle": "ON",
- "Playlist/AutostartPause": 90,
- "Playlist/AutostartPauseFromStart": "OFF",
- "Playlist/AutostartClear": "adaptive",
- }
- def test_empty_playlist_disables(self):
- conn = MagicMock()
- board_settings.apply_autostart({"playlist": ""}, conn=conn)
- conn.set_setting.assert_called_once_with("Playlist/Autostart", "")
- def test_invalid_clear_mode_coerced_to_none(self):
- conn = MagicMock()
- board_settings.apply_autostart({"clear_pattern": "clear_from_in"}, conn=conn)
- conn.set_setting.assert_called_once_with("Playlist/AutostartClear", "none")
- class TestPlaylistMirroring:
- def test_playlist_content_uses_sd_paths(self):
- from modules.core.pattern_manager import _to_sd_path
- content = board_settings._playlist_sd_content(
- ["./patterns/star.thr", "patterns/sub/wave.thr"], _to_sd_path
- )
- assert content == "/patterns/star.thr\n/patterns/sub/wave.thr\n"
- def test_mirror_uploads_txt(self):
- conn = MagicMock()
- board_settings.mirror_playlist("evening", ["./patterns/star.thr"], conn=conn)
- conn.upload_file.assert_called_once()
- sd_path, data, directory = conn.upload_file.call_args.args
- assert sd_path == "/playlists/evening.txt"
- assert data == b"/patterns/star.thr\n"
- assert directory == "/playlists"
|