diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index a81b18bab..3e3a7bcb6 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -127,4 +127,13 @@ jobs: uses: actions/upload-artifact@v2 with: name: skills-module-test-results - path: tests/skills-module-test-results.xml \ No newline at end of file + path: tests/skills-module-test-results.xml + + - name: Test Resource Resolution + run: | + pytest test/test_res.py --doctest-modules --junitxml=tests/resource-resolution-test-results.xml + - name: Upload Resource Resolution test results + uses: actions/upload-artifact@v2 + with: + name: resource-resolution-test-results + path: tests/resource-resolution-test-results.xml \ No newline at end of file diff --git a/.github/workflows/update_skills_image.yml b/.github/workflows/update_skills_image.yml new file mode 100644 index 000000000..4b8330db8 --- /dev/null +++ b/.github/workflows/update_skills_image.yml @@ -0,0 +1,42 @@ +name: Update Skills Image +on: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/neon_skills + +jobs: + update_default_skills_image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + ref: ${{ github.ref }} + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for default_skills Docker + id: default_skills_meta + uses: docker/metadata-action@v2 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-default_skills + tags: | + type=ref,event=branch + - name: Build and push default_skills Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.default_skills_meta.outputs.tags }} + labels: ${{ steps.default_skills_meta.outputs.labels }} + target: default_skills \ No newline at end of file diff --git a/docker/.env b/docker/.env index 5238106aa..ec184a959 100644 --- a/docker/.env +++ b/docker/.env @@ -1 +1,2 @@ -NEON_SKILLS_DIR=./ \ No newline at end of file +NEON_SKILLS_DIR=./ +NEON_CONFIG_PATH=./ \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 486cd4165..a93752e96 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -6,7 +6,7 @@ volumes: driver_opts: type: config o: bind - device: ./ + device: ${NEON_CONFIG_PATH} services: neon-messagebus: container_name: neon-messagebus diff --git a/docker/ngi_local_conf.yml b/docker/ngi_local_conf.yml index e845a2c29..92f102fb6 100755 --- a/docker/ngi_local_conf.yml +++ b/docker/ngi_local_conf.yml @@ -121,7 +121,7 @@ session: ttl: 180 tts: module: coqui - package_spec: neon-tts-plugin-coqui~=0.1 + package_spec: neon-tts-plugin-coqui~=0.2 mozilla: {request_url: http://0.0.0.0:5002/api/tts?} mozilla_remote: {api_url: https://mtts.2022.us/api/tts} mimic: {voice: ap} diff --git a/neon_core/__init__.py b/neon_core/__init__.py index c0994d1d1..ce1125f8e 100644 --- a/neon_core/__init__.py +++ b/neon_core/__init__.py @@ -28,7 +28,8 @@ import sys -from neon_core.config import init_config, get_core_version +from neon_core.config import init_config, get_core_version, \ + setup_resolve_resource_file from os.path import dirname @@ -36,7 +37,7 @@ sys.path.append(NEON_ROOT_PATH) init_config() CORE_VERSION_STR = get_core_version() - +setup_resolve_resource_file() from neon_core.skills import NeonSkill, NeonFallbackSkill from neon_core.skills.intent_service import NeonIntentService diff --git a/neon_core/config.py b/neon_core/config.py index 8f3d55180..41fc91a79 100644 --- a/neon_core/config.py +++ b/neon_core/config.py @@ -138,3 +138,21 @@ def get_core_version() -> str: "(NeonGecko)" mycroft.version.CORE_VERSION_STR = core_version_str return core_version_str + + +def setup_resolve_resource_file(): + """ + Override default resolve_resource_file to include resources in neon-core. + Priority: neon-utils, neon-core, ~/.local/share/neon, ~/.neon, mycroft-core + """ + from neon_utils.file_utils import resolve_neon_resource_file + from mycroft.util.file_utils import resolve_resource_file + + def patched_resolve_resource_file(res_name): + resource = resolve_neon_resource_file(res_name) or \ + resolve_resource_file(res_name) + return resource + + import mycroft.util + mycroft.util.file_utils.resolve_resource_file = \ + patched_resolve_resource_file diff --git a/neon_core/processing_modules/text/modules/translator/__init__.py b/neon_core/processing_modules/text/modules/translator/__init__.py index 2f30bf755..de102d4e2 100644 --- a/neon_core/processing_modules/text/modules/translator/__init__.py +++ b/neon_core/processing_modules/text/modules/translator/__init__.py @@ -51,9 +51,11 @@ def parse(self, utterances, lang=None): else: LOG.debug(f"Detected language: {detected_lang}") if detected_lang != self.language_config["internal"].split("-")[0]: - utterances[idx] = self.translator.translate(original, - self.language_config["internal"], lang.split('-', 1)[0] - or detected_lang) + utterances[idx] = self.translator.translate( + original, + self.language_config["internal"], + lang.split('-', 1)[0] or detected_lang) + LOG.info(f"Translated utterance to: {utterances[idx]}") # add language metadata to context metadata += [{ "source_lang": lang or self.language_config['internal'], diff --git a/neon_core/res/snd/beep.wav b/neon_core/res/snd/beep.wav new file mode 100644 index 000000000..725a86ba0 Binary files /dev/null and b/neon_core/res/snd/beep.wav differ diff --git a/neon_core/res/snd/loaded.wav b/neon_core/res/snd/loaded.wav new file mode 100644 index 000000000..c4bb88243 Binary files /dev/null and b/neon_core/res/snd/loaded.wav differ diff --git a/neon_core/res/text/en-uk/neon.voc b/neon_core/res/text/en-uk/neon.voc new file mode 100644 index 000000000..3879aca6d --- /dev/null +++ b/neon_core/res/text/en-uk/neon.voc @@ -0,0 +1,4 @@ +neon +leon +nyan +me on \ No newline at end of file diff --git a/neon_core/res/text/ru-ru/neon.voc b/neon_core/res/text/ru-ru/neon.voc new file mode 100644 index 000000000..3426292e6 --- /dev/null +++ b/neon_core/res/text/ru-ru/neon.voc @@ -0,0 +1 @@ +неон \ No newline at end of file diff --git a/neon_core/res/text/ua-uk/neon.voc b/neon_core/res/text/ua-uk/neon.voc new file mode 100644 index 000000000..3426292e6 --- /dev/null +++ b/neon_core/res/text/ua-uk/neon.voc @@ -0,0 +1 @@ +неон \ No newline at end of file diff --git a/neon_core/skills/intent_service.py b/neon_core/skills/intent_service.py index ed76ded29..342a7ef42 100644 --- a/neon_core/skills/intent_service.py +++ b/neon_core/skills/intent_service.py @@ -29,6 +29,8 @@ import time import wave +from mycroft.skills.intent_services import ConverseService + from neon_core.configuration import Configuration from neon_core.language import get_lang_config from neon_core.processing_modules.text import TextParsersService @@ -37,6 +39,7 @@ from neon_utils.message_utils import get_message_user from neon_utils.metrics_utils import Stopwatch from neon_utils.log_utils import LOG +from neon_utils.user_utils import apply_local_user_profile_updates from neon_utils.configuration_utils import get_neon_device_type,\ get_neon_user_config from ovos_utils.json_helper import merge_dict @@ -60,10 +63,13 @@ class NeonIntentService(IntentService): def __init__(self, bus): super().__init__(bus) + self.converse = NeonConverseService(bus) self.config = Configuration.get().get('context', {}) self.language_config = get_lang_config() - self.default_user = get_neon_user_config() + # Initialize default user to inject into incoming messages + self._default_user = get_neon_user_config() + self._default_user['user']['username'] = "local" set_default_lang(self.language_config["internal"]) @@ -79,6 +85,15 @@ def __init__(self, bus): except Exception as e: LOG.exception(e) + self.bus.on("neon.profile_update", self.handle_profile_update) + + def handle_profile_update(self, message): + updated_profile = message.data.get("profile") + if updated_profile["user"]["username"] == \ + self._default_user["user"]["username"]: + apply_local_user_profile_updates(updated_profile, + self._default_user) + def shutdown(self): self.parser_service.shutdown() @@ -146,9 +161,9 @@ def handle_utterance(self, message): # Ensure user profile data is present if "user_profiles" not in message.context: - message.context["user_profiles"] = [self.default_user.content] + message.context["user_profiles"] = [self._default_user.content] message.context["username"] = \ - self.default_user.content["user"]["username"] + self._default_user.content["user"]["username"] # Make sure there is a `transcribed` timestamp if not message.context["timing"].get("transcribed"): @@ -184,11 +199,21 @@ def handle_utterance(self, message): # TODO: Try the original lang and fallback to translation # If translated, make sure message.data['lang'] is updated - if message.context.get("translation_data", - [{}])[0].get("was_translated"): + if message.context.get("translation_data") and \ + message.context.get("translation_data")[0].get( + "was_translated"): message.data["lang"] = self.language_config["internal"] # now pass our modified message to Mycroft # TODO: Consider how to implement 'and' parsing and converse DM super().handle_utterance(message) except Exception as err: LOG.exception(err) + + +class NeonConverseService(ConverseService): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _collect_converse_skills(self): + # TODO: Patching bug in ovos-core 0.0.3 + return self.get_active_skills() diff --git a/neon_core/util/qml_file_server.py b/neon_core/util/qml_file_server.py index bf9035e66..6b8748fb8 100644 --- a/neon_core/util/qml_file_server.py +++ b/neon_core/util/qml_file_server.py @@ -34,7 +34,7 @@ from os.path import isdir, join, dirname from threading import Thread, Event -_HTTP_SERVER = None +_HTTP_SERVER: socketserver.TCPServer = None class QmlFileHandler(http.server.SimpleHTTPRequestHandler): @@ -58,13 +58,13 @@ def start_qml_http_server(skills_dir: str, port: int = 8000): served_skills_dir = join(qml_dir, "skills") served_system_dir = join(qml_dir, "system") - if os.path.exists(served_skills_dir): + if os.path.exists(served_skills_dir) or os.path.islink(served_skills_dir): os.remove(served_skills_dir) - if os.path.exists(served_system_dir): + if os.path.exists(served_system_dir) or os.path.islink(served_skills_dir): os.remove(served_system_dir) - os.symlink(skills_dir, join(qml_dir, "skills")) - os.symlink(system_dir, join(qml_dir, "system")) + os.symlink(skills_dir, served_skills_dir) + os.symlink(system_dir, served_system_dir) started_event = Event() http_daemon = Thread(target=_initialize_http_server, args=(started_event, qml_dir, port), diff --git a/neon_core/util/skill_utils.py b/neon_core/util/skill_utils.py index c300517ef..eabfdeec7 100644 --- a/neon_core/util/skill_utils.py +++ b/neon_core/util/skill_utils.py @@ -28,13 +28,15 @@ import json import os.path +import re +from copy import copy import requests -from os import listdir +from os import listdir, makedirs from tempfile import mkdtemp from shutil import rmtree -from os.path import expanduser, join, isdir +from os.path import expanduser, join, isdir, dirname, isfile from ovos_skills_manager.requirements import install_system_deps, pip_install from ovos_skills_manager.skill_entry import SkillEntry @@ -70,6 +72,43 @@ def get_neon_skills_data(skill_meta_repository: str = "https://github.com/neonge return skills_data +def _write_pip_constraints_to_file(output_file: str = None): + """ + Writes out a constraints file for OSM to use to prevent broken dependencies + :param output_file: path to constraints file to write + """ + from neon_utils.packaging_utils import get_package_dependencies + + output_file = output_file or '/etc/mycroft/constraints.txt' + if not isdir(dirname(output_file)): + makedirs(dirname(output_file)) + + with open(output_file, 'w+') as f: + constraints = get_package_dependencies("neon-core") + for c in copy(constraints): + try: + constraint = re.split('[^a-zA-Z0-9_-]', c, 1)[0] or c + constraints.extend(get_package_dependencies(constraint)) + except ModuleNotFoundError: + LOG.warning(f"Ignoring uninstalled dependency: {constraint}") + constraints = [f'{c.split("[")[0]}{c.split("]")[1]}' if '[' in c + else c for c in constraints if '@' not in c] + LOG.debug(f"Got package constraints: {constraints}") + f.write('\n'.join(constraints)) + LOG.info(f"Wrote core constraints to file: {output_file}") + + +def set_osm_constraints_file(constraints_file: str): + """ + Sets the DEFAULT_CONSTRAINTS param for OVOS Skills Manager. + :param constraints_file: path to valid constraints file for neon-core + """ + if not constraints_file: + raise ValueError("constraints_file not defined") + import ovos_skills_manager.requirements + ovos_skills_manager.requirements.DEFAULT_CONSTRAINTS = constraints_file + + def install_skills_from_list(skills_to_install: list, config: dict = None): """ Installs the passed list of skill URLs @@ -85,6 +124,13 @@ def install_skills_from_list(skills_to_install: list, config: dict = None): token_set = True set_github_token(config["neon_token"]) LOG.info(f"Added token to request headers: {config.get('neon_token')}") + try: + _write_pip_constraints_to_file() + except PermissionError: + from ovos_utils.xdg_utils import xdg_data_home + constraints_file = join(xdg_data_home(), "neon", "constraints.txt") + _write_pip_constraints_to_file(constraints_file) + set_osm_constraints_file(constraints_file) for url in skills_to_install: try: normalized_url = normalize_github_url(url) @@ -104,6 +150,10 @@ def install_skills_from_list(skills_to_install: list, config: dict = None): if not os.path.isdir(os.path.join(skill_dir, entry.uuid)): LOG.error(f"Failed to install: " f"{os.path.join(skill_dir, entry.uuid)}") + if entry.download(skill_dir): + LOG.info(f"Downloaded failed skill: {entry.uuid}") + else: + LOG.error(f"Failed to download: {entry.uuid}") else: LOG.info(f"Installed {url} to {skill_dir}") except Exception as e: @@ -149,10 +199,16 @@ def _install_skill_dependencies(skill: SkillEntry): :param skill: Skill to install dependencies for """ sys_deps = skill.requirements.get("system") - requirements = skill.requirements.get("python") + requirements = copy(skill.requirements.get("python")) if sys_deps: install_system_deps(sys_deps) if requirements: + invalid = [r for r in requirements if r.startswith("lingua-franca")] + if any(invalid): + for dep in invalid: + LOG.warning(f"{dep} is not valid under this core" + f" and will be ignored") + requirements.remove(dep) pip_install(requirements) LOG.info(f"Installed dependencies for {skill.skill_folder}") @@ -172,17 +228,19 @@ def install_local_skills(local_skills_dir: str = "/skills") -> list: raise ValueError(f"{local_skills_dir} is not a valid directory") installed_skills = list() for skill in listdir(local_skills_dir): - if not isdir(skill): - pass + skill_dir = join(local_skills_dir, skill) + if not isdir(skill_dir): + continue + if not isfile(join(skill_dir, "__init__.py")): + continue LOG.debug(f"Attempting installation of {skill}") try: - entry = SkillEntry.from_directory(join(local_skills_dir, skill), - github_token) + entry = SkillEntry.from_directory(skill_dir, github_token) _install_skill_dependencies(entry) installed_skills.append(skill) except Exception as e: LOG.error(f"Exception while installing {skill}") - LOG.error(e) + LOG.exception(e) if local_skills_dir not in \ get_neon_skills_config().get("extra_directories", []): LOG.error(f"{local_skills_dir} not found in configuration") diff --git a/requirements/core_modules.txt b/requirements/core_modules.txt index 3cf98184d..2161de99a 100644 --- a/requirements/core_modules.txt +++ b/requirements/core_modules.txt @@ -1,10 +1,8 @@ -ovos-core[audio-backend,PHAL,tts,skills_lgpl,gui,bus]~=0.0.3 -SpeechRecognition~=3.8.1 -PyAudio~=0.2.11 +ovos-core[all]~=0.0.3 ovos-ww-plugin-pocketsphinx~=0.1.2 # neon core modules -neon_messagebus~=0.1 -neon_speech~=1.0 -neon_audio~=1.0 +neon_messagebus~=0.1,>=0.1.1 +neon_speech~=1.0,>=1.1.0 +neon_audio~=1.0,>=1.0.1 neon_gui~=0.1 \ No newline at end of file diff --git a/requirements/docker.txt b/requirements/docker.txt index b67161711..e69de29bb 100644 --- a/requirements/docker.txt +++ b/requirements/docker.txt @@ -1,3 +0,0 @@ -# These are just patching default skill dependencies -ifaddr~=0.1 -pyjokes \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e55410fe2..eec2940aa 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,13 +1,13 @@ # mycroft ovos-core[skills_lgpl]~=0.0.3 -# TODO: Update to stable version - # utils -neon-utils~=0.16 +neon-utils~=0.17,>=0.17.3 ovos_utils~=0.0.20 ovos-skills-manager~=0.0.10 ovos-plugin-manager~=0.0.16 +psutil~=5.6 + # default plugins neon-lang-plugin-libretranslate~=0.1,>=0.1.2 diff --git a/test/local_skills/skill-ovos-homescreen/requirements.txt b/test/local_skills/skill-ovos-homescreen/requirements.txt index 61587cdb5..ebd28dd44 100755 --- a/test/local_skills/skill-ovos-homescreen/requirements.txt +++ b/test/local_skills/skill-ovos-homescreen/requirements.txt @@ -1,3 +1,2 @@ requests>=2.20.0,<2.26.0 -pillow==7.1.2 ovos_utils>=0.0.6 \ No newline at end of file diff --git a/test/test_res.py b/test/test_res.py new file mode 100644 index 000000000..be71c090b --- /dev/null +++ b/test/test_res.py @@ -0,0 +1,73 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import sys +import unittest + +from os.path import join, isfile + +sys.path.append(os.path.dirname(os.path.dirname(__file__))) + + +class ResourceTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + pass + + def test_resolve_resource_file(self): + import neon_core # Ensure neon_core is imported + from mycroft.util.file_utils import resolve_resource_file + for file in ("hey-neon.pb", "hey-neon.pb.params"): + self.assertTrue(isfile(resolve_resource_file(join("precise_models", + file)))) + for file in ("acknowledge.mp3", "beep.wav", "loaded.wav", + "start_listening.wav"): + self.assertTrue(isfile(resolve_resource_file(join("snd", + file)))) + for file in ("neon_logo.png", "SYSTEM_AnimatedImageFrame.qml", + "SYSTEM_HtmlFrame.qml", "SYSTEM_TextFrame.qml", + "SYSTEM_UrlFrame.qml", "WebViewHtmlFrame.qml", + "WebViewUrlFrame.qml"): + self.assertTrue(isfile(resolve_resource_file(join("ui", + file)))) + for file in ("cancel.voc", "i didn't catch that.dialog", + "neon.voc", "no.voc", "not.loaded.dialog", + "not connected to the internet.dialog", + "phonetic_spellings.txt", "skill.error.dialog", + "skills updated.dialog", "yes.voc"): + self.assertTrue(isfile(resolve_resource_file(join("text", "en-us", + file)))) + + for lang in ("en-au", "en-us", "en-uk", "ua-uk", "ru-ru"): + self.assertTrue(isfile(resolve_resource_file(join("text", lang, + "neon.voc")))) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_skill_utils.py b/test/test_skill_utils.py index aac9843f2..8239d9344 100644 --- a/test/test_skill_utils.py +++ b/test/test_skill_utils.py @@ -31,7 +31,7 @@ import shutil import sys import unittest -from copy import deepcopy +from copy import deepcopy, copy from importlib import reload from mock.mock import Mock @@ -68,25 +68,29 @@ def tearDown(self) -> None: def test_get_remote_entries(self): from neon_core.util.skill_utils import get_remote_entries - from ovos_skills_manager.session import set_github_token, clear_github_token + from ovos_skills_manager.session import set_github_token,\ + clear_github_token set_github_token(SKILL_CONFIG["neon_token"]) skills_list = get_remote_entries(SKILL_CONFIG["default_skills"]) clear_github_token() self.assertIsInstance(skills_list, list) self.assertTrue(len(skills_list) > 0) - self.assertTrue(all(skill.startswith("https://github.com") for skill in skills_list)) + self.assertTrue(all(skill.startswith("https://github.com") + for skill in skills_list)) def test_install_skills_from_list_no_auth(self): from neon_core.util.skill_utils import install_skills_from_list install_skills_from_list(TEST_SKILLS_NO_AUTH, SKILL_CONFIG) - skill_dirs = [d for d in os.listdir(SKILL_DIR) if os.path.isdir(os.path.join(SKILL_DIR, d))] + skill_dirs = [d for d in os.listdir(SKILL_DIR) + if os.path.isdir(os.path.join(SKILL_DIR, d))] self.assertEqual(len(skill_dirs), len(TEST_SKILLS_NO_AUTH)) self.assertIn("alerts.neon.neongeckocom", skill_dirs) def test_install_skills_from_list_with_auth(self): from neon_core.util.skill_utils import install_skills_from_list install_skills_from_list(TEST_SKILLS_WITH_AUTH, SKILL_CONFIG) - skill_dirs = [d for d in os.listdir(SKILL_DIR) if os.path.isdir(os.path.join(SKILL_DIR, d))] + skill_dirs = [d for d in os.listdir(SKILL_DIR) + if os.path.isdir(os.path.join(SKILL_DIR, d))] self.assertEqual(len(skill_dirs), len(TEST_SKILLS_WITH_AUTH)) self.assertIn("i-like-brands.neon.neongeckocom", skill_dirs) @@ -127,7 +131,6 @@ def test_install_local_skills(self): self.assertEqual(installed, os.listdir(local_skills_dir)) self.assertEqual(num_installed, install_deps.call_count) - def test_install_skill_dependencies(self): # Patch dependency installation import ovos_skills_manager.requirements @@ -157,6 +160,44 @@ def test_install_skill_dependencies(self): install_system_deps.assert_called_with( entry.json["requirements"]["system"]) + invalid_dep_json = entry.json + invalid_dep_json['requirements']['python'].append('lingua-franca') + invalid_entry = SkillEntry.from_json(invalid_dep_json) + _install_skill_dependencies(invalid_entry) + valid_deps = invalid_entry.json['requirements']['python'] + valid_deps.remove('lingua-franca') + pip_install.assert_called_with(valid_deps) + + def test_write_pip_constraints_to_file(self): + from neon_core.util.skill_utils import _write_pip_constraints_to_file + from neon_utils.packaging_utils import get_package_dependencies + real_deps = get_package_dependencies("neon-core") + real_deps = [f'{c.split("[")[0]}{c.split("]")[1]}' if '[' in c + else c for c in real_deps if '@' not in c] + test_outfile = os.path.join(os.path.dirname(__file__), + "constraints.txt") + _write_pip_constraints_to_file(test_outfile) + with open(test_outfile) as f: + read_deps = f.read().split('\n') + self.assertTrue(all((d in read_deps for d in real_deps))) + + try: + _write_pip_constraints_to_file() + self.assertTrue(os.path.isfile("/etc/mycroft/constraints.txt")) + with open("/etc/mycroft/constraints.txt") as f: + deps = f.read().split('\n') + self.assertTrue(all((d in deps for d in real_deps))) + except Exception as e: + self.assertIsInstance(e, PermissionError) + os.remove(test_outfile) + + def test_set_osm_constraints_file(self): + import ovos_skills_manager.requirements + from neon_core.util.skill_utils import set_osm_constraints_file + set_osm_constraints_file(__file__) + self.assertEqual(ovos_skills_manager.requirements.DEFAULT_CONSTRAINTS, + __file__) + if __name__ == '__main__': unittest.main() diff --git a/version.py b/version.py index a5cb5db4b..bc0a5766f 100644 --- a/version.py +++ b/version.py @@ -26,4 +26,4 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -__version__ = "22.05.0" +__version__ = "22.05.1"