hass-core/tests/components/sonos/conftest.py
J. Nick Koston 9a79320861
Mark executor jobs as background unless created from a tracked task (#114450)
* Mark executor jobs as background unless created from a tracked task

If the current task is not tracked the executor job should not
be a background task to avoid delaying startup and shutdown.

Currently any executor job created in a untracked task or
background task would end up being tracked and delaying
startup/shutdown

* import exec has the same issue

* Avoid tracking import executor jobs

There is no reason to track these jobs as they are always awaited
and we do not want to support fire and forget import executor jobs

* fix xiaomi_miio

* lots of fire time changed without background await

* revert changes moved to other PR

* more

* more

* more

* m

* m

* p

* fix fire and forget tests

* scrape

* sonos

* system

* more

* capture callback before block

* coverage

* more

* more races

* more races

* more

* missed some

* more fixes

* missed some more

* fix

* remove unneeded

* one more race

* two
2024-03-30 00:16:53 -04:00

470 lines
15 KiB
Python

"""Configuration for Sonos tests."""
from copy import copy
from ipaddress import ip_address
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from soco import SoCo
from homeassistant.components import ssdp, zeroconf
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.sonos import DOMAIN
from homeassistant.const import CONF_HOSTS
from tests.common import MockConfigEntry, load_fixture
class SonosMockEventListener:
"""Mock the event listener."""
def __init__(self, ip_address: str) -> None:
"""Initialize the mock event listener."""
self.address = [ip_address, "8080"]
class SonosMockSubscribe:
"""Mock the subscription."""
def __init__(self, ip_address: str, *args, **kwargs) -> None:
"""Initialize the mock subscriber."""
self.event_listener = SonosMockEventListener(ip_address)
self.service = Mock()
async def unsubscribe(self) -> None:
"""Unsubscribe mock."""
class SonosMockService:
"""Mock a Sonos Service used in callbacks."""
def __init__(self, service_type, ip_address="192.168.42.2") -> None:
"""Initialize the instance."""
self.service_type = service_type
self.subscribe = AsyncMock(return_value=SonosMockSubscribe(ip_address))
class SonosMockEvent:
"""Mock a sonos Event used in callbacks."""
def __init__(self, soco, service, variables):
"""Initialize the instance."""
self.sid = f"{soco.uid}_sub0000000001"
self.seq = "0"
self.timestamp = 1621000000.0
self.service = service
self.variables = variables
def increment_variable(self, var_name):
"""Increment the value of the var_name key in variables dict attribute.
Assumes value has a format of <str>:<int>.
"""
self.variables = copy(self.variables)
base, count = self.variables[var_name].split(":")
newcount = int(count) + 1
self.variables[var_name] = ":".join([base, str(newcount)])
return self.variables[var_name]
@pytest.fixture
def zeroconf_payload():
"""Return a default zeroconf payload."""
return zeroconf.ZeroconfServiceInfo(
ip_address=ip_address("192.168.4.2"),
ip_addresses=[ip_address("192.168.4.2")],
hostname="Sonos-aaa",
name="Sonos-aaa@Living Room._sonos._tcp.local.",
port=None,
properties={"bootseq": "1234"},
type="mock_type",
)
@pytest.fixture
async def async_autosetup_sonos(async_setup_sonos):
"""Set up a Sonos integration instance on test run."""
await async_setup_sonos()
@pytest.fixture
def async_setup_sonos(hass, config_entry, fire_zgs_event):
"""Return a coroutine to set up a Sonos integration instance on demand."""
async def _wrapper():
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
await fire_zgs_event()
await hass.async_block_till_done(wait_background_tasks=True)
return _wrapper
@pytest.fixture(name="config_entry")
def config_entry_fixture():
"""Create a mock Sonos config entry."""
return MockConfigEntry(domain=DOMAIN, title="Sonos")
class MockSoCo(MagicMock):
"""Mock the Soco Object."""
uid = "RINCON_test"
play_mode = "NORMAL"
mute = False
night_mode = True
dialog_level = True
loudness = True
volume = 19
audio_delay = 2
balance = (61, 100)
bass = 1
treble = -1
mic_enabled = False
sub_crossover = None # Default to None for non-Amp devices
sub_enabled = False
sub_gain = 5
surround_enabled = True
surround_mode = True
surround_level = 3
music_surround_level = 4
soundbar_audio_input_format = "Dolby 5.1"
@property
def visible_zones(self):
"""Return visible zones and allow property to be overridden by device classes."""
return {self}
class SoCoMockFactory:
"""Factory for creating SoCo Mocks."""
def __init__(
self,
music_library,
speaker_info,
current_track_info_empty,
battery_info,
alarm_clock,
) -> None:
"""Initialize the mock factory."""
self.mock_list: dict[str, MockSoCo] = {}
self.music_library = music_library
self.speaker_info = speaker_info
self.current_track_info = current_track_info_empty
self.battery_info = battery_info
self.alarm_clock = alarm_clock
def cache_mock(
self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A"
) -> MockSoCo:
"""Put a user created mock into the cache."""
mock_soco.mock_add_spec(SoCo)
mock_soco.ip_address = ip_address
if ip_address != "192.168.42.2":
mock_soco.uid += f"_{ip_address}"
mock_soco.music_library = self.music_library
mock_soco.get_current_track_info.return_value = self.current_track_info
mock_soco.music_source_from_uri = SoCo.music_source_from_uri
my_speaker_info = self.speaker_info.copy()
my_speaker_info["zone_name"] = name
my_speaker_info["uid"] = mock_soco.uid
mock_soco.get_speaker_info = Mock(return_value=my_speaker_info)
mock_soco.avTransport = SonosMockService("AVTransport", ip_address)
mock_soco.renderingControl = SonosMockService("RenderingControl", ip_address)
mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology", ip_address)
mock_soco.contentDirectory = SonosMockService("ContentDirectory", ip_address)
mock_soco.deviceProperties = SonosMockService("DeviceProperties", ip_address)
mock_soco.alarmClock = self.alarm_clock
mock_soco.get_battery_info.return_value = self.battery_info
mock_soco.all_zones = {mock_soco}
mock_soco.group.coordinator = mock_soco
self.mock_list[ip_address] = mock_soco
return mock_soco
def get_mock(self, *args) -> SoCo:
"""Return a mock."""
if len(args) > 0:
ip_address = args[0]
else:
ip_address = "192.168.42.2"
if ip_address in self.mock_list:
return self.mock_list[ip_address]
mock_soco = MockSoCo(name=f"Soco Mock {ip_address}")
self.cache_mock(mock_soco, ip_address)
return mock_soco
def patch_gethostbyname(host: str) -> str:
"""Mock to return host name as ip address for testing."""
return host
@pytest.fixture(name="soco_factory")
def soco_factory(
music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock
):
"""Create factory for instantiating SoCo mocks."""
factory = SoCoMockFactory(
music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock
)
with (
patch("homeassistant.components.sonos.SoCo", new=factory.get_mock),
patch("socket.gethostbyname", side_effect=patch_gethostbyname),
patch("homeassistant.components.sonos.ZGS_SUBSCRIPTION_TIMEOUT", 0),
):
yield factory
@pytest.fixture(name="soco")
def soco_fixture(soco_factory):
"""Create a default mock soco SoCo fixture."""
return soco_factory.get_mock()
@pytest.fixture(autouse=True)
async def silent_ssdp_scanner(hass):
"""Start SSDP component and get Scanner, prevent actual SSDP traffic."""
with (
patch("homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"),
patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"),
patch("homeassistant.components.ssdp.Scanner.async_scan"),
patch(
"homeassistant.components.ssdp.Server._async_start_upnp_servers",
),
patch(
"homeassistant.components.ssdp.Server._async_stop_upnp_servers",
),
):
yield
@pytest.fixture(name="discover", autouse=True)
def discover_fixture(soco):
"""Create a mock soco discover fixture."""
def do_callback(hass, callback, *args, **kwargs):
callback(
ssdp.SsdpServiceInfo(
ssdp_location=f"http://{soco.ip_address}/",
ssdp_st="urn:schemas-upnp-org:device:ZonePlayer:1",
ssdp_usn=f"uuid:{soco.uid}_MR::urn:schemas-upnp-org:service:GroupRenderingControl:1",
upnp={
ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}",
},
),
ssdp.SsdpChange.ALIVE,
)
return MagicMock()
with patch(
"homeassistant.components.ssdp.async_register_callback", side_effect=do_callback
) as mock:
yield mock
@pytest.fixture(name="config")
def config_fixture():
"""Create hass config fixture."""
return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.2"]}}}
@pytest.fixture(name="music_library")
def music_library_fixture():
"""Create music_library fixture."""
music_library = MagicMock()
music_library.get_sonos_favorites.return_value.update_id = 1
return music_library
@pytest.fixture(name="alarm_clock")
def alarm_clock_fixture():
"""Create alarmClock fixture."""
alarm_clock = SonosMockService("AlarmClock")
alarm_clock.ListAlarms = Mock()
alarm_clock.ListAlarms.return_value = {
"CurrentAlarmListVersion": "RINCON_test:14",
"CurrentAlarmList": "<Alarms>"
'<Alarm ID="14" StartTime="07:00:00" Duration="02:00:00" Recurrence="DAILY" '
'Enabled="1" RoomUUID="RINCON_test" ProgramURI="x-rincon-buzzer:0" '
'ProgramMetaData="" PlayMode="SHUFFLE_NOREPEAT" Volume="25" '
'IncludeLinkedZones="0"/>'
"</Alarms>",
}
return alarm_clock
@pytest.fixture(name="alarm_clock_extended")
def alarm_clock_fixture_extended():
"""Create alarmClock fixture."""
alarm_clock = SonosMockService("AlarmClock")
alarm_clock.ListAlarms = Mock()
alarm_clock.ListAlarms.return_value = {
"CurrentAlarmListVersion": "RINCON_test:15",
"CurrentAlarmList": "<Alarms>"
'<Alarm ID="14" StartTime="07:00:00" Duration="02:00:00" Recurrence="DAILY" '
'Enabled="1" RoomUUID="RINCON_test" ProgramURI="x-rincon-buzzer:0" '
'ProgramMetaData="" PlayMode="SHUFFLE_NOREPEAT" Volume="25" '
'IncludeLinkedZones="0"/>'
'<Alarm ID="15" StartTime="07:00:00" Duration="02:00:00" '
'Recurrence="DAILY" Enabled="1" RoomUUID="RINCON_test" '
'ProgramURI="x-rincon-buzzer:0" ProgramMetaData="" PlayMode="SHUFFLE_NOREPEAT" '
'Volume="25" IncludeLinkedZones="0"/>'
"</Alarms>",
}
return alarm_clock
@pytest.fixture(name="speaker_info")
def speaker_info_fixture():
"""Create speaker_info fixture."""
return {
"zone_name": "Zone A",
"uid": "RINCON_test",
"model_name": "Model Name",
"model_number": "S12",
"hardware_version": "1.20.1.6-1.1",
"software_version": "49.2-64250",
"mac_address": "00-11-22-33-44-55",
"display_version": "13.1",
}
@pytest.fixture(name="current_track_info_empty")
def current_track_info_empty_fixture():
"""Create current_track_info_empty fixture."""
return {
"title": "",
"artist": "",
"album": "",
"album_art": "",
"position": "NOT_IMPLEMENTED",
"playlist_position": "1",
"duration": "NOT_IMPLEMENTED",
"uri": "",
"metadata": "NOT_IMPLEMENTED",
}
@pytest.fixture(name="battery_info")
def battery_info_fixture():
"""Create battery_info fixture."""
return {
"Health": "GREEN",
"Level": 100,
"Temperature": "NORMAL",
"PowerSource": "SONOS_CHARGING_RING",
}
@pytest.fixture(name="device_properties_event")
def device_properties_event_fixture(soco):
"""Create device_properties_event fixture."""
variables = {
"zone_name": "Zone A",
"mic_enabled": "1",
"more_info": "BattChg:NOT_CHARGING,RawBattPct:100,BattPct:100,BattTmp:25",
}
return SonosMockEvent(soco, soco.deviceProperties, variables)
@pytest.fixture(name="alarm_event")
def alarm_event_fixture(soco):
"""Create alarm_event fixture."""
variables = {
"time_zone": "ffc40a000503000003000502ffc4",
"time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org",
"time_generation": "20000001",
"alarm_list_version": "RINCON_test:1",
"time_format": "INV",
"date_format": "INV",
"daily_index_refresh_time": None,
}
return SonosMockEvent(soco, soco.alarmClock, variables)
@pytest.fixture(name="no_media_event")
def no_media_event_fixture(soco):
"""Create no_media_event_fixture."""
variables = {
"current_crossfade_mode": "0",
"current_play_mode": "NORMAL",
"current_section": "0",
"current_track_meta_data": "",
"current_track_uri": "",
"enqueued_transport_uri": "",
"enqueued_transport_uri_meta_data": "",
"number_of_tracks": "0",
"transport_state": "STOPPED",
}
return SonosMockEvent(soco, soco.avTransport, variables)
@pytest.fixture(name="tv_event")
def tv_event_fixture(soco):
"""Create alarm_event fixture."""
variables = {
"transport_state": "PLAYING",
"current_play_mode": "NORMAL",
"current_crossfade_mode": "0",
"number_of_tracks": "1",
"current_track": "1",
"current_section": "0",
"current_track_uri": f"x-sonos-htastream:{soco.uid}:spdif",
"current_track_duration": "",
"current_track_meta_data": {
"title": " ",
"parent_id": "-1",
"item_id": "-1",
"restricted": True,
"resources": [],
"desc": None,
},
"next_track_uri": "",
"next_track_meta_data": "",
"enqueued_transport_uri": "",
"enqueued_transport_uri_meta_data": "",
"playback_storage_medium": "NETWORK",
"av_transport_uri": f"x-sonos-htastream:{soco.uid}:spdif",
"av_transport_uri_meta_data": {
"title": soco.uid,
"parent_id": "0",
"item_id": "spdif-input",
"restricted": False,
"resources": [],
"desc": None,
},
"current_transport_actions": "Set, Play",
"current_valid_play_modes": "",
}
return SonosMockEvent(soco, soco.avTransport, variables)
@pytest.fixture(autouse=True)
def mock_get_source_ip(mock_get_source_ip):
"""Mock network util's async_get_source_ip in all sonos tests."""
return mock_get_source_ip
@pytest.fixture(name="zgs_discovery", scope="session")
def zgs_discovery_fixture():
"""Load ZoneGroupState discovery payload and return it."""
return load_fixture("sonos/zgs_discovery.xml")
@pytest.fixture(name="fire_zgs_event")
def zgs_event_fixture(hass, soco, zgs_discovery):
"""Create alarm_event fixture."""
variables = {"ZoneGroupState": zgs_discovery}
async def _wrapper():
event = SonosMockEvent(soco, soco.zoneGroupTopology, variables)
subscription = soco.zoneGroupTopology.subscribe.return_value
sub_callback = subscription.callback
sub_callback(event)
await hass.async_block_till_done()
return _wrapper