Add webostv 100% tests coverage for media player (#64723)

This commit is contained in:
Shay Levy 2022-01-23 02:06:48 +02:00 committed by GitHub
parent 5654490aa8
commit 12780a3173
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 529 additions and 27 deletions

View File

@ -1297,7 +1297,6 @@ omit =
homeassistant/components/waze_travel_time/helpers.py homeassistant/components/waze_travel_time/helpers.py
homeassistant/components/waze_travel_time/sensor.py homeassistant/components/waze_travel_time/sensor.py
homeassistant/components/webostv/__init__.py homeassistant/components/webostv/__init__.py
homeassistant/components/webostv/media_player.py
homeassistant/components/whois/__init__.py homeassistant/components/whois/__init__.py
homeassistant/components/whois/sensor.py homeassistant/components/whois/sensor.py
homeassistant/components/wiffi/* homeassistant/components/wiffi/*

View File

@ -12,6 +12,17 @@ TV_NAME = "fake"
ENTITY_ID = f"{MP_DOMAIN}.{TV_NAME}" ENTITY_ID = f"{MP_DOMAIN}.{TV_NAME}"
MOCK_CLIENT_KEYS = {"1.2.3.4": "some-secret"} MOCK_CLIENT_KEYS = {"1.2.3.4": "some-secret"}
CHANNEL_1 = {
"channelNumber": "1",
"channelName": "Channel 1",
"channelId": "ch1id",
}
CHANNEL_2 = {
"channelNumber": "20",
"channelName": "Channel Name 2",
"channelId": "ch2id",
}
async def setup_webostv(hass, unique_id="some-unique-id"): async def setup_webostv(hass, unique_id="some-unique-id"):
"""Initialize webostv and media_player for tests.""" """Initialize webostv and media_player for tests."""

View File

@ -1,8 +1,12 @@
"""Common fixtures and objects for the LG webOS integration tests.""" """Common fixtures and objects for the LG webOS integration tests."""
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import pytest import pytest
from homeassistant.components.webostv.const import LIVE_TV_APP_ID
from . import CHANNEL_1, CHANNEL_2
from tests.common import async_mock_service from tests.common import async_mock_service
@ -20,10 +24,34 @@ def client_fixture():
) as mock_client_class: ) as mock_client_class:
client = mock_client_class.return_value client = mock_client_class.return_value
client.hello_info = {"deviceUUID": "some-fake-uuid"} client.hello_info = {"deviceUUID": "some-fake-uuid"}
client.software_info = {"device_id": "00:01:02:03:04:05"} client.software_info = {"major_ver": "major", "minor_ver": "minor"}
client.system_info = {"modelName": "TVFAKE"} client.system_info = {"modelName": "TVFAKE"}
client.client_key = "0123456789" client.client_key = "0123456789"
client.apps = {0: {"title": "Applicaiton01"}} client.apps = {
client.inputs = {0: {"label": "Input01"}, 1: {"label": "Input02"}} LIVE_TV_APP_ID: {
"title": "Live TV",
"id": LIVE_TV_APP_ID,
"largeIcon": "large-icon",
"icon": "icon",
},
}
client.inputs = {
"in1": {"label": "Input01", "id": "in1", "appId": "app0"},
"in2": {"label": "Input02", "id": "in2", "appId": "app1"},
}
client.current_app_id = LIVE_TV_APP_ID
client.channels = [CHANNEL_1, CHANNEL_2]
client.current_channel = CHANNEL_1
client.volume = 37
client.sound_output = "speaker"
client.muted = False
client.is_on = True
async def mock_state_update_callback():
await client.register_state_update_callback.call_args[0][0](client)
client.mock_state_update = AsyncMock(side_effect=mock_state_update_callback)
yield client yield client

View File

@ -1,50 +1,211 @@
"""The tests for the LG webOS media player platform.""" """The tests for the LG webOS media player platform."""
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN import asyncio
from datetime import timedelta
from unittest.mock import Mock
import pytest
from homeassistant.components import automation
from homeassistant.components.media_player import (
DOMAIN as MP_DOMAIN,
MediaPlayerDeviceClass,
)
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_TITLE,
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_VOLUME_MUTED,
MEDIA_TYPE_CHANNEL,
SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOURCE, SERVICE_SELECT_SOURCE,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_SET,
) )
from homeassistant.components.webostv.const import ( from homeassistant.components.webostv.const import (
ATTR_BUTTON, ATTR_BUTTON,
ATTR_PAYLOAD, ATTR_PAYLOAD,
ATTR_SOUND_OUTPUT,
DOMAIN, DOMAIN,
LIVE_TV_APP_ID,
SERVICE_BUTTON, SERVICE_BUTTON,
SERVICE_COMMAND, SERVICE_COMMAND,
SERVICE_SELECT_SOUND_OUTPUT,
WebOsTvCommandError,
) )
from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_VOLUME_MUTE from homeassistant.components.webostv.media_player import (
SUPPORT_WEBOSTV,
SUPPORT_WEBOSTV_VOLUME,
)
from homeassistant.const import (
ATTR_COMMAND,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ENTITY_MATCH_NONE,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PLAY_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_STOP,
SERVICE_TURN_OFF,
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
STATE_OFF,
STATE_ON,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from . import ENTITY_ID, setup_webostv from . import CHANNEL_2, ENTITY_ID, TV_NAME, setup_webostv
from tests.common import async_fire_time_changed
async def test_mute(hass, client): @pytest.mark.parametrize(
"""Test simple service call.""" "service, attr_data, client_call",
[
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True}, ("set_mute", True)),
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False}, ("set_mute", False)),
(SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1.00}, ("set_volume", 100)),
(SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0.54}, ("set_volume", 54)),
(SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0.0}, ("set_volume", 0)),
],
)
async def test_services_with_parameters(hass, client, service, attr_data, client_call):
"""Test services that has parameters in calls."""
await setup_webostv(hass) await setup_webostv(hass)
data = { data = {ATTR_ENTITY_ID: ENTITY_ID, **attr_data}
ATTR_ENTITY_ID: ENTITY_ID, assert await hass.services.async_call(MP_DOMAIN, service, data, True)
ATTR_MEDIA_VOLUME_MUTED: True,
}
assert await hass.services.async_call(MP_DOMAIN, SERVICE_VOLUME_MUTE, data, True) getattr(client, client_call[0]).assert_called_once_with(client_call[1])
await hass.async_block_till_done()
client.set_mute.assert_called_once()
async def test_select_source_with_empty_source_list(hass, client): @pytest.mark.parametrize(
"service, client_call",
[
(SERVICE_TURN_OFF, "power_off"),
(SERVICE_VOLUME_UP, "volume_up"),
(SERVICE_VOLUME_DOWN, "volume_down"),
(SERVICE_MEDIA_PLAY, "play"),
(SERVICE_MEDIA_PAUSE, "pause"),
(SERVICE_MEDIA_STOP, "stop"),
],
)
async def test_services(hass, client, service, client_call):
"""Test simple services without parameters."""
await setup_webostv(hass)
data = {ATTR_ENTITY_ID: ENTITY_ID}
assert await hass.services.async_call(MP_DOMAIN, service, data, True)
getattr(client, client_call).assert_called_once()
async def test_media_play_pause(hass, client):
"""Test media play pause service."""
await setup_webostv(hass)
data = {ATTR_ENTITY_ID: ENTITY_ID}
# After init state is playing - check pause call
assert await hass.services.async_call(
MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data, True
)
client.pause.assert_called_once()
client.play.assert_not_called()
# After pause state is paused - check play call
assert await hass.services.async_call(
MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data, True
)
client.play.assert_called_once()
client.pause.assert_called_once()
@pytest.mark.parametrize(
"service, client_call",
[
(SERVICE_MEDIA_NEXT_TRACK, ("fast_forward", "channel_up")),
(SERVICE_MEDIA_PREVIOUS_TRACK, ("rewind", "channel_down")),
],
)
async def test_media_next_previous_track(
hass, client, service, client_call, monkeypatch
):
"""Test media next/previous track services."""
await setup_webostv(hass)
# check channel up/down for live TV channels
data = {ATTR_ENTITY_ID: ENTITY_ID}
assert await hass.services.async_call(MP_DOMAIN, service, data, True)
getattr(client, client_call[0]).assert_not_called()
getattr(client, client_call[1]).assert_called_once()
# check next/previous for not Live TV channels
monkeypatch.setattr(client, "current_app_id", "in1")
data = {ATTR_ENTITY_ID: ENTITY_ID}
assert await hass.services.async_call(MP_DOMAIN, service, data, True)
getattr(client, client_call[0]).assert_called_once()
getattr(client, client_call[1]).assert_called_once()
async def test_select_source_with_empty_source_list(hass, client, caplog):
"""Ensure we don't call client methods when we don't have sources.""" """Ensure we don't call client methods when we don't have sources."""
await setup_webostv(hass) await setup_webostv(hass)
await client.mock_state_update()
data = { data = {
ATTR_ENTITY_ID: ENTITY_ID, ATTR_ENTITY_ID: ENTITY_ID,
ATTR_INPUT_SOURCE: "nonexistent", ATTR_INPUT_SOURCE: "nonexistent",
} }
await hass.services.async_call(MP_DOMAIN, SERVICE_SELECT_SOURCE, data) assert await hass.services.async_call(MP_DOMAIN, SERVICE_SELECT_SOURCE, data, True)
await hass.async_block_till_done()
client.launch_app.assert_not_called() client.launch_app.assert_not_called()
client.set_input.assert_not_called() client.set_input.assert_not_called()
assert f"Source nonexistent not found for {TV_NAME}" in caplog.text
async def test_select_app_source(hass, client):
"""Test select app source."""
await setup_webostv(hass)
await client.mock_state_update()
data = {
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_INPUT_SOURCE: "Live TV",
}
assert await hass.services.async_call(MP_DOMAIN, SERVICE_SELECT_SOURCE, data, True)
client.launch_app.assert_called_once_with(LIVE_TV_APP_ID)
client.set_input.assert_not_called()
async def test_select_input_source(hass, client):
"""Test select input source."""
await setup_webostv(hass)
await client.mock_state_update()
data = {
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_INPUT_SOURCE: "Input01",
}
assert await hass.services.async_call(MP_DOMAIN, SERVICE_SELECT_SOURCE, data, True)
client.launch_app.assert_not_called()
client.set_input.assert_called_once_with("in1")
async def test_button(hass, client): async def test_button(hass, client):
@ -55,8 +216,7 @@ async def test_button(hass, client):
ATTR_ENTITY_ID: ENTITY_ID, ATTR_ENTITY_ID: ENTITY_ID,
ATTR_BUTTON: "test", ATTR_BUTTON: "test",
} }
await hass.services.async_call(DOMAIN, SERVICE_BUTTON, data) assert await hass.services.async_call(DOMAIN, SERVICE_BUTTON, data, True)
await hass.async_block_till_done()
client.button.assert_called_once() client.button.assert_called_once()
client.button.assert_called_with("test") client.button.assert_called_with("test")
@ -70,8 +230,7 @@ async def test_command(hass, client):
ATTR_ENTITY_ID: ENTITY_ID, ATTR_ENTITY_ID: ENTITY_ID,
ATTR_COMMAND: "test", ATTR_COMMAND: "test",
} }
await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data) assert await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data, True)
await hass.async_block_till_done()
client.request.assert_called_with("test", payload=None) client.request.assert_called_with("test", payload=None)
@ -85,9 +244,314 @@ async def test_command_with_optional_arg(hass, client):
ATTR_COMMAND: "test", ATTR_COMMAND: "test",
ATTR_PAYLOAD: {"target": "https://www.google.com"}, ATTR_PAYLOAD: {"target": "https://www.google.com"},
} }
await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data) assert await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data, True)
await hass.async_block_till_done()
client.request.assert_called_with( client.request.assert_called_with(
"test", payload={"target": "https://www.google.com"} "test", payload={"target": "https://www.google.com"}
) )
async def test_select_sound_output(hass, client):
"""Test select sound output service."""
await setup_webostv(hass)
data = {
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_SOUND_OUTPUT: "external_speaker",
}
assert await hass.services.async_call(
DOMAIN, SERVICE_SELECT_SOUND_OUTPUT, data, True
)
client.change_sound_output.assert_called_once_with("external_speaker")
async def test_device_info_startup_off(hass, client, monkeypatch):
"""Test device info when device is off at startup."""
monkeypatch.setattr(client, "system_info", None)
monkeypatch.setattr(client, "is_on", False)
entry = await setup_webostv(hass)
await client.mock_state_update()
assert hass.states.get(ENTITY_ID).state == STATE_OFF
device_reg = device_registry.async_get(hass)
device = device_reg.async_get_device({(DOMAIN, entry.unique_id)})
assert device
assert device.identifiers == {(DOMAIN, entry.unique_id)}
assert device.manufacturer == "LG"
assert device.name == TV_NAME
assert device.sw_version is None
assert device.model is None
async def test_entity_attributes(hass, client, monkeypatch):
"""Test entity attributes."""
entry = await setup_webostv(hass)
await client.mock_state_update()
# Attributes when device is on
state = hass.states.get(ENTITY_ID)
attrs = state.attributes
assert state.state == STATE_ON
assert state.name == TV_NAME
assert attrs[ATTR_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
assert attrs[ATTR_MEDIA_VOLUME_MUTED] is False
assert attrs[ATTR_MEDIA_VOLUME_LEVEL] == 0.37
assert attrs[ATTR_INPUT_SOURCE] == "Live TV"
assert attrs[ATTR_INPUT_SOURCE_LIST] == ["Input01", "Input02", "Live TV"]
assert attrs[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_CHANNEL
assert attrs[ATTR_MEDIA_TITLE] == "Channel 1"
assert attrs[ATTR_SOUND_OUTPUT] == "speaker"
# Volume level not available
monkeypatch.setattr(client, "volume", None)
await client.mock_state_update()
attrs = hass.states.get(ENTITY_ID).attributes
assert attrs.get(ATTR_MEDIA_VOLUME_LEVEL) is None
# Channel change
monkeypatch.setattr(client, "current_channel", CHANNEL_2)
await client.mock_state_update()
attrs = hass.states.get(ENTITY_ID).attributes
assert attrs[ATTR_MEDIA_TITLE] == "Channel Name 2"
# Device Info
device_reg = device_registry.async_get(hass)
device = device_reg.async_get_device({(DOMAIN, entry.unique_id)})
assert device
assert device.identifiers == {(DOMAIN, entry.unique_id)}
assert device.manufacturer == "LG"
assert device.name == TV_NAME
assert device.sw_version == "major.minor"
assert device.model == "TVFAKE"
# Sound output when off
monkeypatch.setattr(client, "sound_output", None)
monkeypatch.setattr(client, "is_on", False)
await client.mock_state_update()
state = hass.states.get(ENTITY_ID)
assert state.state == STATE_OFF
assert state.attributes.get(ATTR_SOUND_OUTPUT) is None
async def test_service_entity_id_none(hass, client):
"""Test service call with none as entity id."""
await setup_webostv(hass)
data = {
ATTR_ENTITY_ID: ENTITY_MATCH_NONE,
ATTR_SOUND_OUTPUT: "external_speaker",
}
assert await hass.services.async_call(
DOMAIN, SERVICE_SELECT_SOUND_OUTPUT, data, True
)
client.change_sound_output.assert_not_called()
@pytest.mark.parametrize(
"media_id, ch_id",
[
("Channel 1", "ch1id"), # Perfect Match by channel name
("Name 2", "ch2id"), # Partial Match by channel name
("20", "ch2id"), # Perfect Match by channel number
],
)
async def test_play_media(hass, client, media_id, ch_id):
"""Test play media service."""
await setup_webostv(hass)
await client.mock_state_update()
data = {
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL,
ATTR_MEDIA_CONTENT_ID: media_id,
}
assert await hass.services.async_call(MP_DOMAIN, SERVICE_PLAY_MEDIA, data, True)
client.set_channel.assert_called_once_with(ch_id)
async def test_update_sources_live_tv_find(hass, client, monkeypatch):
"""Test finding live TV app id in update sources."""
await setup_webostv(hass)
await client.mock_state_update()
# Live TV found in app list
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
assert "Live TV" in sources
assert len(sources) == 3
# Live TV is current app
apps = {
LIVE_TV_APP_ID: {
"title": "Live TV",
"id": "some_id",
},
}
monkeypatch.setattr(client, "apps", apps)
monkeypatch.setattr(client, "current_app_id", "some_id")
await client.mock_state_update()
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
assert "Live TV" in sources
assert len(sources) == 3
# Live TV is is in inputs
inputs = {
LIVE_TV_APP_ID: {
"label": "Live TV",
"id": "some_id",
"appId": LIVE_TV_APP_ID,
},
}
monkeypatch.setattr(client, "inputs", inputs)
await client.mock_state_update()
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
assert "Live TV" in sources
assert len(sources) == 1
# Live TV is current input
inputs = {
LIVE_TV_APP_ID: {
"label": "Live TV",
"id": "some_id",
"appId": "some_id",
},
}
monkeypatch.setattr(client, "inputs", inputs)
await client.mock_state_update()
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
assert "Live TV" in sources
assert len(sources) == 1
# Live TV not found
monkeypatch.setattr(client, "current_app_id", "other_id")
await client.mock_state_update()
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
assert "Live TV" in sources
assert len(sources) == 1
# Live TV not found in sources/apps but is current app
monkeypatch.setattr(client, "apps", {})
monkeypatch.setattr(client, "current_app_id", LIVE_TV_APP_ID)
await client.mock_state_update()
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
assert "Live TV" in sources
assert len(sources) == 1
# Bad update, keep old update
monkeypatch.setattr(client, "inputs", {})
await client.mock_state_update()
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
assert "Live TV" in sources
assert len(sources) == 1
async def test_client_disconnected(hass, client, monkeypatch):
"""Test error not raised when client is disconnected."""
await setup_webostv(hass)
monkeypatch.setattr(client, "is_connected", Mock(return_value=False))
monkeypatch.setattr(client, "connect", Mock(side_effect=asyncio.TimeoutError))
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=20))
await hass.async_block_till_done()
async def test_control_error_handling(hass, client, caplog, monkeypatch):
"""Test control errors handling."""
await setup_webostv(hass)
monkeypatch.setattr(client, "play", Mock(side_effect=WebOsTvCommandError))
data = {ATTR_ENTITY_ID: ENTITY_ID}
# Device on, raise HomeAssistantError
with pytest.raises(HomeAssistantError) as exc:
assert await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True)
assert (
str(exc.value)
== f"Error calling async_media_play on entity {ENTITY_ID}, state:on"
)
assert client.play.call_count == 1
# Device off, log a warning
monkeypatch.setattr(client, "is_on", False)
monkeypatch.setattr(client, "play", Mock(side_effect=asyncio.TimeoutError))
await client.mock_state_update()
assert await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True)
assert client.play.call_count == 1
assert (
f"Error calling async_media_play on entity {ENTITY_ID}, state:off, error: TimeoutError()"
in caplog.text
)
async def test_supported_features(hass, client, monkeypatch):
"""Test test supported features."""
monkeypatch.setattr(client, "sound_output", "lineout")
await setup_webostv(hass)
await client.mock_state_update()
# No sound control support
supported = SUPPORT_WEBOSTV
attrs = hass.states.get(ENTITY_ID).attributes
assert attrs[ATTR_SUPPORTED_FEATURES] == supported
# Support volume mute, step
monkeypatch.setattr(client, "sound_output", "external_speaker")
await client.mock_state_update()
supported = supported | SUPPORT_WEBOSTV_VOLUME
attrs = hass.states.get(ENTITY_ID).attributes
assert attrs[ATTR_SUPPORTED_FEATURES] == supported
# Support volume mute, step, set
monkeypatch.setattr(client, "sound_output", "speaker")
await client.mock_state_update()
supported = supported | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET
attrs = hass.states.get(ENTITY_ID).attributes
assert attrs[ATTR_SUPPORTED_FEATURES] == supported
# Support turn on
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "webostv.turn_on",
"entity_id": ENTITY_ID,
},
"action": {
"service": "test.automation",
"data_template": {
"some": ENTITY_ID,
"id": "{{ trigger.id }}",
},
},
},
],
},
)
supported |= SUPPORT_TURN_ON
await client.mock_state_update()
attrs = hass.states.get(ENTITY_ID).attributes
assert attrs[ATTR_SUPPORTED_FEATURES] == supported