Rewrite dyson init test (#45409)

This commit is contained in:
Xiaonan Shen 2021-01-22 23:27:43 +08:00 committed by GitHub
parent 4aceb0dd27
commit 7e8d0a263c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 163 additions and 258 deletions

View File

@ -4,16 +4,20 @@ from typing import Optional, Type
from unittest import mock from unittest import mock
from unittest.mock import MagicMock from unittest.mock import MagicMock
from libpurecool.const import SLEEP_TIMER_OFF, Dyson360EyeMode, FanMode, PowerMode
from libpurecool.dyson_360_eye import Dyson360Eye
from libpurecool.dyson_device import DysonDevice from libpurecool.dyson_device import DysonDevice
from libpurecool.dyson_pure_cool import FanSpeed from libpurecool.dyson_pure_cool import DysonPureCool, FanSpeed
from libpurecool.dyson_pure_cool_link import DysonPureCoolLink
from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
SERIAL = "XX-XXXXX-XX" SERIAL = "XX-XXXXX-XX"
NAME = "Temp Name" NAME = "Temp Name"
ENTITY_NAME = "temp_name" ENTITY_NAME = "temp_name"
IP_ADDRESS = "0.0.0.0"
BASE_PATH = "homeassistant.components.dyson" BASE_PATH = "homeassistant.components.dyson"
@ -25,7 +29,7 @@ CONFIG = {
CONF_DEVICES: [ CONF_DEVICES: [
{ {
"device_id": SERIAL, "device_id": SERIAL,
"device_ip": "0.0.0.0", "device_ip": IP_ADDRESS,
} }
], ],
} }
@ -61,6 +65,46 @@ def get_basic_device(spec: Type[DysonDevice]) -> DysonDevice:
return device return device
@callback
def async_get_360eye_device(state=Dyson360EyeMode.FULL_CLEAN_RUNNING) -> Dyson360Eye:
"""Return a Dyson 360 Eye device."""
device = get_basic_device(Dyson360Eye)
device.state.state = state
device.state.battery_level = 85
device.state.power_mode = PowerMode.QUIET
device.state.position = (0, 0)
return device
@callback
def async_get_purecoollink_device() -> DysonPureCoolLink:
"""Return a Dyson Pure Cool Link device."""
device = get_basic_device(DysonPureCoolLink)
device.state.fan_mode = FanMode.FAN.value
device.state.speed = FanSpeed.FAN_SPEED_1.value
device.state.night_mode = "ON"
device.state.oscillation = "ON"
return device
@callback
def async_get_purecool_device() -> DysonPureCool:
"""Return a Dyson Pure Cool device."""
device = get_basic_device(DysonPureCool)
device.state.fan_power = "ON"
device.state.speed = FanSpeed.FAN_SPEED_1.value
device.state.night_mode = "ON"
device.state.oscillation = "OION"
device.state.oscillation_angle_low = "0024"
device.state.oscillation_angle_high = "0254"
device.state.auto_mode = "OFF"
device.state.front_direction = "ON"
device.state.sleep_timer = SLEEP_TIMER_OFF
device.state.hepa_filter_state = "0100"
device.state.carbon_filter_state = "0100"
return device
async def async_update_device( async def async_update_device(
hass: HomeAssistant, device: DysonDevice, state_type: Optional[Type] = None hass: HomeAssistant, device: DysonDevice, state_type: Optional[Type] = None
) -> None: ) -> None:
@ -70,8 +114,8 @@ async def async_update_device(
# Combining sync calls to avoid multiple executors # Combining sync calls to avoid multiple executors
def _run_callbacks(): def _run_callbacks():
for callback in callbacks: for callback_fn in callbacks:
callback(message) callback_fn(message)
await hass.async_add_executor_job(_run_callbacks) await hass.async_add_executor_job(_run_callbacks)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -16,7 +16,7 @@ from tests.common import async_setup_component
async def device(hass: HomeAssistant, request) -> DysonDevice: async def device(hass: HomeAssistant, request) -> DysonDevice:
"""Fixture to provide Dyson 360 Eye device.""" """Fixture to provide Dyson 360 Eye device."""
platform = request.module.PLATFORM_DOMAIN platform = request.module.PLATFORM_DOMAIN
get_device = request.module.get_device get_device = request.module.async_get_device
if hasattr(request, "param"): if hasattr(request, "param"):
if isinstance(request.param, list): if isinstance(request.param, list):
device = get_device(*request.param) device = get_device(*request.param)

View File

@ -1,7 +1,7 @@
"""Test the Dyson fan component.""" """Test the Dyson fan component."""
from typing import Type from typing import Type
from libpurecool.const import SLEEP_TIMER_OFF, FanMode, FanSpeed, NightMode, Oscillation from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation
from libpurecool.dyson_pure_cool import DysonPureCool, DysonPureCoolLink from libpurecool.dyson_pure_cool import DysonPureCool, DysonPureCoolLink
from libpurecool.dyson_pure_state import DysonPureCoolState from libpurecool.dyson_pure_state import DysonPureCoolState
from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State
@ -50,33 +50,24 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
from .common import ENTITY_NAME, NAME, SERIAL, async_update_device, get_basic_device from .common import (
ENTITY_NAME,
NAME,
SERIAL,
async_get_purecool_device,
async_get_purecoollink_device,
async_update_device,
)
ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}"
@callback @callback
def get_device(spec: Type[DysonPureCoolLink]) -> DysonPureCoolLink: def async_get_device(spec: Type[DysonPureCoolLink]) -> DysonPureCoolLink:
"""Return a Dyson fan device.""" """Return a Dyson fan device."""
device = get_basic_device(spec)
if spec == DysonPureCoolLink: if spec == DysonPureCoolLink:
device.state.fan_mode = FanMode.FAN.value return async_get_purecoollink_device()
device.state.speed = FanSpeed.FAN_SPEED_1.value return async_get_purecool_device()
device.state.night_mode = "ON"
device.state.oscillation = "ON"
else: # DysonPureCool
device.state.fan_power = "ON"
device.state.speed = FanSpeed.FAN_SPEED_1.value
device.state.night_mode = "ON"
device.state.oscillation = "OION"
device.state.oscillation_angle_low = "0024"
device.state.oscillation_angle_high = "0254"
device.state.auto_mode = "OFF"
device.state.front_direction = "ON"
device.state.sleep_timer = SLEEP_TIMER_OFF
device.state.hepa_filter_state = "0100"
device.state.carbon_filter_state = "0100"
return device
@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) @pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True)

