/
/
/
1"""
2Tests for ``HueEntertainmentProvider.update_config`` routing.
3
4Brightness, colour mode and latency are applied to the running bridges in
5place; anything else falls through to the base implementation, which reloads
6the provider and therefore drops the entertainment session and the virtual
7players. The changed keys are derived from the real ``Config.update()`` here
8rather than hardcoded, so the test keeps tracking the ``values/`` namespacing
9the models package produces.
10"""
11
12from __future__ import annotations
13
14import logging
15from copy import deepcopy
16from unittest.mock import AsyncMock, MagicMock, patch
17
18import pytest
19from music_assistant_models.config_entries import ConfigValueType, ProviderConfig
20from music_assistant_models.enums import ProviderType
21
22from music_assistant.constants import CONF_LOG_LEVEL, DEFAULT_PROVIDER_CONFIG_ENTRIES
23from music_assistant.models.provider import Provider
24from music_assistant.providers.hue_entertainment.constants import (
25 CONF_BRIGHTNESS,
26 CONF_COLOR_MODE,
27 CONF_HUE_LATENCY_MS,
28)
29from music_assistant.providers.hue_entertainment.provider import HueEntertainmentProvider
30
31
32async def _make_config() -> ProviderConfig:
33 """Build a ProviderConfig holding the provider's own entries plus the log level."""
34 provider = HueEntertainmentProvider.__new__(HueEntertainmentProvider)
35 # the default entries are shared module-level instances; copy before setting values on them
36 entries = [*await provider.get_config_entries(), *deepcopy(DEFAULT_PROVIDER_CONFIG_ENTRIES)]
37 values = {entry.key: entry for entry in entries}
38 for entry in values.values():
39 entry.value = entry.default_value
40 return ProviderConfig(
41 type=ProviderType.PLUGIN,
42 domain="hue_entertainment",
43 instance_id="hue_entertainment--test",
44 values=values,
45 )
46
47
48def _make_provider(config: ProviderConfig) -> tuple[HueEntertainmentProvider, MagicMock]:
49 """Build a provider with a mocked bridge manager, skipping the framework init."""
50 provider = HueEntertainmentProvider.__new__(HueEntertainmentProvider)
51 provider.mass = MagicMock()
52 provider.config = config
53 provider.logger = logging.getLogger("test")
54 bridge_manager = MagicMock()
55 provider._bridge_manager = bridge_manager
56 return provider, bridge_manager
57
58
59async def test_settings_change_applies_in_place() -> None:
60 """A brightness change updates the bridges without reloading the provider."""
61 config = await _make_config()
62 provider, bridge_manager = _make_provider(config)
63 changed_keys = config.update({CONF_BRIGHTNESS: 50})
64
65 # Guards the premise of the routing: the models package namespaces value keys.
66 assert changed_keys == {f"values/{CONF_BRIGHTNESS}"}
67
68 with patch.object(Provider, "update_config", new_callable=AsyncMock) as base_update:
69 await provider.update_config(config, changed_keys)
70
71 bridge_manager.update_settings.assert_called_once()
72 assert bridge_manager.update_settings.call_args.kwargs["brightness"] == 50
73 base_update.assert_not_awaited()
74 assert provider.config is config
75
76
77@pytest.mark.parametrize(
78 ("update", "expected"),
79 [
80 ({CONF_COLOR_MODE: "ambient"}, {"color_mode": "ambient"}),
81 ({CONF_HUE_LATENCY_MS: 120}, {"hue_latency_ms": 120}),
82 (
83 {CONF_BRIGHTNESS: 0, CONF_COLOR_MODE: "ambient", CONF_HUE_LATENCY_MS: 0},
84 {"brightness": 0, "color_mode": "ambient", "hue_latency_ms": 0},
85 ),
86 ],
87 ids=["color_mode", "latency", "all_settings_at_zero"],
88)
89async def test_every_in_place_setting_skips_the_reload(
90 update: dict[str, ConfigValueType], expected: dict[str, ConfigValueType]
91) -> None:
92 """Each hot-applyable setting - and any combination of them - avoids the reload."""
93 config = await _make_config()
94 provider, bridge_manager = _make_provider(config)
95 changed_keys = config.update(update)
96
97 with patch.object(Provider, "update_config", new_callable=AsyncMock) as base_update:
98 await provider.update_config(config, changed_keys)
99
100 bridge_manager.update_settings.assert_called_once()
101 kwargs = bridge_manager.update_settings.call_args.kwargs
102 assert {key: kwargs[key] for key in expected} == expected
103 base_update.assert_not_awaited()
104
105
106async def test_other_change_falls_through_to_reload() -> None:
107 """A log level change is left to the base implementation."""
108 config = await _make_config()
109 provider, bridge_manager = _make_provider(config)
110 changed_keys = config.update({CONF_LOG_LEVEL: "DEBUG"})
111
112 with patch.object(Provider, "update_config", new_callable=AsyncMock) as base_update:
113 await provider.update_config(config, changed_keys)
114
115 bridge_manager.update_settings.assert_not_called()
116 base_update.assert_awaited_once_with(config, changed_keys)
117
118
119async def test_mixed_change_falls_through_to_reload() -> None:
120 """A setting changed alongside another key must not skip the base implementation."""
121 config = await _make_config()
122 provider, bridge_manager = _make_provider(config)
123 changed_keys = config.update({CONF_BRIGHTNESS: 50, CONF_LOG_LEVEL: "DEBUG"})
124
125 with patch.object(Provider, "update_config", new_callable=AsyncMock) as base_update:
126 await provider.update_config(config, changed_keys)
127
128 bridge_manager.update_settings.assert_not_called()
129 base_update.assert_awaited_once_with(config, changed_keys)
130
131
132async def test_settings_change_without_bridge_manager_falls_through() -> None:
133 """Without running bridges there is nothing to update in place."""
134 config = await _make_config()
135 provider, _ = _make_provider(config)
136 provider._bridge_manager = None
137 changed_keys = config.update({CONF_BRIGHTNESS: 50})
138
139 with patch.object(Provider, "update_config", new_callable=AsyncMock) as base_update:
140 await provider.update_config(config, changed_keys)
141
142 base_update.assert_awaited_once_with(config, changed_keys)
143