diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index d9c2a8d0ba6..93e8e3fe8df 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -1,92 +1,81 @@ """Test qwikswitch sensors.""" +import asyncio import logging -from aiohttp.client_exceptions import ClientError -import pytest - -from homeassistant.bootstrap import async_setup_component from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component -from tests.test_util.aiohttp import mock_aiohttp_client +from tests.test_util.aiohttp import MockLongPollSideEffect _LOGGER = logging.getLogger(__name__) -class AiohttpClientMockResponseList(list): - """Return multiple values for aiohttp Mocker. - - aoihttp mocker uses decode to fetch the next value. - """ - - def decode(self, _): - """Return next item from list.""" - try: - res = list.pop(self, 0) - _LOGGER.debug("MockResponseList popped %s: %s", res, self) - if isinstance(res, Exception): - raise res - return res - except IndexError: - raise AssertionError("MockResponseList empty") - - async def wait_till_empty(self, hass): - """Wait until empty.""" - while self: - await hass.async_block_till_done() - await hass.async_block_till_done() +DEVICES = [ + { + "id": "@000001", + "name": "Switch 1", + "type": "rel", + "val": "OFF", + "time": "1522777506", + "rssi": "51%", + }, + { + "id": "@000002", + "name": "Light 2", + "type": "rel", + "val": "ON", + "time": "1522777507", + "rssi": "45%", + }, + { + "id": "@000003", + "name": "Dim 3", + "type": "dim", + "val": "280c00", + "time": "1522777544", + "rssi": "62%", + }, +] -LISTEN = AiohttpClientMockResponseList() - - -@pytest.fixture -def aioclient_mock(): - """HTTP client listen and devices.""" - devices = """[ - {"id":"@000001","name":"Switch 1","type":"rel","val":"OFF", - "time":"1522777506","rssi":"51%"}, - {"id":"@000002","name":"Light 2","type":"rel","val":"ON", - "time":"1522777507","rssi":"45%"}, - {"id":"@000003","name":"Dim 3","type":"dim","val":"280c00", - "time":"1522777544","rssi":"62%"}]""" - - with mock_aiohttp_client() as mock_session: - mock_session.get("http://127.0.0.1:2020/&listen", content=LISTEN) - mock_session.get("http://127.0.0.1:2020/&device", text=devices) - yield mock_session - - -async def test_binary_sensor_device(hass, aioclient_mock): # noqa: F811 +async def test_binary_sensor_device(hass, aioclient_mock): """Test a binary sensor device.""" config = { "qwikswitch": { "sensors": {"name": "s1", "id": "@a00001", "channel": 1, "type": "imod"} } } + aioclient_mock.get("http://127.0.0.1:2020/&device", json=DEVICES) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) await async_setup_component(hass, QWIKSWITCH, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() state_obj = hass.states.get("binary_sensor.s1") assert state_obj.state == "off" - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - - LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}') - LISTEN.append(ClientError()) # Will cause a sleep - + listen_mock.queue_response( + json={"id": "@a00001", "cmd": "", "data": "4e0e1601", "rssi": "61%"} + ) + await asyncio.sleep(0.01) await hass.async_block_till_done() state_obj = hass.states.get("binary_sensor.s1") assert state_obj.state == "on" - LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}') - hass.data[QWIKSWITCH]._sleep_task.cancel() - await LISTEN.wait_till_empty(hass) + listen_mock.queue_response( + json={"id": "@a00001", "cmd": "", "data": "4e0e1701", "rssi": "61%"}, + ) + await asyncio.sleep(0.01) + await hass.async_block_till_done() state_obj = hass.states.get("binary_sensor.s1") assert state_obj.state == "off" + listen_mock.stop() -async def test_sensor_device(hass, aioclient_mock): # noqa: F811 + +async def test_sensor_device(hass, aioclient_mock): """Test a sensor device.""" config = { "qwikswitch": { @@ -98,18 +87,22 @@ async def test_sensor_device(hass, aioclient_mock): # noqa: F811 } } } + aioclient_mock.get("http://127.0.0.1:2020/&device", json=DEVICES) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) await async_setup_component(hass, QWIKSWITCH, config) - + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() + state_obj = hass.states.get("sensor.ss1") assert state_obj.state == "None" - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - - LISTEN.append( - '{"id":"@a00001","name":"ss1","type":"rel",' '"val":"4733800001a00000"}' + listen_mock.queue_response( + json={"id": "@a00001", "name": "ss1", "type": "rel", "val": "4733800001a00000"}, ) - + await asyncio.sleep(0.01) await hass.async_block_till_done() state_obj = hass.states.get("sensor.ss1") assert state_obj.state == "416" + + listen_mock.stop() diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index e23ba5cb9f5..55bfd79143d 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -1,4 +1,5 @@ """Aiohttp test utils.""" +import asyncio from contextlib import contextmanager import json as _json import re @@ -6,7 +7,7 @@ from unittest import mock from urllib.parse import parse_qs from aiohttp import ClientSession -from aiohttp.client_exceptions import ClientResponseError +from aiohttp.client_exceptions import ClientError, ClientResponseError from aiohttp.streams import StreamReader from yarl import URL @@ -48,15 +49,9 @@ class AiohttpClientMocker: headers={}, exc=None, cookies=None, + side_effect=None, ): """Mock a request.""" - if json is not None: - text = _json.dumps(json) - if text is not None: - content = text.encode("utf-8") - if content is None: - content = b"" - if not isinstance(url, RETYPE): url = URL(url) if params: @@ -64,7 +59,16 @@ class AiohttpClientMocker: self._mocks.append( AiohttpClientMockResponse( - method, url, status, content, cookies, exc, headers + method=method, + url=url, + status=status, + response=content, + json=json, + text=text, + cookies=cookies, + exc=exc, + headers=headers, + side_effect=side_effect, ) ) @@ -134,7 +138,8 @@ class AiohttpClientMocker: for response in self._mocks: if response.match_request(method, url, params): self.mock_calls.append((method, url, data, headers)) - + if response.side_effect: + response = await response.side_effect(method, url, data) if response.exc: raise response.exc return response @@ -148,15 +153,32 @@ class AiohttpClientMockResponse: """Mock Aiohttp client response.""" def __init__( - self, method, url, status, response, cookies=None, exc=None, headers=None + self, + method, + url, + status=200, + response=None, + json=None, + text=None, + cookies=None, + exc=None, + headers=None, + side_effect=None, ): """Initialize a fake response.""" + if json is not None: + text = _json.dumps(json) + if text is not None: + response = text.encode("utf-8") + if response is None: + response = b"" + self.method = method self._url = url self.status = status self.response = response self.exc = exc - + self.side_effect = side_effect self._headers = headers or {} self._cookies = {} @@ -270,3 +292,31 @@ def mock_aiohttp_client(): side_effect=create_session, ): yield mocker + + +class MockLongPollSideEffect: + """Imitate a long_poll request. Once created, actual responses are queued and if queue is empty, will await until done.""" + + def __init__(self): + """Initialize the queue.""" + self.semaphore = asyncio.Semaphore(0) + self.response_list = [] + self.stopping = False + + async def __call__(self, method, url, data): + """Fetch the next response from the queue or wait until the queue has items.""" + if self.stopping: + raise ClientError() + await self.semaphore.acquire() + kwargs = self.response_list.pop(0) + return AiohttpClientMockResponse(method=method, url=url, **kwargs) + + def queue_response(self, **kwargs): + """Add a response to the long_poll queue.""" + self.response_list.append(kwargs) + self.semaphore.release() + + def stop(self): + """Stop the current request and future ones. Avoids exception if there is someone waiting when exiting test.""" + self.stopping = True + self.queue_response(exc=ClientError())