From 59a01da0eddd2bbb064211aae20b0107ad1f3736 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 27 Dec 2023 08:48:07 +0100 Subject: [PATCH] Improve cloud tts tests (#106427) --- tests/components/cloud/conftest.py | 5 + tests/components/cloud/test_tts.py | 190 ++++++++++++++++++++++------- 2 files changed, 148 insertions(+), 47 deletions(-) diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 6eaca4906c0..ef8cb037cdb 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -152,6 +152,11 @@ def mock_tts_cache_dir_autouse(mock_tts_cache_dir): return mock_tts_cache_dir +@pytest.fixture(autouse=True) +def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock): + """Mock writing tags.""" + + @pytest.fixture(autouse=True) def mock_user_data(): """Mock os module.""" diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index ba88ae2af2d..dc32747182d 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -1,23 +1,35 @@ """Tests for cloud tts.""" -from unittest.mock import Mock +from collections.abc import Callable, Coroutine +from http import HTTPStatus +from typing import Any +from unittest.mock import AsyncMock, MagicMock -from hass_nabucasa import voice +from hass_nabucasa.voice import MAP_VOICE, VoiceError import pytest import voluptuous as vol -from homeassistant.components.cloud import const, tts +from homeassistant.components.cloud import DOMAIN, const, tts +from homeassistant.components.tts import DOMAIN as TTS_DOMAIN +from homeassistant.components.tts.helper import get_engine_instance +from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.typing import ClientSessionGenerator -@pytest.fixture -def cloud_with_prefs(cloud_prefs): - """Return a cloud mock with prefs.""" - return Mock(client=Mock(prefs=cloud_prefs)) +@pytest.fixture(autouse=True) +async def internal_url_mock(hass: HomeAssistant) -> None: + """Mock internal URL of the instance.""" + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) def test_default_exists() -> None: """Test our default language exists.""" - assert const.DEFAULT_TTS_DEFAULT_VOICE in voice.MAP_VOICE + assert const.DEFAULT_TTS_DEFAULT_VOICE in MAP_VOICE def test_schema() -> None: @@ -42,54 +54,138 @@ def test_schema() -> None: tts.PLATFORM_SCHEMA({"platform": "cloud"}) +@pytest.mark.parametrize( + ("engine_id", "platform_config"), + [ + ( + DOMAIN, + None, + ), + ( + DOMAIN, + { + "platform": DOMAIN, + "service_name": "yaml", + "language": "fr-FR", + "gender": "female", + }, + ), + ], +) async def test_prefs_default_voice( - hass: HomeAssistant, cloud_with_prefs, cloud_prefs + hass: HomeAssistant, + cloud: MagicMock, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + engine_id: str, + platform_config: dict[str, Any] | None, ) -> None: """Test cloud provider uses the preferences.""" - assert cloud_prefs.tts_default_voice == ("en-US", "female") - - tts_info = {"platform_loaded": Mock()} - provider_pref = await tts.async_get_engine( - Mock(data={const.DOMAIN: cloud_with_prefs}), None, tts_info - ) - provider_conf = await tts.async_get_engine( - Mock(data={const.DOMAIN: cloud_with_prefs}), - {"language": "fr-FR", "gender": "female"}, - None, - ) - - assert provider_pref.default_language == "en-US" - assert provider_pref.default_options == {"gender": "female", "audio_output": "mp3"} - assert provider_conf.default_language == "fr-FR" - assert provider_conf.default_options == {"gender": "female", "audio_output": "mp3"} - - await cloud_prefs.async_update(tts_default_voice=("nl-NL", "male")) + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, TTS_DOMAIN, {TTS_DOMAIN: platform_config}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert provider_pref.default_language == "nl-NL" - assert provider_pref.default_options == {"gender": "male", "audio_output": "mp3"} - assert provider_conf.default_language == "fr-FR" - assert provider_conf.default_options == {"gender": "female", "audio_output": "mp3"} + assert cloud.client.prefs.tts_default_voice == ("en-US", "female") + + on_start_callback = cloud.register_on_start.call_args[0][0] + await on_start_callback() + + engine = get_engine_instance(hass, engine_id) + + assert engine is not None + # The platform config provider will be overridden by the discovery info provider. + assert engine.default_language == "en-US" + assert engine.default_options == {"gender": "female", "audio_output": "mp3"} + + await set_cloud_prefs({"tts_default_voice": ("nl-NL", "male")}) + await hass.async_block_till_done() + + assert engine.default_language == "nl-NL" + assert engine.default_options == {"gender": "male", "audio_output": "mp3"} -async def test_provider_properties(cloud_with_prefs) -> None: +async def test_provider_properties( + hass: HomeAssistant, + cloud: MagicMock, +) -> None: """Test cloud provider.""" - tts_info = {"platform_loaded": Mock()} - provider = await tts.async_get_engine( - Mock(data={const.DOMAIN: cloud_with_prefs}), None, tts_info - ) - assert provider.supported_options == ["gender", "voice", "audio_output"] - assert "nl-NL" in provider.supported_languages - assert tts.Voice( - "ColetteNeural", "ColetteNeural" - ) in provider.async_get_supported_voices("nl-NL") + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + on_start_callback = cloud.register_on_start.call_args[0][0] + await on_start_callback() + + engine = get_engine_instance(hass, DOMAIN) + + assert engine is not None + assert engine.supported_options == ["gender", "voice", "audio_output"] + assert "nl-NL" in engine.supported_languages + supported_voices = engine.async_get_supported_voices("nl-NL") + assert supported_voices is not None + assert tts.Voice("ColetteNeural", "ColetteNeural") in supported_voices + supported_voices = engine.async_get_supported_voices("missing_language") + assert supported_voices is None -async def test_get_tts_audio(cloud_with_prefs) -> None: +@pytest.mark.parametrize( + ("data", "expected_url_suffix"), + [ + ({"platform": DOMAIN}, DOMAIN), + ({"engine_id": DOMAIN}, DOMAIN), + ], +) +@pytest.mark.parametrize( + ("mock_process_tts_return_value", "mock_process_tts_side_effect"), + [ + (b"", None), + (None, VoiceError("Boom!")), + ], +) +async def test_get_tts_audio( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + cloud: MagicMock, + data: dict[str, Any], + expected_url_suffix: str, + mock_process_tts_return_value: bytes | None, + mock_process_tts_side_effect: Exception | None, +) -> None: """Test cloud provider.""" - tts_info = {"platform_loaded": Mock()} - provider = await tts.async_get_engine( - Mock(data={const.DOMAIN: cloud_with_prefs}), None, tts_info + mock_process_tts = AsyncMock( + return_value=mock_process_tts_return_value, + side_effect=mock_process_tts_side_effect, ) - assert provider.supported_options == ["gender", "voice", "audio_output"] - assert "nl-NL" in provider.supported_languages + cloud.voice.process_tts = mock_process_tts + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + on_start_callback = cloud.register_on_start.call_args[0][0] + await on_start_callback() + client = await hass_client() + + url = "/api/tts_get_url" + data |= {"message": "There is someone at the door."} + + req = await client.post(url, json=data) + assert req.status == HTTPStatus.OK + response = await req.json() + + assert response == { + "url": ( + "http://example.local:8123/api/tts_proxy/" + "42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + ), + "path": ( + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + ), + } + await hass.async_block_till_done() + + assert mock_process_tts.call_count == 1 + assert mock_process_tts.call_args is not None + assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." + assert mock_process_tts.call_args.kwargs["language"] == "en-US" + assert mock_process_tts.call_args.kwargs["gender"] == "female" + assert mock_process_tts.call_args.kwargs["output"] == "mp3"