View File

@ -1,231 +1,100 @@
"""Test the parent Dyson component.""" """Test the parent Dyson component."""
import unittest import copy
from unittest import mock from unittest.mock import MagicMock, patch
from homeassistant.components import dyson from homeassistant.components.dyson import DOMAIN
from homeassistant.const import CONF_DEVICES
from homeassistant.core import HomeAssistant
from .common import load_mock_device from .common import (
BASE_PATH,
CONFIG,
ENTITY_NAME,
IP_ADDRESS,
async_get_360eye_device,
async_get_purecool_device,
async_get_purecoollink_device,
)
from tests.common import get_test_home_assistant from tests.common import async_setup_component
def _get_dyson_account_device_available(): async def test_setup_manual(hass: HomeAssistant):
"""Return a valid device provide by Dyson web services.""" """Test set up the component with manually configured device IPs."""
device = mock.Mock() SERIAL_TEMPLATE = "XX-XXXXX-X{}"
load_mock_device(device)
device.connect = mock.Mock(return_value=True) # device1 works
device.auto_connect = mock.Mock(return_value=True) device1 = async_get_purecoollink_device()
return device device1.serial = SERIAL_TEMPLATE.format(1)
# device2 failed to connect
device2 = async_get_purecool_device()
device2.serial = SERIAL_TEMPLATE.format(2)
device2.connect = MagicMock(return_value=False)
# device3 throws exception during connection
device3 = async_get_360eye_device()
device3.serial = SERIAL_TEMPLATE.format(3)
device3.connect = MagicMock(side_effect=OSError)
# device4 not configured in configuration
device4 = async_get_360eye_device()
device4.serial = SERIAL_TEMPLATE.format(4)
devices = [device1, device2, device3, device4]
config = copy.deepcopy(CONFIG)
config[DOMAIN][CONF_DEVICES] = [
{
"device_id": SERIAL_TEMPLATE.format(i),
"device_ip": IP_ADDRESS,
}
for i in [1, 2, 3, 5] # 1 device missing and 1 device not existed
]
with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True) as login, patch(
f"{BASE_PATH}.DysonAccount.devices", return_value=devices
) as devices_method, patch(
f"{BASE_PATH}.DYSON_PLATFORMS", ["fan", "vacuum"]
): # Patch platforms to get rid of sensors
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
login.assert_called_once_with()
devices_method.assert_called_once_with()
# Only one fan and zero vacuum is set up successfully
assert hass.states.async_entity_ids() == [f"fan.{ENTITY_NAME}"]
device1.connect.assert_called_once_with(IP_ADDRESS)
device2.connect.assert_called_once_with(IP_ADDRESS)
device3.connect.assert_called_once_with(IP_ADDRESS)
device4.connect.assert_not_called()
def _get_dyson_account_device_not_available(): async def test_setup_autoconnect(hass: HomeAssistant):
"""Return an invalid device provide by Dyson web services.""" """Test set up the component with auto connect."""
device = mock.Mock() # device1 works
load_mock_device(device) device1 = async_get_purecoollink_device()
device.connect = mock.Mock(return_value=False)
device.auto_connect = mock.Mock(return_value=False) # device2 failed to auto connect
return device device2 = async_get_purecool_device()
device2.auto_connect = MagicMock(return_value=False)
devices = [device1, device2]
config = copy.deepcopy(CONFIG)
config[DOMAIN].pop(CONF_DEVICES)
with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch(
f"{BASE_PATH}.DysonAccount.devices", return_value=devices
), patch(
f"{BASE_PATH}.DYSON_PLATFORMS", ["fan"]
): # Patch platforms to get rid of sensors
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.async_entity_ids_count() == 1
def _get_dyson_account_device_error(): async def test_login_failed(hass: HomeAssistant):
"""Return an invalid device raising OSError while connecting.""" """Test login failure during setup."""
device = mock.Mock() with patch(f"{BASE_PATH}.DysonAccount.login", return_value=False):
load_mock_device(device) assert not await async_setup_component(hass, DOMAIN, CONFIG)
device.connect = mock.Mock(side_effect=OSError("Network error")) await hass.async_block_till_done()
return device
class DysonTest(unittest.TestCase):
"""Dyson parent component test class."""
def setUp(self): # pylint: disable=invalid-name
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.addCleanup(self.tear_down_cleanup)
def tear_down_cleanup(self):
"""Stop everything that was started."""
self.hass.stop()
@mock.patch("libpurecool.dyson.DysonAccount.login", return_value=False)
def test_dyson_login_failed(self, mocked_login):
"""Test if Dyson connection failed."""
dyson.setup(
self.hass,
{
dyson.DOMAIN: {
dyson.CONF_USERNAME: "email",
dyson.CONF_PASSWORD: "password",
dyson.CONF_LANGUAGE: "FR",
}
},
)
assert mocked_login.call_count == 1
@mock.patch("libpurecool.dyson.DysonAccount.devices", return_value=[])
@mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True)
def test_dyson_login(self, mocked_login, mocked_devices):
"""Test valid connection to dyson web service."""
dyson.setup(
self.hass,
{
dyson.DOMAIN: {
dyson.CONF_USERNAME: "email",
dyson.CONF_PASSWORD: "password",
dyson.CONF_LANGUAGE: "FR",
}
},
)
assert mocked_login.call_count == 1
assert mocked_devices.call_count == 1
assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0
@mock.patch("homeassistant.helpers.discovery.load_platform")
@mock.patch(
"libpurecool.dyson.DysonAccount.devices",
return_value=[_get_dyson_account_device_available()],
)
@mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True)
def test_dyson_custom_conf(self, mocked_login, mocked_devices, mocked_discovery):
"""Test device connection using custom configuration."""
dyson.setup(
self.hass,
{
dyson.DOMAIN: {
dyson.CONF_USERNAME: "email",
dyson.CONF_PASSWORD: "password",
dyson.CONF_LANGUAGE: "FR",
dyson.CONF_DEVICES: [
{"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"}
],
}
},
)
assert mocked_login.call_count == 1
assert mocked_devices.call_count == 1
assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1
assert mocked_discovery.call_count == 5
@mock.patch(
"libpurecool.dyson.DysonAccount.devices",
return_value=[_get_dyson_account_device_not_available()],
)
@mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True)
def test_dyson_custom_conf_device_not_available(self, mocked_login, mocked_devices):
"""Test device connection with an invalid device."""
dyson.setup(
self.hass,
{
dyson.DOMAIN: {
dyson.CONF_USERNAME: "email",
dyson.CONF_PASSWORD: "password",
dyson.CONF_LANGUAGE: "FR",
dyson.CONF_DEVICES: [
{"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"}
],
}
},
)
assert mocked_login.call_count == 1
assert mocked_devices.call_count == 1
assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0
@mock.patch(
"libpurecool.dyson.DysonAccount.devices",
return_value=[_get_dyson_account_device_error()],
)
@mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True)
def test_dyson_custom_conf_device_error(self, mocked_login, mocked_devices):
"""Test device connection with device raising an exception."""
dyson.setup(
self.hass,
{
dyson.DOMAIN: {
dyson.CONF_USERNAME: "email",
dyson.CONF_PASSWORD: "password",
dyson.CONF_LANGUAGE: "FR",
dyson.CONF_DEVICES: [
{"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"}
],
}
},
)
assert mocked_login.call_count == 1
assert mocked_devices.call_count == 1
assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0
@mock.patch("homeassistant.helpers.discovery.load_platform")
@mock.patch(
"libpurecool.dyson.DysonAccount.devices",
return_value=[_get_dyson_account_device_available()],
)
@mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True)
def test_dyson_custom_conf_with_unknown_device(
self, mocked_login, mocked_devices, mocked_discovery
):
"""Test device connection with custom conf and unknown device."""
dyson.setup(
self.hass,
{
dyson.DOMAIN: {
dyson.CONF_USERNAME: "email",
dyson.CONF_PASSWORD: "password",
dyson.CONF_LANGUAGE: "FR",
dyson.CONF_DEVICES: [
{"device_id": "XX-XXXXX-XY", "device_ip": "192.168.0.1"}
],
}
},
)
assert mocked_login.call_count == 1
assert mocked_devices.call_count == 1
assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0
assert mocked_discovery.call_count == 0
@mock.patch("homeassistant.helpers.discovery.load_platform")
@mock.patch(
"libpurecool.dyson.DysonAccount.devices",
return_value=[_get_dyson_account_device_available()],
)
@mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True)
def test_dyson_discovery(self, mocked_login, mocked_devices, mocked_discovery):
"""Test device connection using discovery."""
dyson.setup(
self.hass,
{
dyson.DOMAIN: {
dyson.CONF_USERNAME: "email",
dyson.CONF_PASSWORD: "password",
dyson.CONF_LANGUAGE: "FR",
dyson.CONF_TIMEOUT: 5,
dyson.CONF_RETRY: 2,
}
},
)
assert mocked_login.call_count == 1
assert mocked_devices.call_count == 1
assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1
assert mocked_discovery.call_count == 5
@mock.patch(
"libpurecool.dyson.DysonAccount.devices",
return_value=[_get_dyson_account_device_not_available()],
)
@mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True)
def test_dyson_discovery_device_not_available(self, mocked_login, mocked_devices):
"""Test device connection with discovery and invalid device."""
dyson.setup(
self.hass,
{
dyson.DOMAIN: {
dyson.CONF_USERNAME: "email",
dyson.CONF_PASSWORD: "password",
dyson.CONF_LANGUAGE: "FR",
dyson.CONF_TIMEOUT: 5,
dyson.CONF_RETRY: 2,
}
},
)
assert mocked_login.call_count == 1
assert mocked_devices.call_count == 1
assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0

