Skip to content

Commit bd84dac

Browse files
authored
Update roborock test typing (home-assistant#157370)
1 parent 42cbeca commit bd84dac

File tree

2 files changed

+107
-60
lines changed

2 files changed

+107
-60
lines changed

tests/components/roborock/conftest.py

Lines changed: 106 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Global fixtures for Roborock integration."""
22

33
import asyncio
4-
from collections.abc import Awaitable, Callable, Generator
4+
from collections.abc import Generator
55
from copy import deepcopy
66
import logging
77
import pathlib
@@ -13,18 +13,34 @@
1313
from roborock import RoborockCategory
1414
from roborock.data import (
1515
CombinedMapInfo,
16+
DnDTimer,
1617
DyadError,
1718
HomeDataDevice,
1819
HomeDataProduct,
1920
NamedRoomMapping,
2021
NetworkInfo,
22+
RoborockBase,
2123
RoborockDyadStateCode,
2224
ZeoError,
2325
ZeoState,
2426
)
2527
from roborock.devices.device import RoborockDevice
26-
from roborock.devices.traits.v1.map_content import MapContent
27-
from roborock.devices.traits.v1.volume import SoundVolume
28+
from roborock.devices.traits.v1 import PropertiesApi
29+
from roborock.devices.traits.v1.clean_summary import CleanSummaryTrait
30+
from roborock.devices.traits.v1.command import CommandTrait
31+
from roborock.devices.traits.v1.common import V1TraitMixin
32+
from roborock.devices.traits.v1.consumeable import ConsumableTrait
33+
from roborock.devices.traits.v1.do_not_disturb import DoNotDisturbTrait
34+
from roborock.devices.traits.v1.dust_collection_mode import DustCollectionModeTrait
35+
from roborock.devices.traits.v1.home import HomeTrait
36+
from roborock.devices.traits.v1.map_content import MapContent, MapContentTrait
37+
from roborock.devices.traits.v1.maps import MapsTrait
38+
from roborock.devices.traits.v1.network_info import NetworkInfoTrait
39+
from roborock.devices.traits.v1.routines import RoutinesTrait
40+
from roborock.devices.traits.v1.smart_wash_params import SmartWashParamsTrait
41+
from roborock.devices.traits.v1.status import StatusTrait
42+
from roborock.devices.traits.v1.volume import SoundVolumeTrait
43+
from roborock.devices.traits.v1.wash_towel_mode import WashTowelModeTrait
2844
from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol
2945

3046
from homeassistant.components.roborock.const import (
@@ -130,71 +146,100 @@ async def get_devices(self) -> list[RoborockDevice]:
130146
return self._devices
131147

132148

133-
def make_fake_switch(obj: Any) -> Any:
134-
"""Update the fake object to emulate the switch trait behavior."""
135-
obj.is_on = True
136-
obj.enable = AsyncMock()
137-
obj.enable.side_effect = lambda: setattr(obj, "is_on", True)
138-
obj.disable = AsyncMock()
139-
obj.disable.side_effect = lambda: setattr(obj, "is_on", False)
140-
obj.refresh = AsyncMock()
141-
return obj
149+
def make_mock_trait(
150+
trait_spec: type[V1TraitMixin] | None = None,
151+
dataclass_template: RoborockBase | None = None,
152+
) -> AsyncMock:
153+
"""Create a mock roborock trait."""
154+
trait = AsyncMock(spec=trait_spec or V1TraitMixin)
155+
if dataclass_template is not None:
156+
# Copy all attributes and property methods (e.g. computed properties)
157+
template_copy = deepcopy(dataclass_template)
158+
for attr_name in dir(template_copy):
159+
if attr_name.startswith("_"):
160+
continue
161+
setattr(trait, attr_name, getattr(template_copy, attr_name))
162+
trait.refresh = AsyncMock()
163+
return trait
164+
165+
166+
def make_mock_switch(
167+
trait_spec: type[V1TraitMixin] | None = None,
168+
dataclass_template: RoborockBase | None = None,
169+
) -> AsyncMock:
170+
"""Create a mock roborock switch trait."""
171+
trait = make_mock_trait(
172+
trait_spec=trait_spec,
173+
dataclass_template=dataclass_template,
174+
)
175+
trait.is_on = True
176+
trait.enable = AsyncMock()
177+
trait.enable.side_effect = lambda: setattr(trait, "is_on", True)
178+
trait.disable = AsyncMock()
179+
trait.disable.side_effect = lambda: setattr(trait, "is_on", False)
180+
return trait
142181

143182

144-
def set_timer_fn(obj: Any) -> Callable[[Any], Awaitable[None]]:
183+
def make_dnd_timer(dataclass_template: RoborockBase) -> AsyncMock:
145184
"""Make a function for the fake timer trait that emulates the real behavior."""
185+
dnd_trait = make_mock_switch(
186+
trait_spec=DoNotDisturbTrait,
187+
dataclass_template=dataclass_template,
188+
)
146189

147-
async def update_timer_attributes(timer: Any) -> None:
148-
setattr(obj, "start_hour", timer.start_hour)
149-
setattr(obj, "start_minute", timer.start_minute)
150-
setattr(obj, "end_hour", timer.end_hour)
151-
setattr(obj, "end_minute", timer.end_minute)
152-
setattr(obj, "enabled", timer.enabled)
190+
async def set_dnd_timer(timer: DnDTimer) -> None:
191+
setattr(dnd_trait, "start_hour", timer.start_hour)
192+
setattr(dnd_trait, "start_minute", timer.start_minute)
193+
setattr(dnd_trait, "end_hour", timer.end_hour)
194+
setattr(dnd_trait, "end_minute", timer.end_minute)
195+
setattr(dnd_trait, "enabled", timer.enabled)
153196

154-
return update_timer_attributes
197+
dnd_trait.set_dnd_timer = AsyncMock()
198+
dnd_trait.set_dnd_timer.side_effect = set_dnd_timer
199+
return dnd_trait
155200

156201

157-
def create_v1_properties(network_info: NetworkInfo) -> Mock:
202+
def create_v1_properties(network_info: NetworkInfo) -> AsyncMock:
158203
"""Create v1 properties for each fake device."""
159-
v1_properties = Mock()
160-
v1_properties.status: Any = deepcopy(STATUS)
161-
v1_properties.status.refresh = AsyncMock()
162-
v1_properties.dnd: Any = make_fake_switch(deepcopy(DND_TIMER))
163-
v1_properties.dnd.set_dnd_timer = AsyncMock()
164-
v1_properties.dnd.set_dnd_timer.side_effect = set_timer_fn(v1_properties.dnd)
165-
v1_properties.clean_summary: Any = deepcopy(CLEAN_SUMMARY)
204+
v1_properties = AsyncMock(spec=PropertiesApi)
205+
v1_properties.status = make_mock_trait(
206+
trait_spec=StatusTrait,
207+
dataclass_template=STATUS,
208+
)
209+
v1_properties.dnd = make_dnd_timer(dataclass_template=DND_TIMER)
210+
v1_properties.clean_summary = make_mock_trait(
211+
trait_spec=CleanSummaryTrait,
212+
dataclass_template=CLEAN_SUMMARY,
213+
)
166214
v1_properties.clean_summary.last_clean_record = deepcopy(CLEAN_RECORD)
167-
v1_properties.clean_summary.refresh = AsyncMock()
168-
v1_properties.consumables = deepcopy(CONSUMABLE)
169-
v1_properties.consumables.refresh = AsyncMock()
215+
v1_properties.consumables = make_mock_trait(
216+
trait_spec=ConsumableTrait, dataclass_template=CONSUMABLE
217+
)
170218
v1_properties.consumables.reset_consumable = AsyncMock()
171-
v1_properties.sound_volume = SoundVolume(volume=50)
219+
v1_properties.sound_volume = make_mock_trait(trait_spec=SoundVolumeTrait)
220+
v1_properties.sound_volume.volume = 50
172221
v1_properties.sound_volume.set_volume = AsyncMock()
173222
v1_properties.sound_volume.set_volume.side_effect = lambda vol: setattr(
174223
v1_properties.sound_volume, "volume", vol
175224
)
176-
v1_properties.sound_volume.refresh = AsyncMock()
177-
v1_properties.command = AsyncMock()
225+
v1_properties.command = AsyncMock(spec=CommandTrait)
178226
v1_properties.command.send = AsyncMock()
179-
v1_properties.maps = AsyncMock()
227+
v1_properties.maps = make_mock_trait(trait_spec=MapsTrait)
180228
v1_properties.maps.current_map = MULTI_MAP_LIST.map_info[1].map_flag
181-
v1_properties.maps.refresh = AsyncMock()
182229
v1_properties.maps.set_current_map = AsyncMock()
183-
v1_properties.map_content = AsyncMock()
230+
v1_properties.map_content = make_mock_trait(trait_spec=MapContentTrait)
184231
v1_properties.map_content.image_content = b"\x89PNG-001"
185232
v1_properties.map_content.map_data = deepcopy(MAP_DATA)
186-
v1_properties.map_content.refresh = AsyncMock()
187-
v1_properties.child_lock = make_fake_switch(AsyncMock())
188-
v1_properties.led_status = make_fake_switch(AsyncMock())
189-
v1_properties.flow_led_status = make_fake_switch(AsyncMock())
190-
v1_properties.valley_electricity_timer = make_fake_switch(AsyncMock())
191-
v1_properties.dust_collection_mode = AsyncMock()
192-
v1_properties.dust_collection_mode.refresh = AsyncMock()
193-
v1_properties.wash_towel_mode = AsyncMock()
194-
v1_properties.wash_towel_mode.refresh = AsyncMock()
195-
v1_properties.smart_wash_params = AsyncMock()
196-
v1_properties.smart_wash_params.refresh = AsyncMock()
197-
v1_properties.home = AsyncMock()
233+
v1_properties.child_lock = make_mock_switch()
234+
v1_properties.led_status = make_mock_switch()
235+
v1_properties.flow_led_status = make_mock_switch()
236+
v1_properties.valley_electricity_timer = make_mock_switch()
237+
v1_properties.dust_collection_mode = make_mock_trait(
238+
trait_spec=DustCollectionModeTrait
239+
)
240+
v1_properties.wash_towel_mode = make_mock_trait(trait_spec=WashTowelModeTrait)
241+
v1_properties.smart_wash_params = make_mock_trait(trait_spec=SmartWashParamsTrait)
242+
v1_properties.home = make_mock_trait(trait_spec=HomeTrait)
198243
home_map_info = {
199244
map_data.map_flag: CombinedMapInfo(
200245
name=map_data.name,
@@ -219,10 +264,11 @@ def create_v1_properties(network_info: NetworkInfo) -> Mock:
219264
v1_properties.home.home_map_info = home_map_info
220265
v1_properties.home.current_map_data = home_map_info[STATUS.current_map]
221266
v1_properties.home.home_map_content = home_map_content
222-
v1_properties.home.refresh = AsyncMock()
223-
v1_properties.network_info = deepcopy(network_info)
224-
v1_properties.network_info.refresh = AsyncMock()
225-
v1_properties.routines = AsyncMock()
267+
v1_properties.network_info = make_mock_trait(
268+
trait_spec=NetworkInfoTrait,
269+
dataclass_template=network_info,
270+
)
271+
v1_properties.routines = make_mock_trait(trait_spec=RoutinesTrait)
226272
v1_properties.routines.get_routines = AsyncMock(return_value=SCENES)
227273
v1_properties.routines.execute_routine = AsyncMock()
228274
# Mock diagnostics for a subset of properties
@@ -267,21 +313,22 @@ def fake_vacuum_fixture(fake_devices: list[FakeDevice]) -> FakeDevice:
267313
return fake_devices[0]
268314

269315

270-
@pytest.fixture(name="send_message_side_effect")
271-
def send_message_side_effect_fixture() -> Any:
316+
@pytest.fixture(name="send_message_exception")
317+
def send_message_exception_fixture() -> Exception | None:
272318
"""Fixture to return a side effect for the send_message method."""
273319
return None
274320

275321

276322
@pytest.fixture(name="vacuum_command", autouse=True)
277323
def fake_vacuum_command_fixture(
278-
fake_vacuum: FakeDevice, send_message_side_effect: Any
279-
) -> Mock:
324+
fake_vacuum: FakeDevice,
325+
send_message_exception: Exception | None,
326+
) -> AsyncMock:
280327
"""Get the fake vacuum device command trait for asserting that commands happened."""
281328
assert fake_vacuum.v1_properties is not None
282329
command_trait = fake_vacuum.v1_properties.command
283-
if send_message_side_effect is not None:
284-
command_trait.send.side_effect = send_message_side_effect
330+
if send_message_exception is not None:
331+
command_trait.send.side_effect = send_message_exception
285332
return command_trait
286333

287334

tests/components/roborock/test_switch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ async def test_update_success(
8686
],
8787
)
8888
@pytest.mark.parametrize(
89-
"send_message_side_effect", [roborock.exceptions.RoborockTimeout]
89+
"send_message_exception", [roborock.exceptions.RoborockTimeout]
9090
)
9191
async def test_update_failed(
9292
hass: HomeAssistant,

0 commit comments

Comments
 (0)