From 72b3226eb9d22211b987a126b9e5a99343d627b5 Mon Sep 17 00:00:00 2001 From: Mike Gray Date: Mon, 1 Jan 2024 11:25:03 -0600 Subject: [PATCH 1/3] feat: testing and cleanup features - significant unit test coverage - some refactors to accommodate testing - e2e tests - GHA workflows to do all testing and linting - bandit - safety - flake8 - isort - ruff - mypy fixes for ci/cd more fixes test fixes more test fixes sudo more e2e more fixes further improvements improve e2e testing get better logs give it some time, and drop testing for mimic3 get mimic running for e2e testing sudo it more e2e attempted fixes another approach for e2e fixes better logic more special sam work, force v1 sam is very special more attempted fixes another approach branch piper tests --- .coveragerc | 5 + .github/workflows/e2e_tests.yml | 62 ++++++++ .github/workflows/publish_alpha.yml | 2 +- .github/workflows/unit_tests.yml | 49 ++++++- .gitignore | 162 ++++++++++++++++++++ ovos_tts_plugin_server/__init__.py | 95 ++++++++---- requirements-dev.txt | 12 ++ requirements-e2e.txt | 11 ++ requirements.txt | 3 +- setup.cfg | 4 + setup.py | 88 ++++++----- tests/__init__.py | 0 tests/e2e.py | 35 +++++ tests/main_test.py | 220 ++++++++++++++++++++++++++++ tests/mimic/mimic.gpg.key | 24 +++ tests/mimic/mimic.list | 1 + 16 files changed, 688 insertions(+), 85 deletions(-) create mode 100644 .coveragerc create mode 100644 .github/workflows/e2e_tests.yml create mode 100644 .gitignore create mode 100644 requirements-dev.txt create mode 100644 requirements-e2e.txt create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/e2e.py create mode 100644 tests/main_test.py create mode 100644 tests/mimic/mimic.gpg.key create mode 100644 tests/mimic/mimic.list diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..25d0adc --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +branch = True +source = ovos_tts_plugin_server +omit = + */version.py diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml new file mode 100644 index 0000000..123743b --- /dev/null +++ b/.github/workflows/e2e_tests.yml @@ -0,0 +1,62 @@ +name: End-to-End Tests + +on: + push: + branches: [master] + pull_request: + branches: [master, dev] + +jobs: + e2e-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - plugin: "ovos-tts-plugin-piper" + # image: "ghcr.io/openvoiceos/piper-tts-server:dev" # TODO: This doesn't exist yet + image: "ghcr.io/mikejgray/piper-tts-server:dev" + - plugin: "ovos-tts-plugin-espeakng" + image: "ghcr.io/openvoiceos/espeakng:dev" + - plugin: "ovos-tts-plugin-mimic" + image: "ghcr.io/openvoiceos/mimic:dev" + - plugin: "ovos-tts-plugin-beepspeak" + image: "ghcr.io/openvoiceos/beepspeak-tts-server:master" + - plugin: "ovos-tts-plugin-sam" + image: "ghcr.io/openvoiceos/sam:dev" + + env: + PLUGIN_TO_TEST: ${{ matrix.plugin }} + + services: + sam: + image: ${{ matrix.image }} + ports: + - 9666:9666 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Cache Python packages + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-e2e.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + pip install -U -r requirements.txt + pip install -U --pre -r requirements-e2e.txt + pip install . + + - name: E2E Tests + run: | + pytest tests/e2e.py diff --git a/.github/workflows/publish_alpha.yml b/.github/workflows/publish_alpha.yml index 7ec11af..de7c61a 100644 --- a/.github/workflows/publish_alpha.yml +++ b/.github/workflows/publish_alpha.yml @@ -29,4 +29,4 @@ jobs: alpha_var: VERSION_ALPHA build_var: VERSION_BUILD minor_var: VERSION_MINOR - major_var: VERSION_MAJOR \ No newline at end of file + major_var: VERSION_MAJOR diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 3e380f3..f87f166 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -1,10 +1,49 @@ name: Unit Tests on: - push: + pull_request: + branches: + - master + - dev workflow_dispatch: jobs: - py_build_tests: - uses: neongeckocom/.github/.github/workflows/python_build_tests.yml@master - with: - python_version: "3.8" + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache Python dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }} + restore-keys: | + ${{ runner.os }}-python-${{ matrix.python-version }}- + ${{ runner.os }}-python- + + - name: Install dependencies + run: |- + pip install -r requirements.txt + pip install -r requirements-dev.txt + pip install . + + - name: Lint + run: |- + black --line-length=119 . + bandit -ll -r ovos_tts_plugin_server + flake8 --max-line-length=119 ovos_tts_plugin_server + isort --profile black . + ruff ovos_tts_plugin_server + mypy --ignore-missing-imports --exclude tests ovos_tts_plugin_server + safety check --ignore=58755 --continue-on-error + + - name: Test + run: pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62ccfc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ +.DS_Store +.vscode/ diff --git a/ovos_tts_plugin_server/__init__.py b/ovos_tts_plugin_server/__init__.py index 98c4c8a..d788459 100755 --- a/ovos_tts_plugin_server/__init__.py +++ b/ovos_tts_plugin_server/__init__.py @@ -1,24 +1,34 @@ -from typing import Optional -import requests +"""OpenVoiceOS companion plugin for OpenVoiceOS TTS Server.""" import random -from ovos_plugin_manager.templates.tts import TTS, TTSValidator, RemoteTTSException +from typing import Any, Dict, List, Optional, Tuple + +import requests +from ovos_plugin_manager.templates.tts import TTS, RemoteTTSException, TTSValidator +from ovos_utils.log import LOG + +PUBLIC_TTS_SERVERS = [ + "https://pipertts.ziggyai.online", + "https://tts.smartgic.io/piper", +] class OVOSServerTTS(TTS): - public_servers = [ - "https://pipertts.ziggyai.online", - "https://tts.smartgic.io/piper" - ] + """Interface to OVOS TTS server""" + + public_servers: List[str] = PUBLIC_TTS_SERVERS def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs, audio_ext="wav", - validator=OVOSServerTTSValidator(self)) + super().__init__(*args, **kwargs, audio_ext="wav", validator=OVOSServerTTSValidator(self)) + self.log = LOG(name=__name__) if not self.verify_ssl: - self.log.warning("SSL verification disabled, this is not secure and should" - "only be used for test systems! Please set up a valid certificate!") + self.log.warning( + "SSL verification disabled, this is not secure and should" + "only be used for test systems! Please set up a valid certificate!" + ) @property def host(self) -> Optional[str]: + """If using a custom server, set the host here, otherwise it defaults to public servers.""" return self.config.get("host", None) @property @@ -28,55 +38,76 @@ def v2(self) -> bool: @property def verify_ssl(self) -> bool: + """Whether or not to verify SSL certificates when connecting to the server. Defaults to True.""" return self.config.get("verify_ssl", True) - def get_tts(self, sentence, wav_file, lang=None, voice=None): - lang = lang or self.lang - voice = voice or self.voice - params = {"lang": lang, "voice": voice} + def get_tts( + self, + sentence, + wav_file, + lang: Optional[str] = None, + voice: Optional[str] = None, + ) -> Tuple[Any, None]: + """Fetch TTS audio using OVOS TTS server. + Language and voice can be overridden, otherwise defaults to config.""" + params: Dict[str, Optional[str]] = { + "lang": lang or self.lang, + "voice": voice or self.voice, + } if not voice or voice == "default": params.pop("voice") if self.host: if isinstance(self.host, str): - servers = [self.host] + servers: List[str] = [self.host] else: servers = self.host else: - servers = self.public_servers - data = self._get_from_servers(params, sentence, servers) - with open(wav_file, "wb") as f: - f.write(data) + servers = self.public_servers + data: bytes = self._fetch_audio_data(params, sentence, servers) + self._write_audio_file(wav_file, data) return wav_file, None - def _get_from_servers(self, params: dict, sentence: str, servers: list): + def _write_audio_file(self, wav_file: str, data: bytes) -> None: + with open(file=wav_file, mode="wb") as f: + f.write(data) + + def _fetch_audio_data(self, params: dict, sentence: str, servers: list) -> bytes: + """Get audio bytes from servers.""" random.shuffle(servers) # Spread the load among all public servers for url in servers: try: if self.v2: - url = f'{url}/v2/synthesize' + url = f"{url}/v2/synthesize" params["utterance"] = sentence else: - url = f'{url}/synthesize/{sentence}' - r = requests.get(url, params=params, verify=self.verify_ssl) + url = f"{url}/synthesize/{sentence}" + r: requests.Response = requests.get(url=url, params=params, verify=self.verify_ssl, timeout=30) if r.ok: return r.content - except: + self.log.error(f"Failed to get audio, response from {url}: {r.text}") + except Exception as err: # pylint: disable=broad-except + self.log.error(f"Failed to get audio from {url}: {err}") continue - raise RemoteTTSException(f"All OVOS TTS servers are down!") + raise RemoteTTSException("All OVOS TTS servers are down!") class OVOSServerTTSValidator(TTSValidator): - def __init__(self, tts): + """Validate settings for OVOS TTS server plugin.""" + + def __init__(self, tts) -> None: # pylint: disable=useless-parent-delegation super(OVOSServerTTSValidator, self).__init__(tts) - def validate_lang(self): - pass + def validate_lang(self) -> None: + """Validate language setting.""" + return - def validate_connection(self): - pass + def validate_connection(self) -> None: + """Validate connection to server.""" + return def get_tts_class(self): + """Return TTS class.""" return OVOSServerTTS -OVOSServerTTSConfig = {} +OVOSServerTTSConfig: Dict[Any, Any] = {} diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..cb66865 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,12 @@ +ovos-classifiers~=0.0.0a40 +pytest +pytest-cov + +black +bandit +flake8 +isort +ruff +mypy +types-requests +safety diff --git a/requirements-e2e.txt b/requirements-e2e.txt new file mode 100644 index 0000000..f65993f --- /dev/null +++ b/requirements-e2e.txt @@ -0,0 +1,11 @@ +ovos-tts-plugin-piper~=0.0.0 +# ovos-tts-plugin-espeakng~=0.0.2 # May be bugged +ovos-tts-plugin-mimic~=0.2.8 +ovos-tts-plugin-beepspeak~=0.0.1a1 +# ovos-tts-plugin-responsivevoice~=0.1.1 # May be bugged + +ovos-tts-server~=0.0.3a10 + +pytest +pytest-cov +ovos-classifiers~=0.0.0a40 diff --git a/requirements.txt b/requirements.txt index a5e66b9..dd07ffe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -ovos-plugin-manager>=0.0.5 \ No newline at end of file +ovos-plugin-manager>=0.0.5 +requests ~= 2.31.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..d1d768f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[tool:pytest] +addopts = --cov=ovos_tts_plugin_server --cov-report=term --cov-report=html +testpaths = + tests diff --git a/setup.py b/setup.py index 6c9fef2..6668868 100755 --- a/setup.py +++ b/setup.py @@ -1,28 +1,28 @@ #!/usr/bin/env python3 import os + from setuptools import setup BASEDIR = os.path.abspath(os.path.dirname(__file__)) def get_version(): - """ Find the version of the package""" + """Find the version of the package""" version = None - version_file = os.path.join(BASEDIR, 'ovos_tts_plugin_server', 'version.py') + version_file = os.path.join(BASEDIR, "ovos_tts_plugin_server", "version.py") major, minor, build, alpha = (None, None, None, None) with open(version_file) as f: for line in f: - if 'VERSION_MAJOR' in line: - major = line.split('=')[1].strip() - elif 'VERSION_MINOR' in line: - minor = line.split('=')[1].strip() - elif 'VERSION_BUILD' in line: - build = line.split('=')[1].strip() - elif 'VERSION_ALPHA' in line: - alpha = line.split('=')[1].strip() + if "VERSION_MAJOR" in line: + major = line.split("=")[1].strip() + elif "VERSION_MINOR" in line: + minor = line.split("=")[1].strip() + elif "VERSION_BUILD" in line: + build = line.split("=")[1].strip() + elif "VERSION_ALPHA" in line: + alpha = line.split("=")[1].strip() - if ((major and minor and build and alpha) or - '# END_VERSION_BLOCK' in line): + if (major and minor and build and alpha) or "# END_VERSION_BLOCK" in line: break version = f"{major}.{minor}.{build}" if alpha and int(alpha) > 0: @@ -31,14 +31,13 @@ def get_version(): def required(requirements_file): - """ Read requirements file and remove comments and empty lines. """ - with open(os.path.join(BASEDIR, requirements_file), 'r') as f: + """Read requirements file and remove comments and empty lines.""" + with open(os.path.join(BASEDIR, requirements_file), "r") as f: requirements = f.read().splitlines() - if 'MYCROFT_LOOSE_REQUIREMENTS' in os.environ: - print('USING LOOSE REQUIREMENTS!') - requirements = [r.replace('==', '>=').replace('~=', '>=') for r in requirements] - return [pkg for pkg in requirements - if pkg.strip() and not pkg.startswith("#")] + if "MYCROFT_LOOSE_REQUIREMENTS" in os.environ: + print("USING LOOSE REQUIREMENTS!") + requirements = [r.replace("==", ">=").replace("~=", ">=") for r in requirements] + return [pkg for pkg in requirements if pkg.strip() and not pkg.startswith("#")] def get_description(): @@ -46,42 +45,39 @@ def get_description(): long_description = f.read() return long_description -PLUGIN_ENTRY_POINT = 'ovos-tts-plugin-server = ovos_tts_plugin_server:OVOSServerTTS' -CONFIG_ENTRY_POINT = 'ovos-tts-plugin-server.config = ovos_tts_plugin_server:OVOSServerTTSConfig' + +PLUGIN_ENTRY_POINT = "ovos-tts-plugin-server = ovos_tts_plugin_server:OVOSServerTTS" +CONFIG_ENTRY_POINT = "ovos-tts-plugin-server.config = ovos_tts_plugin_server:OVOSServerTTSConfig" setup( - name='ovos-tts-plugin-server', + name="ovos-tts-plugin-server", version=get_version(), - description='ovos tts server plugin for mycroft', + description="ovos tts server plugin for mycroft", long_description=get_description(), long_description_content_type="text/markdown", - url='https://github.com/OpenVoiceOS/ovos-tts-server-plugin', - author='JarbasAi', - author_email='jarbasai@mailfence.com', - license='Apache-2.0', - packages=['ovos_tts_plugin_server'], + url="https://github.com/OpenVoiceOS/ovos-tts-server-plugin", + author="JarbasAi", + author_email="jarbasai@mailfence.com", + license="Apache-2.0", + packages=["ovos_tts_plugin_server"], install_requires=required("requirements.txt"), zip_safe=True, include_package_data=True, classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Topic :: Text Processing :: Linguistic', - 'License :: OSI Approved :: Apache Software License', - - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.0', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Text Processing :: Linguistic", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], - keywords='mycroft OpenVoiceOS OVOS plugin tts', - entry_points={'mycroft.plugin.tts': PLUGIN_ENTRY_POINT, - 'mycroft.plugin.tts.config': CONFIG_ENTRY_POINT} + keywords="mycroft OpenVoiceOS OVOS plugin tts", + entry_points={ + "mycroft.plugin.tts": PLUGIN_ENTRY_POINT, + "mycroft.plugin.tts.config": CONFIG_ENTRY_POINT, + }, ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e.py b/tests/e2e.py new file mode 100644 index 0000000..59f53a9 --- /dev/null +++ b/tests/e2e.py @@ -0,0 +1,35 @@ +# pylint: disable=missing-docstring,redefined-outer-name,protected-access +import os + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +from ovos_tts_plugin_server import OVOSServerTTS + + +def requests_retry_session( + retries=3, backoff_factor=0.3, status_forcelist=(500, 502, 503, 504) +): + session = requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount("http://", adapter) + session.mount("https://", adapter) + return session + + +def test_tts_plugin_e2e(): + tts_instance = OVOSServerTTS( + config={"host": "http://localhost:9666"} + ) + tts_instance.get_tts(sentence="Hello%20world", wav_file="test.wav") + assert os.path.exists(path="test.wav") + assert os.path.getsize(filename="test.wav") > 0 + os.remove("test.wav") diff --git a/tests/main_test.py b/tests/main_test.py new file mode 100644 index 0000000..6c37558 --- /dev/null +++ b/tests/main_test.py @@ -0,0 +1,220 @@ +# pylint: disable=missing-docstring,redefined-outer-name,protected-access +from unittest.mock import MagicMock, mock_open, patch + +import pytest +from ovos_utils.log import LOG +from requests.exceptions import RequestException + +from ovos_tts_plugin_server import PUBLIC_TTS_SERVERS, OVOSServerTTS, RemoteTTSException + + +@pytest.fixture +def tts_instance() -> OVOSServerTTS: + return OVOSServerTTS() + + +@pytest.fixture +def tts_instance_factory(): + def create_instance(config): + return OVOSServerTTS(config=config) + + return create_instance + + +def test_initialization(tts_instance): + assert tts_instance.audio_ext == "wav" + assert tts_instance.validator is not None + assert isinstance(tts_instance.log, LOG) + assert tts_instance.host is None + assert tts_instance.v2 is True + assert tts_instance.verify_ssl is True + + +def test_host_property(tts_instance, tts_instance_factory): + # Default behavior - No host set + assert tts_instance.host is None + + # Custom host set + custom_host = "https://customhost.com" + custom_tts_instance = tts_instance_factory({"host": custom_host}) + assert tts_instance.host != custom_tts_instance.host + assert custom_tts_instance.host == custom_host + + +@pytest.mark.parametrize( + "host,expected", [(None, True), ("https://customhost.com", False)] +) +def test_v2_property(tts_instance, host, expected): + tts_instance.config["host"] = host + assert tts_instance.v2 is expected + + +def test_verify_ssl_property(tts_instance): + # Default behavior - SSL verification enabled + assert tts_instance.verify_ssl is True + + # SSL verification explicitly disabled + tts_instance.config["verify_ssl"] = False + assert tts_instance.verify_ssl is False + + # SSL verification explicitly enabled + tts_instance.config["verify_ssl"] = True + assert tts_instance.verify_ssl is True + + +@patch( + "ovos_tts_plugin_server.OVOSServerTTS._fetch_audio_data", return_value=b"audio data" +) +@patch("ovos_tts_plugin_server.OVOSServerTTS._write_audio_file") +def test_get_tts(mock_write_audio, mock_fetch_audio, tts_instance): + sentence = "test sentence" + wav_file = "test.wav" + result = tts_instance.get_tts(sentence, wav_file) + + mock_fetch_audio.assert_called_once() + mock_write_audio.assert_called_once_with(wav_file, b"audio data") + assert result[0] == wav_file + + +@patch("ovos_tts_plugin_server.requests.get") +def test_fetch_audio_data_success(mock_get, tts_instance): + mock_response = MagicMock() + mock_response.ok = True + mock_response.content = b"audio data" + mock_get.return_value = mock_response + + sentence = "test sentence" + servers = tts_instance.public_servers + data = tts_instance._fetch_audio_data({}, sentence, servers) + + assert data == b"audio data" + mock_get.assert_called() + + +@patch("ovos_tts_plugin_server.requests.get") +def test_fetch_audio_data_failure(mock_get, tts_instance): + mock_response = MagicMock() + mock_response.ok = False + mock_get.return_value = mock_response + + sentence = "test sentence" + servers = tts_instance.public_servers + + with pytest.raises(RemoteTTSException): + tts_instance._fetch_audio_data({}, sentence, servers) + + assert mock_get.call_count == len(servers) # Ensure all servers are tried + + +@patch("ovos_tts_plugin_server.requests.get", side_effect=RequestException) +def test_fetch_audio_data_exception(mock_get, tts_instance): + sentence = "test sentence" + servers = tts_instance.public_servers + + with pytest.raises(RemoteTTSException): + tts_instance._fetch_audio_data({}, sentence, servers) + + assert mock_get.call_count == len(servers) # Ensure all servers are tried + + +@patch("ovos_utils.log.LOG.warning") +def test_logged_warning(mock_log_warning, tts_instance_factory): + # Create an instance with the specific configuration using the factory fixture + tts_instance_factory({"verify_ssl": False}) + + # Assertions to check the behavior and logging + assert mock_log_warning.call_count == 1 + assert mock_log_warning.call_args[0][0] == ( + "SSL verification disabled, this is not secure and should" + "only be used for test systems! Please set up a valid certificate!" + ) + + +def test_write_audio_file_success(tts_instance): + mock_data = b"test audio data" + mock_file_path = "test.wav" + + # Patch the built-in open function + with patch("builtins.open", mock_open()) as mocked_file: + tts_instance._write_audio_file(mock_file_path, mock_data) + + # Check if open was called correctly + mocked_file.assert_called_with(file=mock_file_path, mode="wb") + + # Check if the correct data was written to the file + mocked_file.return_value.write.assert_called_with(mock_data) + + +def test_validator(tts_instance): + assert tts_instance.validator.validate_lang() is None + assert tts_instance.validator.validate_connection() is None + assert tts_instance.validator.get_tts_class() == OVOSServerTTS + + +@patch( + "ovos_tts_plugin_server.OVOSServerTTS._fetch_audio_data", return_value=b"audio data" +) +@patch("ovos_tts_plugin_server.OVOSServerTTS._write_audio_file") +def test_get_tts_param_change(_, fetch_audio_data, tts_instance): + tts_instance.get_tts( + sentence="test", wav_file="test.wav", lang="en-us", voice="default" + ) + fetch_audio_data.assert_called_with({"lang": "en-us"}, "test", PUBLIC_TTS_SERVERS) + fetch_audio_data.reset_mock() + + tts_instance.get_tts(sentence="test", wav_file="test.wav", lang="en-us") + fetch_audio_data.assert_called_with({"lang": "en-us"}, "test", PUBLIC_TTS_SERVERS) + fetch_audio_data.reset_mock() + + tts_instance.get_tts( + sentence="test", wav_file="test.wav", lang="en-us", voice="apope-low" + ) + fetch_audio_data.assert_called_with( + {"lang": "en-us", "voice": "apope-low"}, "test", PUBLIC_TTS_SERVERS + ) + + +@patch( + "ovos_tts_plugin_server.OVOSServerTTS._fetch_audio_data", return_value=b"audio data" +) +@patch("ovos_tts_plugin_server.OVOSServerTTS._write_audio_file") +def test_get_tts_server_lists(_, fetch_audio_data, tts_instance_factory): + # Default behavior - No host set + tts_instance = tts_instance_factory(config={}) + tts_instance.get_tts("test", "test.wav") + fetch_audio_data.assert_called_with( + {"lang": "en-us"}, "test", tts_instance.public_servers + ) + fetch_audio_data.reset_mock() + # Custom host set + custom_host = "https://customhost.com" + tts_instance = tts_instance_factory(config={"host": custom_host}) + tts_instance.get_tts("test", "test.wav") + fetch_audio_data.assert_called_with({"lang": "en-us"}, "test", [custom_host]) + fetch_audio_data.reset_mock() + # Multiple hosts set + custom_hosts = ["https://customhost1.com", "https://customhost2.com"] + tts_instance = tts_instance_factory(config={"host": custom_hosts}) + tts_instance.get_tts("test", "test.wav") + fetch_audio_data.assert_called_with({"lang": "en-us"}, "test", custom_hosts) + + +@patch("requests.get") +@patch("ovos_tts_plugin_server.OVOSServerTTS._write_audio_file") +def test_v2_property_passing(_, mock_requests, tts_instance_factory): + # Default behavior + tts_instance = tts_instance_factory({}) + assert tts_instance.v2 is True + + # Custom host set + tts_instance = tts_instance_factory( + config={"v2": False, "host": "https://customhost.com"} + ) + assert tts_instance.v2 is False + tts_instance.get_tts("test", "test.wav") + mock_requests.assert_called_with( + url="https://customhost.com/synthesize/test", + params={"lang": "en-us"}, + verify=True, + timeout=30, + ) diff --git a/tests/mimic/mimic.gpg.key b/tests/mimic/mimic.gpg.key new file mode 100644 index 0000000..f289847 --- /dev/null +++ b/tests/mimic/mimic.gpg.key @@ -0,0 +1,24 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBF+3+kEBDADyBHjOfzE2n+rPxovOO7aOTX3KUgq8qqtqZ5dz4MVZ8fzX4cxy +j69/lT6Zx81JnDJy4GAid4IWRFnpBcgNVoMwF59BiDW/quvNfMj25IHTqoLMGhWY +PmTwmNGQBc5Bm/NNLxjtZBRSjUqqSFDlWalvRptdKi22GheCf2PxRYF+UG8jxB4m +QIGfy4/tna91Iez3DgPXYuXy4rawSfxJsOozVnJUGO8G3AakHSEDzVOACEX1yKmt +ibVH5V2/bvEit0qhQeZwlEChvcHq0lv7bxgqPsE5X2B1O1V2x6vOcetZhJJZ1jqY +S/Y36iwJMAbQNAfistBaAhHAq7PZ3pReaa4nVW2oLqPHC59Drq60u3WsveQOJnTp +Mt5FfW75/1+PPX/sCL+27HfFpYEHpaoLJHPH4z/ht/u8SWR601qlA/JjjEarQ/Qy +EuG8hYF7u83aiHcuuwaSVMk2i2OZGesbhbepElPNCwxHsJnUNQaUQsDhPeP5rP2s +MkYl+x2MXooPwDEAEQEAAbQtbXljcm9mdC1kZXNrdG9wLXJlcG8gPGFrZS5mb3Jz +bHVuZEBnbWFpbC5jb20+iQHUBBMBCgA+AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B +AheAFiEEj6zH4HNa/2SUFHneZ3azPalyr9EFAmN9KkUFCRaRMwQACgkQZ3azPaly +r9Heigv/VQwxJP3hCUlqBppvYQnqRdLUTgwKN/JDnjAiqLvE0fKgoYXJ0Af0tf1q +oyOdhwurNhKHHV+hWqclt8MRXb/BoExFG2h5Au3NNh6HYVBNWSdUbac6fgEuoaBS +QHc/7K5NTAVGtSsFucAPQ0Kg73TVqvmoUUW/sJXyMgt1yiIwp17rt9zhPX2pJ7xG +pHncg579+8yitTygcPFxrtmAB3ApPGDdysJhnM/7gB9YwnqN9I60/ROvrfrg6eOk +r0skKoPPQnAZHkoK/anpOlCdQao+8PX8k6s6Nm79tNfLg2HjCPciL/k+jr59D2eI +2iFq7L1rvLhqxKTEoB5m1guOnnLUWlHyvoQmox/idUVYDYGyHs7L9nkfGEkaVnKq +q9g0QpIME2/tDMNDXvsFB9b94nYFJodBbmNh1lvrSuUeqT+YFB05psANcObD1oOf +w1idiadf433Ho7eOz41JruGKQvz/LAZEKwVMPVyh0G31jNvp+GdNkV5KqehGpp6o +wgf4+q7R +=VQMc +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/mimic/mimic.list b/tests/mimic/mimic.list new file mode 100644 index 0000000..55d3616 --- /dev/null +++ b/tests/mimic/mimic.list @@ -0,0 +1 @@ +deb http://forslund.github.io/mycroft-desktop-repo bionic main From d63ceb9e107d468cf6561dfd303bccb78d842745 Mon Sep 17 00:00:00 2001 From: Mike Gray Date: Mon, 1 Jan 2024 15:09:35 -0600 Subject: [PATCH 2/3] follow established plugin logging pattern remove now unnecessary mimic stuff --- ovos_tts_plugin_server/__init__.py | 2 +- tests/mimic/mimic.gpg.key | 24 ------------------------ tests/mimic/mimic.list | 1 - 3 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 tests/mimic/mimic.gpg.key delete mode 100644 tests/mimic/mimic.list diff --git a/ovos_tts_plugin_server/__init__.py b/ovos_tts_plugin_server/__init__.py index d788459..504d617 100755 --- a/ovos_tts_plugin_server/__init__.py +++ b/ovos_tts_plugin_server/__init__.py @@ -19,7 +19,7 @@ class OVOSServerTTS(TTS): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs, audio_ext="wav", validator=OVOSServerTTSValidator(self)) - self.log = LOG(name=__name__) + self.log = LOG if not self.verify_ssl: self.log.warning( "SSL verification disabled, this is not secure and should" diff --git a/tests/mimic/mimic.gpg.key b/tests/mimic/mimic.gpg.key deleted file mode 100644 index f289847..0000000 --- a/tests/mimic/mimic.gpg.key +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQGNBF+3+kEBDADyBHjOfzE2n+rPxovOO7aOTX3KUgq8qqtqZ5dz4MVZ8fzX4cxy -j69/lT6Zx81JnDJy4GAid4IWRFnpBcgNVoMwF59BiDW/quvNfMj25IHTqoLMGhWY -PmTwmNGQBc5Bm/NNLxjtZBRSjUqqSFDlWalvRptdKi22GheCf2PxRYF+UG8jxB4m -QIGfy4/tna91Iez3DgPXYuXy4rawSfxJsOozVnJUGO8G3AakHSEDzVOACEX1yKmt -ibVH5V2/bvEit0qhQeZwlEChvcHq0lv7bxgqPsE5X2B1O1V2x6vOcetZhJJZ1jqY -S/Y36iwJMAbQNAfistBaAhHAq7PZ3pReaa4nVW2oLqPHC59Drq60u3WsveQOJnTp -Mt5FfW75/1+PPX/sCL+27HfFpYEHpaoLJHPH4z/ht/u8SWR601qlA/JjjEarQ/Qy -EuG8hYF7u83aiHcuuwaSVMk2i2OZGesbhbepElPNCwxHsJnUNQaUQsDhPeP5rP2s -MkYl+x2MXooPwDEAEQEAAbQtbXljcm9mdC1kZXNrdG9wLXJlcG8gPGFrZS5mb3Jz -bHVuZEBnbWFpbC5jb20+iQHUBBMBCgA+AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B -AheAFiEEj6zH4HNa/2SUFHneZ3azPalyr9EFAmN9KkUFCRaRMwQACgkQZ3azPaly -r9Heigv/VQwxJP3hCUlqBppvYQnqRdLUTgwKN/JDnjAiqLvE0fKgoYXJ0Af0tf1q -oyOdhwurNhKHHV+hWqclt8MRXb/BoExFG2h5Au3NNh6HYVBNWSdUbac6fgEuoaBS -QHc/7K5NTAVGtSsFucAPQ0Kg73TVqvmoUUW/sJXyMgt1yiIwp17rt9zhPX2pJ7xG -pHncg579+8yitTygcPFxrtmAB3ApPGDdysJhnM/7gB9YwnqN9I60/ROvrfrg6eOk -r0skKoPPQnAZHkoK/anpOlCdQao+8PX8k6s6Nm79tNfLg2HjCPciL/k+jr59D2eI -2iFq7L1rvLhqxKTEoB5m1guOnnLUWlHyvoQmox/idUVYDYGyHs7L9nkfGEkaVnKq -q9g0QpIME2/tDMNDXvsFB9b94nYFJodBbmNh1lvrSuUeqT+YFB05psANcObD1oOf -w1idiadf433Ho7eOz41JruGKQvz/LAZEKwVMPVyh0G31jNvp+GdNkV5KqehGpp6o -wgf4+q7R -=VQMc ------END PGP PUBLIC KEY BLOCK----- diff --git a/tests/mimic/mimic.list b/tests/mimic/mimic.list deleted file mode 100644 index 55d3616..0000000 --- a/tests/mimic/mimic.list +++ /dev/null @@ -1 +0,0 @@ -deb http://forslund.github.io/mycroft-desktop-repo bionic main From c92df138d918ad65031e4cdded3a5b2dab97b725 Mon Sep 17 00:00:00 2001 From: Mike Gray Date: Mon, 1 Jan 2024 15:13:50 -0600 Subject: [PATCH 3/3] fix logging test --- tests/main_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/main_test.py b/tests/main_test.py index 6c37558..8994e01 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -24,7 +24,7 @@ def create_instance(config): def test_initialization(tts_instance): assert tts_instance.audio_ext == "wav" assert tts_instance.validator is not None - assert isinstance(tts_instance.log, LOG) + assert tts_instance.log is LOG assert tts_instance.host is None assert tts_instance.v2 is True assert tts_instance.verify_ssl is True