View File

@ -79,7 +79,7 @@ def _async_assign_values(
@callback @callback
def get_device(spec: Type[DysonPureCoolLink], combi=False) -> DysonPureCoolLink: def async_get_device(spec: Type[DysonPureCoolLink], combi=False) -> DysonPureCoolLink:
"""Return a device of the given type.""" """Return a device of the given type."""
device = get_basic_device(spec) device = get_basic_device(spec)
_async_assign_values(device, combi=combi) _async_assign_values(device, combi=combi)
@ -165,7 +165,7 @@ async def test_temperature(
"""Test the temperature sensor in different units.""" """Test the temperature sensor in different units."""
hass.config.units = unit_system hass.config.units = unit_system
device = get_device(DysonPureCoolLink) device = async_get_device(DysonPureCoolLink)
with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch(
f"{BASE_PATH}.DysonAccount.devices", return_value=[device] f"{BASE_PATH}.DysonAccount.devices", return_value=[device]
), patch(f"{BASE_PATH}.DYSON_PLATFORMS", [PLATFORM_DOMAIN]): ), patch(f"{BASE_PATH}.DYSON_PLATFORMS", [PLATFORM_DOMAIN]):

View File

@ -26,20 +26,21 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
from .common import ENTITY_NAME, NAME, SERIAL, async_update_device, get_basic_device from .common import (
ENTITY_NAME,
NAME,
SERIAL,
async_get_360eye_device,
async_update_device,
)
ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}"
@callback @callback
def get_device(state=Dyson360EyeMode.FULL_CLEAN_RUNNING) -> Dyson360Eye: def async_get_device(state=Dyson360EyeMode.FULL_CLEAN_RUNNING) -> Dyson360Eye:
"""Return a Dyson 360 Eye device.""" """Return a Dyson 360 Eye device."""
device = get_basic_device(Dyson360Eye) return async_get_360eye_device(state)
device.state.state = state
device.state.battery_level = 85
device.state.power_mode = PowerMode.QUIET
device.state.position = (0, 0)
return device
async def test_state(hass: HomeAssistant, device: Dyson360Eye) -> None: async def test_state(hass: HomeAssistant, device: Dyson360Eye) -> None: