2023-04-17 17:01:50 +00:00
|
|
|
"""Provide common tests tools for tts."""
|
2024-03-08 13:44:56 +00:00
|
|
|
|
2023-04-17 17:01:50 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-07-01 09:54:42 +00:00
|
|
|
from collections.abc import Generator
|
2023-11-06 20:26:00 +00:00
|
|
|
from http import HTTPStatus
|
2024-06-03 15:43:18 +00:00
|
|
|
from pathlib import Path
|
2023-04-17 17:01:50 +00:00
|
|
|
from typing import Any
|
2023-05-25 09:59:20 +00:00
|
|
|
from unittest.mock import MagicMock, patch
|
2023-04-17 17:01:50 +00:00
|
|
|
|
2023-05-25 09:59:20 +00:00
|
|
|
import pytest
|
2023-04-17 17:01:50 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2023-04-21 02:55:46 +00:00
|
|
|
from homeassistant.components import media_source
|
2023-04-17 17:01:50 +00:00
|
|
|
from homeassistant.components.tts import (
|
|
|
|
CONF_LANG,
|
2023-04-21 02:55:46 +00:00
|
|
|
DOMAIN as TTS_DOMAIN,
|
2024-06-26 12:22:52 +00:00
|
|
|
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
|
2023-04-17 17:01:50 +00:00
|
|
|
Provider,
|
2023-04-21 02:55:46 +00:00
|
|
|
TextToSpeechEntity,
|
2023-04-17 17:01:50 +00:00
|
|
|
TtsAudioType,
|
2023-04-22 00:41:14 +00:00
|
|
|
Voice,
|
2023-05-25 09:59:20 +00:00
|
|
|
_get_cache_files,
|
2023-04-17 17:01:50 +00:00
|
|
|
)
|
2023-04-21 02:55:46 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2023-04-19 11:47:49 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2023-04-21 02:55:46 +00:00
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
2023-04-17 17:01:50 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
2023-04-21 02:55:46 +00:00
|
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
|
|
|
|
from tests.common import (
|
|
|
|
MockConfigEntry,
|
|
|
|
MockModule,
|
|
|
|
MockPlatform,
|
|
|
|
mock_integration,
|
|
|
|
mock_platform,
|
|
|
|
)
|
2023-11-06 20:26:00 +00:00
|
|
|
from tests.typing import ClientSessionGenerator
|
2023-04-17 17:01:50 +00:00
|
|
|
|
2023-04-21 02:55:46 +00:00
|
|
|
DEFAULT_LANG = "en_US"
|
2023-04-20 12:56:50 +00:00
|
|
|
SUPPORT_LANGUAGES = ["de_CH", "de_DE", "en_GB", "en_US"]
|
2023-04-21 02:55:46 +00:00
|
|
|
TEST_DOMAIN = "test"
|
2023-04-17 17:01:50 +00:00
|
|
|
|
|
|
|
|
2024-06-06 15:33:27 +00:00
|
|
|
def mock_tts_get_cache_files_fixture_helper() -> Generator[MagicMock]:
|
2023-05-25 09:59:20 +00:00
|
|
|
"""Mock the list TTS cache function."""
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.tts._get_cache_files", return_value={}
|
|
|
|
) as mock_cache_files:
|
|
|
|
yield mock_cache_files
|
|
|
|
|
|
|
|
|
|
|
|
def mock_tts_init_cache_dir_fixture_helper(
|
|
|
|
init_tts_cache_dir_side_effect: Any,
|
2024-06-06 15:33:27 +00:00
|
|
|
) -> Generator[MagicMock]:
|
2023-05-25 09:59:20 +00:00
|
|
|
"""Mock the TTS cache dir in memory."""
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.tts._init_tts_cache_dir",
|
|
|
|
side_effect=init_tts_cache_dir_side_effect,
|
|
|
|
) as mock_cache_dir:
|
|
|
|
yield mock_cache_dir
|
|
|
|
|
|
|
|
|
|
|
|
def init_tts_cache_dir_side_effect_fixture_helper() -> Any:
|
|
|
|
"""Return the cache dir."""
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def mock_tts_cache_dir_fixture_helper(
|
2024-06-03 15:43:18 +00:00
|
|
|
tmp_path: Path,
|
|
|
|
mock_tts_init_cache_dir: MagicMock,
|
|
|
|
mock_tts_get_cache_files: MagicMock,
|
|
|
|
request: pytest.FixtureRequest,
|
2024-06-06 15:33:27 +00:00
|
|
|
) -> Generator[Path]:
|
2023-05-25 09:59:20 +00:00
|
|
|
"""Mock the TTS cache dir with empty dir."""
|
|
|
|
mock_tts_init_cache_dir.return_value = str(tmp_path)
|
|
|
|
|
|
|
|
# Restore original get cache files behavior, we're working with a real dir.
|
|
|
|
mock_tts_get_cache_files.side_effect = _get_cache_files
|
|
|
|
|
|
|
|
yield tmp_path
|
|
|
|
|
|
|
|
if not hasattr(request.node, "rep_call") or request.node.rep_call.passed:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Print contents of dir if failed
|
|
|
|
print("Content of dir for", request.node.nodeid) # noqa: T201
|
|
|
|
for fil in tmp_path.iterdir():
|
|
|
|
print(fil.relative_to(tmp_path)) # noqa: T201
|
|
|
|
|
|
|
|
# To show the log.
|
|
|
|
pytest.fail("Test failed, see log for details")
|
|
|
|
|
|
|
|
|
2024-06-06 15:33:27 +00:00
|
|
|
def tts_mutagen_mock_fixture_helper() -> Generator[MagicMock]:
|
2023-05-25 09:59:20 +00:00
|
|
|
"""Mock writing tags."""
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.tts.SpeechManager.write_tags",
|
|
|
|
side_effect=lambda *args: args[1],
|
|
|
|
) as mock_write_tags:
|
|
|
|
yield mock_write_tags
|
|
|
|
|
|
|
|
|
2023-04-21 02:55:46 +00:00
|
|
|
async def get_media_source_url(hass: HomeAssistant, media_content_id: str) -> str:
|
|
|
|
"""Get the media source url."""
|
|
|
|
if media_source.DOMAIN not in hass.config.components:
|
|
|
|
assert await async_setup_component(hass, media_source.DOMAIN, {})
|
2023-04-17 17:01:50 +00:00
|
|
|
|
2023-04-21 02:55:46 +00:00
|
|
|
resolved = await media_source.async_resolve_media(hass, media_content_id, None)
|
|
|
|
return resolved.url
|
|
|
|
|
|
|
|
|
2023-11-06 20:26:00 +00:00
|
|
|
async def retrieve_media(
|
|
|
|
hass: HomeAssistant, hass_client: ClientSessionGenerator, media_content_id: str
|
|
|
|
) -> HTTPStatus:
|
|
|
|
"""Get the media source url."""
|
|
|
|
url = await get_media_source_url(hass, media_content_id)
|
|
|
|
|
|
|
|
# Ensure media has been generated by requesting it
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
client = await hass_client()
|
|
|
|
req = await client.get(url)
|
|
|
|
|
|
|
|
return req.status
|
|
|
|
|
|
|
|
|
2023-04-21 02:55:46 +00:00
|
|
|
class BaseProvider:
|
2023-04-17 17:01:50 +00:00
|
|
|
"""Test speech API provider."""
|
|
|
|
|
|
|
|
def __init__(self, lang: str) -> None:
|
|
|
|
"""Initialize test provider."""
|
|
|
|
self._lang = lang
|
2024-08-28 11:48:49 +00:00
|
|
|
self._supported_languages = SUPPORT_LANGUAGES
|
|
|
|
self._supported_options = ["voice", "age"]
|
2023-04-17 17:01:50 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def default_language(self) -> str:
|
|
|
|
"""Return the default language."""
|
|
|
|
return self._lang
|
|
|
|
|
|
|
|
@property
|
|
|
|
def supported_languages(self) -> list[str]:
|
|
|
|
"""Return list of supported languages."""
|
2024-08-28 11:48:49 +00:00
|
|
|
return self._supported_languages
|
2023-04-17 17:01:50 +00:00
|
|
|
|
2023-04-19 11:47:49 +00:00
|
|
|
@callback
|
2023-04-22 00:41:14 +00:00
|
|
|
def async_get_supported_voices(self, language: str) -> list[Voice] | None:
|
2023-04-19 11:47:49 +00:00
|
|
|
"""Return list of supported languages."""
|
|
|
|
if language == "en-US":
|
2023-04-22 00:41:14 +00:00
|
|
|
return [
|
|
|
|
Voice("james_earl_jones", "James Earl Jones"),
|
|
|
|
Voice("fran_drescher", "Fran Drescher"),
|
|
|
|
]
|
2023-04-19 11:47:49 +00:00
|
|
|
return None
|
|
|
|
|
2023-04-17 17:01:50 +00:00
|
|
|
@property
|
|
|
|
def supported_options(self) -> list[str]:
|
|
|
|
"""Return list of supported options like voice, emotions."""
|
2024-08-28 11:48:49 +00:00
|
|
|
return self._supported_options
|
2023-04-17 17:01:50 +00:00
|
|
|
|
|
|
|
def get_tts_audio(
|
2023-05-24 19:02:55 +00:00
|
|
|
self, message: str, language: str, options: dict[str, Any]
|
2023-04-17 17:01:50 +00:00
|
|
|
) -> TtsAudioType:
|
|
|
|
"""Load TTS dat."""
|
|
|
|
return ("mp3", b"")
|
|
|
|
|
|
|
|
|
2024-08-28 11:48:49 +00:00
|
|
|
class MockTTSProvider(BaseProvider, Provider):
|
2023-04-21 02:55:46 +00:00
|
|
|
"""Test speech API provider."""
|
|
|
|
|
|
|
|
def __init__(self, lang: str) -> None:
|
|
|
|
"""Initialize test provider."""
|
|
|
|
super().__init__(lang)
|
|
|
|
self.name = "Test"
|
|
|
|
|
|
|
|
|
|
|
|
class MockTTSEntity(BaseProvider, TextToSpeechEntity):
|
|
|
|
"""Test speech API provider."""
|
|
|
|
|
2024-08-26 13:28:55 +00:00
|
|
|
_attr_name = "Test"
|
2023-04-21 02:55:46 +00:00
|
|
|
|
|
|
|
|
2023-04-17 17:01:50 +00:00
|
|
|
class MockTTS(MockPlatform):
|
|
|
|
"""A mock TTS platform."""
|
|
|
|
|
2024-06-26 12:22:52 +00:00
|
|
|
PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend(
|
2023-04-25 15:54:42 +00:00
|
|
|
{vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)}
|
2023-04-17 17:01:50 +00:00
|
|
|
)
|
|
|
|
|
2024-08-28 11:48:49 +00:00
|
|
|
def __init__(self, provider: MockTTSProvider, **kwargs: Any) -> None:
|
2023-04-17 17:01:50 +00:00
|
|
|
"""Initialize."""
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
self._provider = provider
|
|
|
|
|
|
|
|
async def async_get_engine(
|
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
config: ConfigType,
|
|
|
|
discovery_info: DiscoveryInfoType | None = None,
|
|
|
|
) -> Provider | None:
|
|
|
|
"""Set up a mock speech component."""
|
2023-04-21 02:55:46 +00:00
|
|
|
return self._provider
|
|
|
|
|
|
|
|
|
|
|
|
async def mock_setup(
|
|
|
|
hass: HomeAssistant,
|
2024-08-28 11:48:49 +00:00
|
|
|
mock_provider: MockTTSProvider,
|
2023-04-21 02:55:46 +00:00
|
|
|
) -> None:
|
|
|
|
"""Set up a test provider."""
|
|
|
|
mock_integration(hass, MockModule(domain=TEST_DOMAIN))
|
|
|
|
mock_platform(hass, f"{TEST_DOMAIN}.{TTS_DOMAIN}", MockTTS(mock_provider))
|
|
|
|
|
|
|
|
await async_setup_component(
|
|
|
|
hass, TTS_DOMAIN, {TTS_DOMAIN: {"platform": TEST_DOMAIN}}
|
|
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
|
|
|
|
async def mock_config_entry_setup(
|
2024-08-26 17:39:09 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
tts_entity: MockTTSEntity,
|
|
|
|
test_domain: str = TEST_DOMAIN,
|
2023-04-21 02:55:46 +00:00
|
|
|
) -> MockConfigEntry:
|
|
|
|
"""Set up a test tts platform via config entry."""
|
|
|
|
|
|
|
|
async def async_setup_entry_init(
|
|
|
|
hass: HomeAssistant, config_entry: ConfigEntry
|
|
|
|
) -> bool:
|
|
|
|
"""Set up test config entry."""
|
Ensure config entries are not unloaded while their platforms are setting up (#118767)
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* run with error on to find them
* cert_exp, hold lock
* cert_exp, hold lock
* shelly async_late_forward_entry_setups
* compact
* compact
* found another
* patch up mobileapp
* patch up hue tests
* patch up smartthings
* fix mqtt
* fix esphome
* zwave_js
* mqtt
* rework
* fixes
* fix mocking
* fix mocking
* do not call async_forward_entry_setup directly
* docstrings
* docstrings
* docstrings
* add comments
* doc strings
* fixed all in core, turn off strict
* coverage
* coverage
* missing
* coverage
2024-06-05 01:34:39 +00:00
|
|
|
await hass.config_entries.async_forward_entry_setups(config_entry, [TTS_DOMAIN])
|
2023-04-21 02:55:46 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
async def async_unload_entry_init(
|
|
|
|
hass: HomeAssistant, config_entry: ConfigEntry
|
|
|
|
) -> bool:
|
2023-06-17 01:08:14 +00:00
|
|
|
"""Unload test config entry."""
|
2023-04-21 02:55:46 +00:00
|
|
|
await hass.config_entries.async_forward_entry_unload(config_entry, TTS_DOMAIN)
|
|
|
|
return True
|
|
|
|
|
|
|
|
mock_integration(
|
|
|
|
hass,
|
|
|
|
MockModule(
|
2024-08-26 17:39:09 +00:00
|
|
|
test_domain,
|
2023-04-21 02:55:46 +00:00
|
|
|
async_setup_entry=async_setup_entry_init,
|
|
|
|
async_unload_entry=async_unload_entry_init,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_setup_entry_platform(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
config_entry: ConfigEntry,
|
|
|
|
async_add_entities: AddEntitiesCallback,
|
|
|
|
) -> None:
|
|
|
|
"""Set up test tts platform via config entry."""
|
|
|
|
async_add_entities([tts_entity])
|
|
|
|
|
|
|
|
loaded_platform = MockPlatform(async_setup_entry=async_setup_entry_platform)
|
2024-08-26 17:39:09 +00:00
|
|
|
mock_platform(hass, f"{test_domain}.{TTS_DOMAIN}", loaded_platform)
|
2023-04-21 02:55:46 +00:00
|
|
|
|
2024-08-26 17:39:09 +00:00
|
|
|
config_entry = MockConfigEntry(domain=test_domain)
|
2023-04-21 02:55:46 +00:00
|
|
|
config_entry.add_to_hass(hass)
|
|
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
return config_entry
|