From 3bd6dea63b85fee47c38c1289fd25f6a36e9e46f Mon Sep 17 00:00:00 2001 From: Dara Adib Date: Wed, 24 May 2023 02:25:03 -0400 Subject: [PATCH] Add tests for Microsoft TTS (#92215) * Add tests for Microsoft TTS * Update requirements_test_all * Update tests/components/microsoft/__init__.py --------- Co-authored-by: Erik Montnemery --- requirements_test_all.txt | 3 + tests/components/microsoft/__init__.py | 1 + tests/components/microsoft/test_tts.py | 242 +++++++++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 tests/components/microsoft/__init__.py create mode 100644 tests/components/microsoft/test_tts.py diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38f0ea09158..0595d6aa588 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1159,6 +1159,9 @@ pycomfoconnect==0.5.1 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.5 +# homeassistant.components.microsoft +pycsspeechtts==1.0.8 + # homeassistant.components.daikin pydaikin==2.9.0 diff --git a/tests/components/microsoft/__init__.py b/tests/components/microsoft/__init__.py new file mode 100644 index 00000000000..5c1ed8dad09 --- /dev/null +++ b/tests/components/microsoft/__init__.py @@ -0,0 +1 @@ +"""Tests for the Microsoft Text-to-Speech component.""" diff --git a/tests/components/microsoft/test_tts.py b/tests/components/microsoft/test_tts.py new file mode 100644 index 00000000000..d3f8e36e314 --- /dev/null +++ b/tests/components/microsoft/test_tts.py @@ -0,0 +1,242 @@ +"""Tests for Microsoft Text-to-Speech.""" +import os +import shutil +from unittest.mock import patch + +from pycsspeechtts import pycsspeechtts +import pytest +from requests import HTTPError + +from homeassistant.components import media_source, tts +from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, +) +from homeassistant.components.microsoft.tts import SUPPORTED_LANGUAGES +from homeassistant.config import async_process_ha_core_config +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceNotFound +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + + +async def get_media_source_url(hass: HomeAssistant, media_content_id): + """Get the media source url.""" + if media_source.DOMAIN not in hass.config.components: + assert await async_setup_component(hass, media_source.DOMAIN, {}) + + resolved = await media_source.async_resolve_media(hass, media_content_id, None) + return resolved.url + + +@pytest.fixture(autouse=True) +def cleanup_cache(hass: HomeAssistant): + """Clean up TTS cache.""" + yield + default_tts = hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(default_tts): + shutil.rmtree(default_tts) + + +@pytest.fixture +async def calls(hass: HomeAssistant): + """Mock media player calls.""" + return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + +@pytest.fixture(autouse=True) +async def setup_internal_url(hass: HomeAssistant): + """Set up internal url.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"} + ) + + +@pytest.fixture +def mock_tts(): + """Mock tts.""" + with patch( + "homeassistant.components.microsoft.tts.pycsspeechtts.TTSTranslator" + ) as mock_tts: + mock_tts.return_value.speak.return_value = b"" + yield mock_tts + + +async def test_service_say(hass: HomeAssistant, mock_tts, calls) -> None: + """Test service call say.""" + + await async_setup_component( + hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "microsoft", "api_key": ""}} + ) + + await hass.services.async_call( + tts.DOMAIN, + "microsoft_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) + + assert len(calls) == 1 + url = await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + assert len(mock_tts.mock_calls) == 2 + assert url.endswith(".mp3") + + assert mock_tts.mock_calls[1][2] == { + "language": "en-us", + "gender": "Female", + "voiceType": "JennyNeural", + "output": "audio-24khz-96kbitrate-mono-mp3", + "rate": "0%", + "volume": "0%", + "pitch": "default", + "contour": "", + "text": "There is a person at the front door.", + } + + +async def test_service_say_en_gb_config(hass: HomeAssistant, mock_tts, calls) -> None: + """Test service call say with en-gb code in the config.""" + + await async_setup_component( + hass, + tts.DOMAIN, + { + tts.DOMAIN: { + "platform": "microsoft", + "api_key": "", + "language": "en-gb", + "type": "AbbiNeural", + } + }, + ) + + await hass.services.async_call( + tts.DOMAIN, + "microsoft_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) + + assert len(calls) == 1 + await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + assert len(mock_tts.mock_calls) == 2 + assert mock_tts.mock_calls[1][2] == { + "language": "en-gb", + "gender": "Female", + "voiceType": "AbbiNeural", + "output": "audio-24khz-96kbitrate-mono-mp3", + "rate": "0%", + "volume": "0%", + "pitch": "default", + "contour": "", + "text": "There is a person at the front door.", + } + + +async def test_service_say_en_gb_service(hass: HomeAssistant, mock_tts, calls) -> None: + """Test service call say with en-gb code in the service.""" + + await async_setup_component( + hass, + tts.DOMAIN, + {tts.DOMAIN: {"platform": "microsoft", "api_key": ""}}, + ) + + await hass.services.async_call( + tts.DOMAIN, + "microsoft_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_LANGUAGE: "en-gb", + tts.ATTR_OPTIONS: {"type": "AbbiNeural"}, + }, + blocking=True, + ) + + assert len(calls) == 1 + await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + assert len(mock_tts.mock_calls) == 2 + assert mock_tts.mock_calls[1][2] == { + "language": "en-gb", + "gender": "Female", + "voiceType": "AbbiNeural", + "output": "audio-24khz-96kbitrate-mono-mp3", + "rate": "0%", + "volume": "0%", + "pitch": "default", + "contour": "", + "text": "There is a person at the front door.", + } + + +def test_supported_languages() -> None: + """Test list of supported languages.""" + for lang in ["en-us", "en-gb"]: + assert lang in SUPPORTED_LANGUAGES + assert "en-US" not in SUPPORTED_LANGUAGES + for lang in [ + "en", + "en-uk", + "english", + "english (united states)", + "jennyneural", + "en-us-jennyneural", + ]: + assert lang not in {s.lower() for s in SUPPORTED_LANGUAGES} + assert len(SUPPORTED_LANGUAGES) > 70 + + +async def test_invalid_language(hass: HomeAssistant, mock_tts, calls) -> None: + """Test setup component with invalid language.""" + await async_setup_component( + hass, + tts.DOMAIN, + {tts.DOMAIN: {"platform": "microsoft", "api_key": "", "language": "en"}}, + ) + + with pytest.raises(ServiceNotFound): + await hass.services.async_call( + tts.DOMAIN, + "microsoft_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) + + assert len(calls) == 0 + assert len(mock_tts.mock_calls) == 0 + + +async def test_service_say_error(hass: HomeAssistant, mock_tts, calls) -> None: + """Test service call say with http error.""" + mock_tts.return_value.speak.side_effect = pycsspeechtts.requests.HTTPError + await async_setup_component( + hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "microsoft", "api_key": ""}} + ) + + await hass.services.async_call( + tts.DOMAIN, + "microsoft_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) + + assert len(calls) == 1 + # Note: the integration currently catches HTTPException instead of HTTPError. + with pytest.raises(HTTPError): + await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + assert len(mock_tts.mock_calls) == 2