From 24368b172aad0a74d84f12a2c7734ffe844b72b5 Mon Sep 17 00:00:00 2001 From: miro Date: Wed, 18 Sep 2024 21:00:22 +0100 Subject: [PATCH 1/3] refactor!:drop mycroft --- ovos_workshop/__init__.py | 5 - ovos_workshop/decorators/compat.py | 44 -- ovos_workshop/decorators/ocp.py | 2 +- ovos_workshop/settings.py | 3 +- ovos_workshop/skill_launcher.py | 14 +- ovos_workshop/skills/__init__.py | 7 +- ovos_workshop/skills/active.py | 4 +- ovos_workshop/skills/auto_translatable.py | 6 +- ovos_workshop/skills/base.py | 17 - ovos_workshop/skills/common_play.py | 2 +- ovos_workshop/skills/common_query_skill.py | 16 - ovos_workshop/skills/decorators/__init__.py | 4 - ovos_workshop/skills/decorators/converse.py | 4 - .../skills/decorators/fallback_handler.py | 4 - ovos_workshop/skills/decorators/killable.py | 4 - ovos_workshop/skills/decorators/layers.py | 4 - ovos_workshop/skills/decorators/ocp.py | 4 - ovos_workshop/skills/fallback.py | 245 +------ ovos_workshop/skills/mycroft_skill.py | 312 -------- ovos_workshop/skills/ovos.py | 271 +------ setup.py | 1 - test/unittests/skills/test_active.py | 6 +- .../skills/test_auto_translatable.py | 8 +- test/unittests/skills/test_base.py | 44 +- .../skills/test_common_query_skill.py | 4 - test/unittests/skills/test_fallback_skill.py | 172 +---- .../skills/test_idle_display_skill.py | 4 +- .../skills/test_mycroft_skill/__init__.py | 0 .../intent_file/vocab/en-us/test.intent | 0 .../intent_file/vocab/en-us/test_ent.entity | 0 .../locale/en-us/turn_off2_test.voc | 1 - .../locale/en-us/turn_off_test.voc | 3 - .../skills/test_mycroft_skill/mocks.py | 76 -- .../test_mycroft_skill/test_mycroft_skill.py | 673 ------------------ .../test_mycroft_skill_get_response.py | 302 -------- .../test_mycroft_skill/test_skill/__init__.py | 23 - .../dialog/en-us/what do you want.dialog | 1 - .../in-dialog/dialog/en-us/good_things.list | 3 - .../in-dialog/dialog/en-us/named_things.value | 4 - .../in-dialog/dialog/en-us/test.template | 1 - .../in-locale/locale/de-de/good_things.list | 3 - .../in-locale/locale/de-de/named_things.value | 3 - .../in-locale/locale/de-de/test.template | 1 - .../in-locale/locale/en-us/good_things.list | 3 - .../in-locale/locale/en-us/named_things.value | 4 - .../in-locale/locale/en-us/not_in_german.list | 3 - .../in-locale/locale/en-us/test.template | 1 - test/unittests/skills/test_ovos.py | 6 +- test/unittests/test_abstract_app.py | 19 +- test/unittests/test_imports.py | 12 - test/unittests/test_skill.py | 62 -- test/unittests/test_skill_launcher.py | 2 - 52 files changed, 65 insertions(+), 2352 deletions(-) delete mode 100644 ovos_workshop/decorators/compat.py delete mode 100644 ovos_workshop/skills/base.py delete mode 100644 ovos_workshop/skills/decorators/__init__.py delete mode 100644 ovos_workshop/skills/decorators/converse.py delete mode 100644 ovos_workshop/skills/decorators/fallback_handler.py delete mode 100644 ovos_workshop/skills/decorators/killable.py delete mode 100644 ovos_workshop/skills/decorators/layers.py delete mode 100644 ovos_workshop/skills/decorators/ocp.py delete mode 100644 ovos_workshop/skills/mycroft_skill.py delete mode 100644 test/unittests/skills/test_mycroft_skill/__init__.py delete mode 100644 test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test.intent delete mode 100644 test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test_ent.entity delete mode 100644 test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off2_test.voc delete mode 100644 test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off_test.voc delete mode 100644 test/unittests/skills/test_mycroft_skill/mocks.py delete mode 100644 test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py delete mode 100644 test/unittests/skills/test_mycroft_skill/test_mycroft_skill_get_response.py delete mode 100644 test/unittests/skills/test_mycroft_skill/test_skill/__init__.py delete mode 100644 test/unittests/skills/test_mycroft_skill/test_skill/dialog/en-us/what do you want.dialog delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/good_things.list delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/named_things.value delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/test.template delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/good_things.list delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/named_things.value delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/test.template delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/good_things.list delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/named_things.value delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/not_in_german.list delete mode 100644 test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/test.template delete mode 100644 test/unittests/test_imports.py diff --git a/ovos_workshop/__init__.py b/ovos_workshop/__init__.py index aad16f17..e69de29b 100644 --- a/ovos_workshop/__init__.py +++ b/ovos_workshop/__init__.py @@ -1,5 +0,0 @@ -from ovos_workshop.app import OVOSAbstractApplication -from ovos_workshop.decorators import * -from ovos_workshop.decorators.killable import killable_event, \ - AbortEvent, AbortQuestion -from ovos_workshop.decorators.layers import IntentLayers diff --git a/ovos_workshop/decorators/compat.py b/ovos_workshop/decorators/compat.py deleted file mode 100644 index a9952b91..00000000 --- a/ovos_workshop/decorators/compat.py +++ /dev/null @@ -1,44 +0,0 @@ -from functools import wraps - - -def backwards_compat(classic_core=None, pre_008=None, no_core=None): - """ - Decorator to run a different method if specific ovos-core versions are detected - """ - - def backwards_compat_decorator(func): - is_classic = False - is_old = False - is_standalone = True - try: - from mycroft.version import CORE_VERSION_STR # all classic mycroft and ovos versions - is_classic = True - is_standalone = False - - try: - from ovos_core.version import OVOS_VERSION_MINOR # ovos-core >= 0.0.8 - is_classic = False - except ImportError: - is_old = True - try: - from mycroft.version import OVOS_VERSION_MINOR # ovos-core <= 0.0.7 - is_classic = False - except: - is_standalone = True - - except: - is_standalone = True - - @wraps(func) - def func_wrapper(*args, **kwargs): - if is_classic and callable(classic_core): - return classic_core(*args, **kwargs) - if is_old and callable(pre_008): - return pre_008(*args, **kwargs) - if is_standalone and callable(no_core): - return no_core(*args, **kwargs) - return func(*args, **kwargs) - - return func_wrapper - - return backwards_compat_decorator diff --git a/ovos_workshop/decorators/ocp.py b/ovos_workshop/decorators/ocp.py index 45999382..a8e60f05 100644 --- a/ovos_workshop/decorators/ocp.py +++ b/ovos_workshop/decorators/ocp.py @@ -1,4 +1,4 @@ -from ovos_workshop.backwards_compat import MediaType, PlayerState, MediaState, MatchConfidence, \ +from ovos_utils.ocp import MediaType, PlayerState, MediaState, MatchConfidence, \ PlaybackType, PlaybackMode, LoopState, TrackState diff --git a/ovos_workshop/settings.py b/ovos_workshop/settings.py index 52988452..ca826806 100644 --- a/ovos_workshop/settings.py +++ b/ovos_workshop/settings.py @@ -16,9 +16,8 @@ class SkillSettingsManager: def __init__(self, skill): - from ovos_workshop.skills.base import BaseSkill self.download_timer: Optional[Timer] = None - self.skill: BaseSkill = skill + self.skill = skill self.api = DeviceApi() self.remote_settings = \ RemoteSkillSettings(self.skill_id, diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index e67277b6..0d55fd0d 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -18,23 +18,19 @@ from ovos_workshop.skills.active import ActiveSkill from ovos_workshop.skills.auto_translatable import UniversalSkill, UniversalFallback -from ovos_workshop.skills.base import BaseSkill from ovos_workshop.skills.common_play import OVOSCommonPlaybackSkill from ovos_workshop.skills.common_query_skill import CommonQuerySkill from ovos_workshop.skills.fallback import FallbackSkill -from ovos_workshop.skills.mycroft_skill import MycroftSkill -from ovos_workshop.skills.ovos import OVOSSkill, OVOSFallbackSkill +from ovos_workshop.skills.ovos import OVOSSkill SKILL_BASE_CLASSES = [ - BaseSkill, MycroftSkill, OVOSSkill, OVOSFallbackSkill, - OVOSCommonPlaybackSkill, OVOSFallbackSkill, CommonQuerySkill, ActiveSkill, + OVOSSkill, OVOSCommonPlaybackSkill, CommonQuerySkill, ActiveSkill, FallbackSkill, UniversalSkill, UniversalFallback ] SKILL_MAIN_MODULE = '__init__.py' - def remove_submodule_refs(module_name: str): """ Ensure submodules are reloaded by removing the refs from sys.modules. @@ -85,13 +81,13 @@ def load_skill_module(path: str, skill_id: str) -> ModuleType: def get_skill_class(skill_module: ModuleType) -> Optional[callable]: """ - Find MycroftSkill based class in skill module. + Find OVOSSkill based class in skill module. Arguments: skill_module (module): module to search for Skill class Returns: - (MycroftSkill): Found subclass of MycroftSkill or None. + (OVOSSkill): Found subclass of OVOSSkill or None. """ if not skill_module: raise ValueError("Expected module and got None") @@ -156,7 +152,7 @@ def __init__(self, bus: MessageBusClient, self._loaded = None self.load_attempted = False self.last_loaded = 0 - self.instance: Optional[BaseSkill] = None + self.instance: Optional[OVOSSkill] = None self.active = True self._watchdog = None self.config = Configuration() diff --git a/ovos_workshop/skills/__init__.py b/ovos_workshop/skills/__init__.py index eaacb75c..c8a93e22 100644 --- a/ovos_workshop/skills/__init__.py +++ b/ovos_workshop/skills/__init__.py @@ -1,5 +1,6 @@ from ovos_workshop.decorators.layers import IntentLayers -from ovos_workshop.skills.ovos import OVOSSkill, OVOSFallbackSkill +from ovos_workshop.skills.ovos import OVOSSkill from ovos_workshop.skills.idle_display_skill import IdleDisplaySkill -from ovos_workshop.skills.mycroft_skill import MycroftSkill - +from ovos_workshop.skills.fallback import FallbackSkill +from ovos_workshop.skills.common_query_skill import CommonQuerySkill +from ovos_workshop.skills.common_play import OVOSCommonPlaybackSkill diff --git a/ovos_workshop/skills/active.py b/ovos_workshop/skills/active.py index 325cd22c..7ba019de 100644 --- a/ovos_workshop/skills/active.py +++ b/ovos_workshop/skills/active.py @@ -6,12 +6,12 @@ def bind(self, bus): super(ActiveSkill, self).bind(bus) if bus: """ insert skill in active skill list on load """ - self.make_active() + self.activate() def handle_skill_deactivated(self, message=None): """ skill is always in active skill list, ie, converse is always called """ - self.make_active() + self.activate() diff --git a/ovos_workshop/skills/auto_translatable.py b/ovos_workshop/skills/auto_translatable.py index 6c2cb2be..bcbbe4fe 100644 --- a/ovos_workshop/skills/auto_translatable.py +++ b/ovos_workshop/skills/auto_translatable.py @@ -7,7 +7,7 @@ from ovos_utils.log import LOG from ovos_workshop.resource_files import SkillResources from ovos_workshop.skills.common_query_skill import CommonQuerySkill -from ovos_workshop.skills.fallback import FallbackSkillV2 +from ovos_workshop.skills.fallback import FallbackSkill from ovos_workshop.skills.ovos import OVOSSkill @@ -311,7 +311,7 @@ def _handle_converse_request(self, message: Message): super()._handle_converse_request(message) -class UniversalFallback(UniversalSkill, FallbackSkillV2): +class UniversalFallback(UniversalSkill, FallbackSkill): """ Fallback Skill that auto translates input/output from any language. @@ -373,7 +373,7 @@ def register_fallback(self, handler, priority: int): - `create_universal_fallback_handler` method for creating universal fallback handlers. """ handler = self.create_universal_fallback_handler(handler) - FallbackSkillV2.register_fallback(self, handler, priority) + FallbackSkill.register_fallback(self, handler, priority) class UniversalCommonQuerySkill(UniversalSkill, CommonQuerySkill, ABC): diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py deleted file mode 100644 index 4a8195c4..00000000 --- a/ovos_workshop/skills/base.py +++ /dev/null @@ -1,17 +0,0 @@ -# DEPRECATED - merged into OVOSSkill, imports for compat onlu -from ovos_workshop.skills.ovos import OVOSSkill, simple_trace, is_classic_core, SkillGUI -from ovos_utils.log import log_deprecation -from ovos_utils.process_utils import RuntimeRequirements - - -# backwards compat alias -class SkillNetworkRequirements(RuntimeRequirements): - def __init__(self, *args, **kwargs): - log_deprecation("Replace with " - "`ovos_utils.process_utils.RuntimeRequirements`", - "0.1.0") - super().__init__(*args, **kwargs) - - -BaseSkill = OVOSSkill # backwards compat - diff --git a/ovos_workshop/skills/common_play.py b/ovos_workshop/skills/common_play.py index c299b41f..0e41a4da 100644 --- a/ovos_workshop/skills/common_play.py +++ b/ovos_workshop/skills/common_play.py @@ -13,7 +13,7 @@ # backwards compat imports, do not delete, skills import from here from ovos_workshop.decorators.ocp import ocp_play, ocp_next, ocp_pause, ocp_resume, ocp_search, \ ocp_previous, ocp_featured_media -from ovos_workshop.backwards_compat import MediaType, MediaState, MatchConfidence, \ +from ovos_utils.ocp import MediaType, MediaState, MatchConfidence, \ PlaybackType, PlaybackMode, PlayerState, LoopState, TrackState, Playlist, PluginStream, MediaEntry diff --git a/ovos_workshop/skills/common_query_skill.py b/ovos_workshop/skills/common_query_skill.py index fb77fc50..f7659d6e 100644 --- a/ovos_workshop/skills/common_query_skill.py +++ b/ovos_workshop/skills/common_query_skill.py @@ -19,7 +19,6 @@ from ovos_utils.file_utils import resolve_resource_file from ovos_utils.log import LOG, log_deprecation -from ovos_workshop.decorators.compat import backwards_compat from ovos_workshop.skills.ovos import OVOSSkill @@ -233,21 +232,6 @@ def __calc_confidence(self, match: str, phrase: str, level: CQSMatchLevel, return confidence - def __handle_query_classic(self, message): - """ - does not perform self.speak, < 0.0.8 this is done by core itself - """ - if message.data["skill_id"] != self.skill_id: - # Not for this skill! - return - self.activate() - phrase = message.data["phrase"] - data = message.data.get("callback_data") or {} - # Invoke derived class to provide playback data - self.CQS_action(phrase, data) - - @backwards_compat(classic_core=__handle_query_classic, - pre_008=__handle_query_classic) def __handle_query_action(self, message: Message): """ If this skill's response was spoken to the user, this method is called. diff --git a/ovos_workshop/skills/decorators/__init__.py b/ovos_workshop/skills/decorators/__init__.py deleted file mode 100644 index 18588273..00000000 --- a/ovos_workshop/skills/decorators/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from ovos_workshop.decorators import * -# backwards compat import -from ovos_utils.log import log_deprecation -log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/converse.py b/ovos_workshop/skills/decorators/converse.py deleted file mode 100644 index b75b6224..00000000 --- a/ovos_workshop/skills/decorators/converse.py +++ /dev/null @@ -1,4 +0,0 @@ -from ovos_workshop.decorators.converse import * -# backwards compat import -from ovos_utils.log import log_deprecation -log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/fallback_handler.py b/ovos_workshop/skills/decorators/fallback_handler.py deleted file mode 100644 index 003e994f..00000000 --- a/ovos_workshop/skills/decorators/fallback_handler.py +++ /dev/null @@ -1,4 +0,0 @@ -from ovos_workshop.decorators.fallback_handler import * -# backwards compat import -from ovos_utils.log import log_deprecation -log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/killable.py b/ovos_workshop/skills/decorators/killable.py deleted file mode 100644 index d7de18d7..00000000 --- a/ovos_workshop/skills/decorators/killable.py +++ /dev/null @@ -1,4 +0,0 @@ -from ovos_workshop.decorators.killable import * -# backwards compat import -from ovos_utils.log import log_deprecation -log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/layers.py b/ovos_workshop/skills/decorators/layers.py deleted file mode 100644 index 2497168f..00000000 --- a/ovos_workshop/skills/decorators/layers.py +++ /dev/null @@ -1,4 +0,0 @@ -from ovos_workshop.decorators.layers import * -# backwards compat import -from ovos_utils.log import log_deprecation -log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/ocp.py b/ovos_workshop/skills/decorators/ocp.py deleted file mode 100644 index 32a563c5..00000000 --- a/ovos_workshop/skills/decorators/ocp.py +++ /dev/null @@ -1,4 +0,0 @@ -from ovos_workshop.decorators.ocp import * -# backwards compat import -from ovos_utils.log import log_deprecation -log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index 8001984b..67a7adae 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -13,7 +13,7 @@ # limitations under the License. import operator -from typing import Optional, List, Callable, Tuple +from typing import Optional, List from ovos_bus_client import MessageBusClient from ovos_bus_client.message import Message, dig_for_message @@ -24,25 +24,10 @@ from ovos_utils.skills import get_non_properties from ovos_workshop.decorators.killable import AbortEvent, killable_event -from ovos_workshop.decorators.compat import backwards_compat -from ovos_workshop.permissions import FallbackMode from ovos_workshop.skills.ovos import OVOSSkill -class _MutableFallback(type(OVOSSkill)): - """ To override isinstance checks we need to use a metaclass """ - - def __instancecheck__(self, instance): - if isinstance(instance, _MetaFB): - return True - return super().__instancecheck__(instance) - - -class _MetaFB(OVOSSkill): - pass - - -class FallbackSkill(_MetaFB, metaclass=_MutableFallback): +class FallbackSkill(OVOSSkill): """ Fallbacks come into play when no skill matches an Adapt or closely with a Padatious intent. All Fallback skills work together to give them a @@ -63,45 +48,8 @@ class FallbackSkill(_MetaFB, metaclass=_MutableFallback): A Fallback can either observe or consume an utterance. A consumed utterance will not be seen by any other Fallback handlers. """ - - def __new__classic__(cls, *args, **kwargs): - if cls is FallbackSkill: - # direct instantiation of class, dynamic wizardry for unittests - # return V2 as expected, V1 will eventually be dropped - return FallbackSkillV2(*args, **kwargs) - cls.__bases__ = (FallbackSkillV1, FallbackSkill, _MetaFB) - return super().__new__(cls) - - @backwards_compat(classic_core=__new__classic__, - pre_008=__new__classic__) - def __new__(cls, *args, **kwargs): - if cls is FallbackSkill: - # direct instantiation of class, dynamic wizardry for unittests - # return V2 as expected, V1 will eventually be dropped - return FallbackSkillV2(*args, **kwargs) - cls.__bases__ = (FallbackSkillV2, FallbackSkill, _MetaFB) - return super().__new__(cls) - - @classmethod - def make_intent_failure_handler(cls, bus: MessageBusClient): - """ - backwards compat, old version of ovos-core call this method to bind - the bus to old class - """ - return FallbackSkillV1.make_intent_failure_handler(bus) - - -class FallbackSkillV1(_MetaFB, metaclass=_MutableFallback): - fallback_handlers = {} - wrapper_map: List[Tuple[callable, callable]] = [] # [(handler, wrapper)] - - def __init__(self, name=None, bus=None, use_settings=True, **kwargs): - # list of fallback handlers registered by this instance - self.instance_fallback_handlers = [] - # "skill_id": priority (int) overrides - self.fallback_config = Configuration()["skills"].get("fallbacks", {}) - - super().__init__(name=name, bus=bus, **kwargs) + # "skill_id": priority (int) overrides + fallback_config = Configuration().get("skills", {}).get("fallbacks", {}) @classmethod def make_intent_failure_handler(cls, bus: MessageBusClient): @@ -158,152 +106,6 @@ def handler(message): return handler - @staticmethod - def _report_timing(ident: str, system: str, timing: Stopwatch, - additional_data: Optional[dict] = None): - """ - Create standardized message for reporting timing. - @param ident: identifier for user interaction - @param system: identifier for system being timed - @param timing: Stopwatch object with recorded timing - @param additional_data: Optional dict data to include with metric - """ - # TODO: Move to an imported function and deprecate this - try: - from mycroft.metrics import report_timing - report_timing(ident, system, timing, additional_data) - except ImportError: - pass - - @classmethod - def _register_fallback(cls, handler: callable, wrapper: callable, - priority: int): - """ - Add a fallback handler to the class - @param handler: original handler method used for reference - @param wrapper: wrapped handler used to handle fallback requests - @param priority: fallback priority - """ - while priority in cls.fallback_handlers: - priority += 1 - - cls.fallback_handlers[priority] = wrapper - cls.wrapper_map.append((handler, wrapper)) - - def register_fallback(self, handler: Callable[[Message], None], - priority: int): - """ - core >= 0.8.0 makes skill active - """ - opmode = self.fallback_config.get("fallback_mode", - FallbackMode.ACCEPT_ALL) - priority_overrides = self.fallback_config.get("fallback_priorities", {}) - fallback_blacklist = self.fallback_config.get("fallback_blacklist", []) - fallback_whitelist = self.fallback_config.get("fallback_whitelist", []) - - if opmode == FallbackMode.BLACKLIST and \ - self.skill_id in fallback_blacklist: - return - if opmode == FallbackMode.WHITELIST and \ - self.skill_id not in fallback_whitelist: - return - - # check if .conf is overriding the priority for this skill - priority = priority_overrides.get(self.skill_id, priority) - - def wrapper(*args, **kwargs): - if handler(*args, **kwargs): - self.activate() - return True - return False - - self.instance_fallback_handlers.append(handler) - self._register_fallback(handler, wrapper, priority) - - @classmethod - def _remove_registered_handler(cls, wrapper_to_del: callable) -> bool: - """ - Remove a registered wrapper. - @param wrapper_to_del: wrapped handler to be removed - @return: True if one or more handlers were removed, otherwise False. - """ - found_handler = False - for priority, handler in list(cls.fallback_handlers.items()): - if handler == wrapper_to_del: - found_handler = True - del cls.fallback_handlers[priority] - - if not found_handler: - LOG.warning('No fallback matching {}'.format(wrapper_to_del)) - return found_handler - - @classmethod - def remove_fallback(cls, handler_to_del: callable) -> bool: - """ - Remove a fallback handler. - @param handler_to_del: registered callback handler (or wrapped handler) - @return: True if at least one handler was removed, otherwise False - """ - # Find wrapper from handler or wrapper - wrapper_to_del = None - for h, w in cls.wrapper_map: - if handler_to_del in (h, w): - handler_to_del = h - wrapper_to_del = w - break - - if wrapper_to_del: - cls.wrapper_map.remove((handler_to_del, wrapper_to_del)) - remove_ok = cls._remove_registered_handler(wrapper_to_del) - else: - LOG.warning('Could not find matching fallback handler') - remove_ok = False - return remove_ok - - def remove_instance_handlers(self): - """ - Remove all fallback handlers registered by the fallback skill. - """ - LOG.info('Removing all handlers...') - while len(self.instance_fallback_handlers): - handler = self.instance_fallback_handlers.pop() - self.remove_fallback(handler) - - def default_shutdown(self): - """ - Remove all registered handlers and perform skill shutdown. - """ - self.remove_instance_handlers() - super().default_shutdown() - - def _register_decorated(self): - """ - Register all decorated fallback handlers. - - Looks for all functions that have been marked by a decorator - and read the fallback priority from them. The handlers aren't the - only decorators used. Skip properties as calling getattr on them - executes the code which may have unintended side effects. - """ - super()._register_decorated() - for attr_name in get_non_properties(self): - method = getattr(self, attr_name) - if hasattr(method, 'fallback_priority'): - self.register_fallback(method, method.fallback_priority) - - -class FallbackSkillV2(_MetaFB, metaclass=_MutableFallback): - # "skill_id": priority (int) overrides - fallback_config = Configuration().get("skills", {}).get("fallbacks", {}) - - @classmethod - def make_intent_failure_handler(cls, bus: MessageBusClient): - """ - backwards compat, old version of ovos-core call this method to bind - the bus to old class - """ - return FallbackSkillV1.make_intent_failure_handler(bus) - def __init__(self, bus=None, skill_id="", **kwargs): self._fallback_handlers = [] super().__init__(bus=bus, skill_id=skill_id, **kwargs) @@ -340,10 +142,9 @@ def _register_system_event_handlers(self): fallback skill. """ super()._register_system_event_handlers() - self.add_event('ovos.skills.fallback.ping', self._handle_fallback_ack, + self.add_event('ovos.skills.fallback.ping', self._handle_fallback_ack, speak_errors=False) + self.add_event(f"ovos.skills.fallback.{self.skill_id}.request", self._handle_fallback_request, speak_errors=False) - self.add_event(f"ovos.skills.fallback.{self.skill_id}.request", - self._handle_fallback_request, speak_errors=False) def _handle_fallback_ack(self, message: Message): """ @@ -351,18 +152,16 @@ def _handle_fallback_ack(self, message: Message): """ utts = message.data.get("utterances", []) lang = message.data.get("lang") - self.bus.emit(message.reply( - "ovos.skills.fallback.pong", - data={"skill_id": self.skill_id, - "can_handle": self.can_answer(utts, lang)}, - context={"skill_id": self.skill_id})) + self.bus.emit(message.reply("ovos.skills.fallback.pong", + data={"skill_id": self.skill_id, + "can_handle": self.can_answer(utts, lang)}, + context={"skill_id": self.skill_id})) def _on_timeout(self): """_handle_fallback_request timed out and was forcefully killed by ovos-core""" message = dig_for_message() - self.bus.emit(message.forward( - f"ovos.skills.fallback.{self.skill_id}.killed", - data={"error": "timed out"})) + self.bus.emit(message.forward(f"ovos.skills.fallback.{self.skill_id}.killed", + data={"error": "timed out"})) @killable_event("ovos.skills.fallback.force_timeout", callback=_on_timeout, check_skill_id=True) @@ -404,26 +203,6 @@ def _handle_fallback_request(self, message: Message): self.bus.emit(message.forward("ovos.utterance.handled", {"handler": handler_name})) - def _old_register_fallback(self, handler: callable, priority: int): - """ core < 0.0.8 """ - - LOG.info(f"registering fallback handler -> " - f"ovos.skills.fallback.{self.skill_id}") - - def wrapper(*args, **kwargs): - if handler(*args, **kwargs): - self.activate() - return True - return False - - self._fallback_handlers.append((priority, wrapper)) - self.bus.on(f"ovos.skills.fallback.{self.skill_id}", wrapper) - # register with fallback service - self.bus.emit(Message("ovos.skills.fallback.register", - {"skill_id": self.skill_id, - "priority": self.priority})) - - @backwards_compat(classic_core=_old_register_fallback, pre_008=_old_register_fallback) def register_fallback(self, handler: callable, priority: int): """ Register a fallback handler and add a messagebus handler to call it on diff --git a/ovos_workshop/skills/mycroft_skill.py b/ovos_workshop/skills/mycroft_skill.py deleted file mode 100644 index 17cdb133..00000000 --- a/ovos_workshop/skills/mycroft_skill.py +++ /dev/null @@ -1,312 +0,0 @@ -# Copyright 2019 Mycroft AI Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import inspect -import shutil -from os.path import join, exists, dirname - -from json_database import JsonStorage -from ovos_bus_client import MessageBusClient, Message -from ovos_utils.log import LOG, log_deprecation, deprecated - -from ovos_workshop.decorators.compat import backwards_compat -from ovos_workshop.filesystem import FileSystemAccess -from ovos_workshop.skills.ovos import OVOSSkill, is_classic_core, _OVOSSkillMetaclass - - -class _SkillMetaclass(_OVOSSkillMetaclass): - """ - This metaclass ensures we can load skills like regular python objects. - mycroft-core required a skill loader helper class, which created the skill - and then finished object init. This meant skill_id and bus were not - available in init method, so mycroft introduced a method named `initialize` - that was called after `skill_id` and `bus` were defined. - - To make skills pythonic and standalone, this metaclass is used to auto init - old skills and help in migrating to new standards. - - To override isinstance checks we also need to use a metaclass - - TODO: remove compat ovos-core 0.2.0 at the latest, including MycroftSkill class - """ - - def __instancecheck_classic__(self, instance): - # instance imported from vanilla mycroft - from mycroft.skills import MycroftSkill as _CoreSkill - from ovos_workshop.app import OVOSAbstractApplication - if issubclass(instance.__class__, _CoreSkill): - return True - return issubclass(instance.__class__, OVOSSkill) and \ - not issubclass(instance.__class__, OVOSAbstractApplication) - - @backwards_compat(classic_core=__instancecheck_classic__) - def __instancecheck__(self, instance): - from ovos_workshop.app import OVOSAbstractApplication - if issubclass(instance.__class__, OVOSAbstractApplication): - return False - return super().__instancecheck__(instance) or \ - issubclass(instance.__class__, OVOSSkill) - - def __call__(cls, *args, **kwargs): - from ovos_bus_client import MessageBusClient - from ovos_utils.messagebus import FakeBus - bus = None - skill_id = None - - if "bus" not in kwargs: - for a in args: - if isinstance(a, MessageBusClient) or isinstance(a, FakeBus): - bus = a - LOG.warning( - f"bus should be a kwarg, guessing {a} is the bus") - break - else: - LOG.warning("skill initialized without bus!! this is legacy " - "behaviour and requires you to call skill.bind(bus)" - " or skill._startup(skill_id, bus)\n" - "bus will be required starting on ovos-core 0.1.0") - return super().__call__(*args, **kwargs) - - if "skill_id" in kwargs: - skill_id = kwargs.pop("skill_id") - if "bus" in kwargs: - bus = kwargs.pop("bus") - if not skill_id: - LOG.warning(f"skill_id should be a kwarg, please update " - f"{cls.__name__}") - if args and isinstance(args[0], str): - a = args[0] - if a[0].isupper(): - # in mycroft name is CamelCase by convention, not skill_id - LOG.debug(f"ambiguous skill_id, ignoring {a} as it appears " - f"to be a CamelCase name") - else: - LOG.warning(f"ambiguous skill_id, assuming positional " - f"argument: {a}") - skill_id = a - - if not skill_id: - LOG.error("skill initialized without skill_id!! this is legacy " - "behaviour. skill_id will be required starting on ovos-core 0.1.0") - # by convention skill_id is the folder name - # usually repo.author - skill_id = dirname(inspect.getfile(cls)).split("/")[-1] - LOG.warning(f"missing skill_id, assuming folder name convention: {skill_id}") - - try: - # skill follows latest best practices, - # accepts kwargs and does its own init - return super().__call__(skill_id=skill_id, bus=bus, **kwargs) - except TypeError: - LOG.warning(f"Legacy skill signature detected for {skill_id};" - f" attempting to init skill manually, self.bus and " - f"self.skill_id will only be available in " - f"self.initialize. `__init__` method needs to accept " - f"`skill_id` and `bus` to resolve this.") - - # skill did not update its init method, init it manually - # NOTE: no try/except because all skills must accept this initialization - # this is what skill loader does internally - skill = super().__call__(*args, **kwargs) - skill._startup(bus, skill_id) - return skill - - -class MycroftSkill(OVOSSkill, metaclass=_SkillMetaclass): - """ - Base class for mycroft skills providing common behaviour and parameters - to all Skill implementations. This class is kept for backwards-compat. It is - recommended to implement `OVOSSkill` to properly implement new methods. - """ - - @deprecated("mycroft-core has been deprecated, please move to ovos-core", "0.1.0") - def __classic_init__(self, name: str = None, bus: MessageBusClient = None, - use_settings: bool = True, *args, **kwargs): - """ - Create a MycroftSkill object. - @param name: DEPRECATED skill_name - @param bus: MessageBusClient to bind to skill - @param use_settings: DEPRECATED option to disable settings sync - """ - super().__init__(name=name, bus=bus, *args, **kwargs) - - self._initial_settings = {} - self.settings_write_path = None - self.settings_manager = None - - # old kludge from fallback skills, unused according to grep - if use_settings is False: - log_deprecation("use_settings has been deprecated! " - "skill settings are always enabled", "0.1.0") - - self.settings_write_path = self.root_dir - - @backwards_compat(classic_core=__classic_init__) - @deprecated("MycroftSkill class has been deprecated, please subclass from OVOSSkill", "0.1.0") - def __init__(self, name: str = None, bus: MessageBusClient = None, - use_settings: bool = True, *args, **kwargs): - """ - Create a MycroftSkill object. - @param name: DEPRECATED skill_name - @param bus: MessageBusClient to bind to skill - @param use_settings: DEPRECATED option to disable settings sync - """ - super().__init__(name=name, bus=bus, *args, **kwargs) - self._initial_settings = {} - self.settings_write_path = None - self.settings_manager = None - - @property - def file_system(self) -> FileSystemAccess: - """ - Get an object that provides managed access to a local Filesystem. - """ - if not self._file_system: - self._file_system = FileSystemAccess(join('skills', self.name)) - LOG.warning(f"with MycroftSkill self.file_system does not respect self.skill_id, path: {self._file_system.path}") - return self._file_system - - @file_system.setter - def file_system(self, val): - LOG.warning("you are not supposed to override self.file_system, expect breakage!") - self._file_system = val - - @property - def settings(self) -> dict: - """ - Get settings specific to this skill - """ - if self._settings is None: - self._settings = JsonStorage(self.settings_write_path, - disable_lock=True) - LOG.warning(f"with MycroftSkill self.settings may not respect self.skill_id, path: {self._settings.path}") - return self._settings - - @settings.setter - def settings(self, val): - LOG.warning("Skills are not supposed to override self.settings, expect breakage! Set individual dict keys instead") - self._settings = val - - def _init_settings(self): - """ - Set up skill settings. Defines settings in the specified file path, - handles any settings passed to skill init, and starts watching the - settings file for changes. - """ - self.log.debug(f"initializing skill settings for {self.skill_id}") - - if self._settings is None: - self._settings = JsonStorage(self.settings_write_path, - disable_lock=True) - - # starting on ovos-core 0.0.8 a bus event is emitted - # all settings.json files are monitored for changes in ovos-core - self.add_event("ovos.skills.settings_changed", self._handle_settings_changed) - - if self._monitor_own_settings: - self._start_filewatcher() - - def __init_settings_manager_classic(self): - super()._init_settings_manager() - from mycroft.skills.settings import SettingsMetaUploader - self._settings_meta = SettingsMetaUploader(self.root_dir, - self.skill_id) - - def __init_settings_manager_standalone(self): - super()._init_settings_manager() - - @backwards_compat(classic_core=__init_settings_manager_classic, - no_core=__init_settings_manager_standalone) - def _init_settings_manager(self): - super()._init_settings_manager() - # backwards compat - self.settings_meta has been deprecated - # in favor of settings manager - from mycroft.deprecated.skills.settings import SettingsMetaUploader - self._settings_meta = SettingsMetaUploader(self.root_dir, - self.skill_id) - - def __init_settings_classic(self): - # migrate settings if needed - if not exists(self.settings_path) and \ - exists(self._old_settings_path): - LOG.warning("Found skill settings at pre-xdg location, " - "migrating!") - shutil.copy(self._old_settings_path, self.settings_path) - LOG.info(f"{self._old_settings_path} moved to " - f"{self.settings_path}") - super()._init_settings() - - @backwards_compat(classic_core=__init_settings_classic) - def _init_settings(self): - """Setup skill settings.""" - super()._init_settings() - - # patched due to functional (internal) differences under mycroft-core - def __on_end_classic(self, message: Message, handler_info: str, - skill_data: dict, is_intent: bool = False): - # mycroft-core style settings - if self.settings != self._initial_settings: - try: - from mycroft.skills.settings import save_settings - save_settings(self.settings_write_path, self.settings) - self._initial_settings = dict(self.settings) - except Exception as e: - LOG.exception(f"Failed to save skill settings: {e}") - if handler_info: - msg_type = handler_info + '.complete' - message.context["skill_id"] = self.skill_id - self.bus.emit(message.forward(msg_type, skill_data)) - - @backwards_compat(classic_core=__on_end_classic) - def _on_event_end(self, message: Message, handler_info: str, - skill_data: dict, is_intent: bool = False): - """ - Store settings and indicate that the skill handler has completed - """ - return super()._on_event_end(message, handler_info, skill_data, is_intent) - - # refactored - backwards compat + log warnings - @property - def settings_meta(self): - log_deprecation("Use `self.settings_manager`", "0.1.0") - return self._settings_meta - - # refactored - backwards compat + log warnings - @settings_meta.setter - def settings_meta(self, val): - log_deprecation("Use `self.settings_manager`", "0.1.0") - self._settings_meta = val - - # internal - deprecated under ovos-core - @property - def _old_settings_path(self): - log_deprecation("This path is no longer used", "0.1.0") - old_dir = self.config_core.get("data_dir") or "/opt/mycroft" - old_folder = self.config_core.get("skills", {}).get("msm", {}) \ - .get("directory") or "skills" - return join(old_dir, old_folder, self.skill_id, 'settings.json') - - # patched due to functional (internal) differences under mycroft-core - def __get_settings_pclassic(self): - if self.settings_write_path and \ - self.settings_write_path != self.root_dir: - log_deprecation("`self.settings_write_path` is no longer used", - "0.1.0") - return join(self.settings_write_path, 'settings.json') - return super().settings_path - - @property - @backwards_compat(classic_core=__get_settings_pclassic) - def settings_path(self): - return super().settings_path diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 61117452..09ecfb5d 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -5,7 +5,6 @@ import sys import time import traceback -from abc import ABCMeta from copy import copy from hashlib import md5 from inspect import signature @@ -17,7 +16,6 @@ from json_database import JsonStorage from lingua_franca.format import pronounce_number, join_list from lingua_franca.parse import yes_or_no, extract_number -from ovos_backend_client.api import EmailApi, MetricsApi from ovos_bus_client import MessageBusClient from ovos_bus_client.apis.enclosure import EnclosureAPI from ovos_bus_client.apis.gui import GUIInterface @@ -32,17 +30,15 @@ from ovos_utils.dialog import get_dialog, MustacheDialogRenderer from ovos_utils.events import EventContainer, EventSchedulerInterface from ovos_utils.events import get_handler_name, create_wrapper -from ovos_utils.file_utils import FileWatcher, resolve_resource_file +from ovos_utils.file_utils import FileWatcher from ovos_utils.gui import get_ui_directories from ovos_utils.json_helper import merge_dict -from ovos_utils.log import LOG, log_deprecation, deprecated +from ovos_utils.log import LOG from ovos_utils.parse import match_one from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills import get_non_properties -from ovos_utils.sound import play_audio from padacioso import IntentContainer -from ovos_workshop.decorators.compat import backwards_compat from ovos_workshop.decorators.killable import AbortEvent, killable_event, \ AbortQuestion from ovos_workshop.decorators.layers import IntentLayers @@ -55,11 +51,6 @@ from ovos_workshop.settings import SkillSettingsManager -@deprecated("OVOS no longer supports running under classic mycroft-core! this function always returns False", "1.0.0") -def is_classic_core(): - return False - - def simple_trace(stack_trace: List[str]) -> str: """ Generate a simplified traceback. @@ -74,24 +65,7 @@ def simple_trace(stack_trace: List[str]) -> str: return tb -class _OVOSSkillMetaclass(ABCMeta): - """ - To override isinstance checks - """ - - def __instancecheck_classic__(self, instance): - # instance imported from vanilla mycroft - from mycroft.skills import MycroftSkill as _CoreSkill - if issubclass(instance.__class__, _CoreSkill): - return True - return issubclass(instance.__class__, OVOSSkill) - - @backwards_compat(classic_core=__instancecheck_classic__) - def __instancecheck__(self, instance): - return super().__instancecheck__(instance) - - -class OVOSSkill(metaclass=_OVOSSkillMetaclass): +class OVOSSkill: """ Base class for OpenVoiceOS skills providing common behaviour and parameters to all Skill implementations. @@ -970,32 +944,6 @@ def _upload_settings(self): # update settingsmeta file on disk self.settings_manager.save_meta() - def __bind_classic(self, bus): - self._bus = bus - self.events.set_bus(bus) - self.intent_service.set_bus(bus) - self.event_scheduler.set_bus(bus) - self._enclosure.set_bus(bus) - self._register_system_event_handlers() - self._register_public_api() - self.intent_layers.bind(self) - self.audio_service = OCPInterface(self.bus) - self.private_settings = PrivateSettings(self.skill_id) - - log_deprecation("Support for mycroft-core is deprecated", "0.1.0") - # inject ovos exclusive features in vanilla mycroft-core - # if possible - # limited support for missing skill deactivated event - try: - from ovos_utils.intents.converse import ConverseTracker - ConverseTracker.connect_bus(self.bus) # pull/1468 - except ImportError: - pass # deprecated in utils 0.1.0 - self.add_event("converse.skill.deactivated", - self._handle_skill_deactivated, - speak_errors=False) - - @backwards_compat(classic_core=__bind_classic) def bind(self, bus: MessageBusClient): """ Register MessageBusClient with skill. @@ -1701,33 +1649,6 @@ def speak_dialog(self, key: str, data: Optional[dict] = None, ) self.speak(key, expect_response, wait, {}) - def _play_audio_old(self, filename: str, instant: bool = False, - wait: Union[bool, int] = False): - """ compat for ovos-core <= 0.0.7 """ - if instant: - LOG.warning("self.play_audio instant flag requires ovos-core >= 0.0.8, " - "falling back to local skill playback") - play_audio(filename).wait() - else: - message = dig_for_message() or Message("") - self.bus.emit(message.forward("mycroft.audio.queue", - {"filename": filename, # TODO - deprecate filename in ovos-audio - "uri": filename # new namespace - })) - if wait: - timeout = 30 if isinstance(wait, bool) else wait - sess = SessionManager.get(message) - sess.is_speaking = True - SessionManager.wait_while_speaking(timeout, sess) - - def _play_audio_classic(self, filename: str, instant: bool = False, - wait: Union[bool, int] = False): - """ compat for classic mycroft-core """ - LOG.warning("self.play_audio requires ovos-core >= 0.0.4, " - "falling back to local skill playback") - play_audio(filename).wait() - - @backwards_compat(pre_008=_play_audio_old, classic_core=_play_audio_classic) def play_audio(self, filename: str, instant: bool = False, wait: Union[bool, int] = False): """ @@ -1765,53 +1686,6 @@ def play_audio(self, filename: str, instant: bool = False, sess.is_speaking = True SessionManager.wait_while_speaking(timeout, sess) - def __get_response_v1(self, session=None): - """Helper to get a response from the user - - NOTE: There is a race condition here. There is a small amount of - time between the end of the device speaking and the converse method - being overridden in this method. If an utterance is injected during - this time, the wrong converse method is executed. The condition is - hidden during normal use due to the amount of time it takes a user - to speak a response. The condition is revealed when an automated - process injects an utterance quicker than this method can flip the - converse methods. - - Returns: - str: user's response or None on a timeout - """ - session = session or SessionManager.get() - - def converse(utterances, lang=None): - self.__responses[session.session_id] = utterances - converse.response = utterances[0] if utterances else None - converse.finished = True - return True - - # install a temporary conversation handler - self.activate() - converse.finished = False - converse.response = None - self.converse = converse - - # 10 for listener, 5 for SST, then timeout - ans = [] - # NOTE: a threading.Event is not used otherwise we can't raise the - # AbortEvent exception to kill the thread - # this is for compat with killable_intents decorators - start = time.time() - while time.time() - start <= 15 and not ans: - ans = self.__responses[session.session_id] - time.sleep(0.1) - if ans is None: - # aborted externally (if None) - self.log.debug("get_response aborted") - converse.finished = True - break - - self.converse = self._original_converse - return ans - def __handle_get_response(self, message): """ Handle the response message to a previous get_response / speak call @@ -1828,7 +1702,6 @@ def __handle_get_response(self, message): # received get_response self.__responses[sess2.session_id] = utterances - @backwards_compat(classic_core=__get_response_v1, pre_008=__get_response_v1) def __get_response(self, session: Session): """Helper to get a response from the user @@ -2089,21 +1962,6 @@ def _real_wait_response(self, is_cancel, validator, on_fail, num_retries, else: self.bus.emit(message.reply('mycroft.mic.listen')) - def __acknowledge_classic(self): - """ - Acknowledge a successful request. - - This method plays a sound to acknowledge a request that does not - require a verbal response. This is intended to provide simple feedback - to the user that their request was handled successfully. - """ - audio_file = self.config_core.get('sounds', {}).get('acknowledge', - 'snd/acknowledge.mp3') - audio_file = resolve_resource_file(audio_file) - if audio_file: - return play_audio(audio_file) - - @backwards_compat(classic_core=__acknowledge_classic) def acknowledge(self): """ Acknowledge a successful request. @@ -2554,34 +2412,6 @@ def remove_cross_skill_context(self, context: str): {'context': context})) # killable_events support - def __send_stop_signal_classic(self, stop_event: Optional[str] = None): - """ - Notify services to stop current execution - @param stop_event: optional `stop` event name to forward - """ - waiter = Event() - msg = dig_for_message() or Message("mycroft.stop") - # stop event execution - if stop_event: - self.bus.emit(msg.forward(stop_event)) - - # stop TTS - self.bus.emit(msg.forward("mycroft.audio.speech.stop")) - - # Tell ovos-core to stop recording (not in mycroft-core) - self.bus.emit(msg.forward('recognizer_loop:record_stop')) - - # NOTE: mycroft does not have an event to stop recording - # this attempts to force a stop by sending silence to end STT step - self.bus.emit(Message('mycroft.mic.mute')) - waiter.wait(1.5) # the silence from muting should make STT stop recording - self.bus.emit(Message('mycroft.mic.unmute')) - - # TODO: register TTS events to track state instead of guessing - waiter.wait(0.5) # if TTS had not yet started - self.bus.emit(msg.forward("mycroft.audio.speech.stop")) - - @backwards_compat(classic_core=__send_stop_signal_classic) def send_stop_signal(self, stop_event: Optional[str] = None): """ Notify services to stop current execution @@ -2603,34 +2433,6 @@ def send_stop_signal(self, stop_event: Optional[str] = None): waiter.wait(0.5) # if TTS had not yet started self.bus.emit(msg.forward("mycroft.audio.speech.stop")) - # below deprecated and marked for removal - @deprecated("use MetricsApi().report_metric", "0.1.0") - def report_metric(self, name: str, data: dict): - """ - Report a skill metric to the Mycroft servers. - - Args: - name (str): Name of metric. Must use only letters and hyphens - data (dict): JSON dictionary to report. Must be valid JSON - """ - try: - if Configuration().get('opt_in', False): - MetricsApi().report_metric(name, data) - except Exception as e: - self.log.error(f'Metric couldn\'t be uploaded, due to a network error ({e})') - - @deprecated("use EmailApi().send_email", "0.1.0") - def send_email(self, title: str, body: str): - """ - Send an email to the registered user's email. - - Args: - title (str): Title of email - body (str): HTML body of email. This supports - simple HTML like bold and italics - """ - EmailApi().send_email(title, body, self.skill_id) - @classproperty def network_requirements(self) -> RuntimeRequirements: LOG.warning("network_requirements renamed to runtime_requirements, " @@ -2652,59 +2454,6 @@ def voc_match_cache(self, val): if isinstance(val, dict): self._voc_cache = val - # below only for api compat with MycroftSkill class - @deprecated("Use `self.resources.render_dialog`", "0.1.0") - def translate(self, text: str, data: Optional[dict] = None): - """ - Deprecated method for translating a dialog file. - use self.resources.render_dialog(text, data) instead - """ - return self.resources.render_dialog(text, data) - - @deprecated("Use `self.resources.load_named_value_file`", "0.1.0") - def translate_namedvalues(self, name: str, delim: str = ','): - """ - Deprecated method for translating a name/value file. - use self.resources.load_named_value_filetext, data) instead - """ - return self.resources.load_named_value_file(name, delim) - - @deprecated("Use `self.resources.load_list_file`", "0.1.0") - def translate_list(self, list_name: str, data: Optional[dict] = None): - """ - Deprecated method for translating a list. - use self.resources.load_list_file(text, data) instead - """ - return self.resources.load_list_file(list_name, data) - - @deprecated("Use `self.resources.load_template_file`", "0.1.0") - def translate_template(self, template_name: str, - data: Optional[dict] = None): - """ - Deprecated method for translating a template file - use self.resources.template_file(text, data) instead - """ - return self.resources.load_template_file(template_name, data) - - @deprecated("Use `self.resources.load_dialog_files`", "0.1.0") - def init_dialog(self, root_directory: Optional[str] = None): - """ - DEPRECATED: use load_dialog_files instead - """ - self.load_dialog_files(root_directory) - - @deprecated("Use `activate`", "0.1.0") - def make_active(self): - """ - Bump skill to active_skill list in intent_service. - - This enables converse method to be called even without skill being - used in last 5 minutes. - - deprecated: use self.activate() instead - """ - self.activate() - class SkillGUI(GUIInterface): def __init__(self, skill: OVOSSkill): @@ -2719,17 +2468,3 @@ def __init__(self, skill: OVOSSkill): GUIInterface.__init__(self, skill_id=skill_id, bus=bus, config=config, ui_directories=ui_directories) - @property - @deprecated("`skill` should not be referenced directly", "0.1.0") - def skill(self): - return self._skill - - -# backwards compat alias, no functional difference -class OVOSFallbackSkill(OVOSSkill): - def __new__(cls, *args, **kwargs): - log_deprecation("Implement " - "`ovos_workshop.skills.fallback.FallbackSkill`", - "0.1.0") - from ovos_workshop.skills.fallback import FallbackSkill - return FallbackSkill(*args, **kwargs) diff --git a/setup.py b/setup.py index 03b7e232..f91dca4a 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,6 @@ def get_description(): version=get_version(), packages=['ovos_workshop', 'ovos_workshop.skills', - 'ovos_workshop.skills.decorators', 'ovos_workshop.decorators'], install_requires=required("requirements/requirements.txt"), extras_require={ diff --git a/test/unittests/skills/test_active.py b/test/unittests/skills/test_active.py index 9811a0f3..dc47e71f 100644 --- a/test/unittests/skills/test_active.py +++ b/test/unittests/skills/test_active.py @@ -2,14 +2,14 @@ from unittest.mock import Mock from ovos_utils.messagebus import FakeBus +from ovos_workshop.skills.ovos import OVOSSkill from ovos_workshop.skills.active import ActiveSkill -from ovos_workshop.skills.base import BaseSkill class ActiveSkillExample(ActiveSkill): active = Mock() - def make_active(self): + def activate(self, *args): self.active() ActiveSkill.activate(self) @@ -17,7 +17,7 @@ def make_active(self): class TestActiveSkill(unittest.TestCase): def test_skill(self): skill = ActiveSkillExample() - self.assertIsInstance(skill, BaseSkill) + self.assertIsInstance(skill, OVOSSkill) skill.bind(FakeBus()) skill.active.assert_called_once() self.assertTrue(skill.active) diff --git a/test/unittests/skills/test_auto_translatable.py b/test/unittests/skills/test_auto_translatable.py index 6b0271cc..c1dd9fed 100644 --- a/test/unittests/skills/test_auto_translatable.py +++ b/test/unittests/skills/test_auto_translatable.py @@ -2,7 +2,7 @@ from ovos_workshop.skills.common_query_skill import CommonQuerySkill from ovos_workshop.skills.fallback import FallbackSkill -from ovos_workshop.skills.base import BaseSkill +from ovos_workshop.skills.ovos import OVOSSkill class TestUniversalSkill(unittest.TestCase): @@ -11,7 +11,7 @@ class TestUniversalSkill(unittest.TestCase): def test_00_init(self): self.assertIsInstance(self.test_skill, self.UniversalSkill) - self.assertIsInstance(self.test_skill, BaseSkill) + self.assertIsInstance(self.test_skill, OVOSSkill) # TODO: Test other class methods @@ -22,7 +22,7 @@ class TestUniversalFallbackSkill(unittest.TestCase): def test_00_init(self): self.assertIsInstance(self.test_skill, self.UniversalFallback) - self.assertIsInstance(self.test_skill, BaseSkill) + self.assertIsInstance(self.test_skill, OVOSSkill) self.assertIsInstance(self.test_skill, FallbackSkill) # TODO: Test other class methods @@ -39,7 +39,7 @@ def CQS_match_query_phrase(self, phrase): def test_00_init(self): self.assertIsInstance(self.test_skill, self.UniversalCommonQuerySkill) - self.assertIsInstance(self.test_skill, BaseSkill) + self.assertIsInstance(self.test_skill, OVOSSkill) self.assertIsInstance(self.test_skill, CommonQuerySkill) # TODO: Test other class methods diff --git a/test/unittests/skills/test_base.py b/test/unittests/skills/test_base.py index 33fbf804..2570fe3e 100644 --- a/test/unittests/skills/test_base.py +++ b/test/unittests/skills/test_base.py @@ -8,28 +8,17 @@ from time import time from unittest.mock import Mock, patch from os.path import join, dirname, isdir +from ovos_workshop.skills.ovos import OVOSSkill from ovos_utils.messagebus import FakeBus -class TestBase(unittest.TestCase): - def test_is_classic_core(self): - from ovos_workshop.skills.base import is_classic_core - self.assertIsInstance(is_classic_core(), bool) - - def test_simple_trace(self): - from ovos_workshop.skills.base import simple_trace - trace = ["line_1\n", " line_2 \n", " \n", "line_3 \n"] - self.assertEqual(simple_trace(trace), "Traceback:\nline_1\n line_2 \n") - - -class TestBaseSkill(unittest.TestCase): +class TestOVOSSkill(unittest.TestCase): test_config_path = join(dirname(__file__), "temp_config") os.environ["XDG_CONFIG_HOME"] = test_config_path - from ovos_workshop.skills.base import BaseSkill bus = FakeBus() skill_id = "test_base_skill" - skill = BaseSkill(bus=bus, skill_id=skill_id) + skill = OVOSSkill(bus=bus, skill_id=skill_id) @classmethod def tearDownClass(cls) -> None: @@ -161,7 +150,7 @@ def test_init_settings_manager(self): def test_start_filewatcher(self): test_skill_id = "test_settingschanged.skill" - test_skill = self.BaseSkill(bus=self.bus, skill_id=test_skill_id) + test_skill = OVOSSkill(bus=self.bus, skill_id=test_skill_id) settings_changed = Event() on_file_change = Mock(side_effect=lambda x: settings_changed.set()) test_skill._handle_settings_file_change = on_file_change @@ -335,8 +324,8 @@ def test_register_intent(self): pass def test_register_intent_file(self): - from ovos_workshop.skills.base import BaseSkill - skill = BaseSkill(bus=self.bus, skill_id=self.skill_id) + from ovos_workshop.skills.ovos import OVOSSkill + skill = OVOSSkill(bus=self.bus, skill_id=self.skill_id) skill._lang_resources = dict() skill.intent_service = Mock() skill.res_dir = join(dirname(__file__), "test_locale") @@ -362,8 +351,8 @@ def test_register_intent_file(self): f"{skill.skill_id}:time.intent", uk_intent_file, "uk-ua") def test_register_entity_file(self): - from ovos_workshop.skills.base import BaseSkill - skill = BaseSkill(bus=self.bus, skill_id=self.skill_id) + from ovos_workshop.skills.ovos import OVOSSkill + skill = OVOSSkill(bus=self.bus, skill_id=self.skill_id) skill._lang_resources = dict() skill.intent_service = Mock() skill.res_dir = join(dirname(__file__), "test_locale") @@ -479,7 +468,7 @@ def test_shutdown(self): def test_default_shutdown(self): test_skill_id = "test_shutdown.skill" - test_skill = self.BaseSkill(bus=self.bus, skill_id=test_skill_id) + test_skill = OVOSSkill(bus=self.bus, skill_id=test_skill_id) test_skill.settings["changed"] = True test_skill.stop = Mock() test_skill.shutdown = Mock() @@ -558,18 +547,3 @@ class GuiSkill(Mock): "legacy": False}} root_dir = join(dirname(__file__), "test_gui") - @patch("ovos_workshop.skills.ovos.GUIInterface.__init__") - def test_skill_gui(self, interface_init): - from ovos_bus_client.apis.gui import GUIInterface - from ovos_workshop.skills.base import SkillGUI - - # Old skill with `ui` directory in root - old_skill = self.LegacySkill() - old_gui = SkillGUI(old_skill) - self.assertEqual(old_gui.skill, old_skill) - self.assertIsInstance(old_gui, GUIInterface) - interface_init.assert_called_once_with( - old_gui, skill_id=old_skill.skill_id, bus=old_skill.bus, - config=old_skill.config_core['gui'], - ui_directories={"qt5": join(old_skill.root_dir, "ui")}) - diff --git a/test/unittests/skills/test_common_query_skill.py b/test/unittests/skills/test_common_query_skill.py index ef182e86..7c1a822c 100644 --- a/test/unittests/skills/test_common_query_skill.py +++ b/test/unittests/skills/test_common_query_skill.py @@ -1,7 +1,6 @@ from unittest import TestCase from ovos_utils.messagebus import FakeBus -from ovos_workshop.skills.base import BaseSkill from ovos_workshop.skills.common_query_skill import CommonQuerySkill, CQSMatchLevel @@ -18,10 +17,7 @@ class TestCommonQuerySkill(TestCase): def test_class_inheritance(self): from ovos_workshop.skills.ovos import OVOSSkill - from ovos_workshop.skills.mycroft_skill import MycroftSkill - self.assertIsInstance(self.skill, BaseSkill) self.assertIsInstance(self.skill, OVOSSkill) - self.assertIsInstance(self.skill, MycroftSkill) self.assertIsInstance(self.skill, CommonQuerySkill) def test_00_skill_init(self): diff --git a/test/unittests/skills/test_fallback_skill.py b/test/unittests/skills/test_fallback_skill.py index 656ab4da..8e2cbad3 100644 --- a/test/unittests/skills/test_fallback_skill.py +++ b/test/unittests/skills/test_fallback_skill.py @@ -6,23 +6,12 @@ from ovos_utils.messagebus import FakeBus from ovos_bus_client.message import Message from ovos_workshop.decorators import fallback_handler -from ovos_workshop.skills.base import BaseSkill -from ovos_workshop.skills.fallback import FallbackSkillV1, FallbackSkillV2, \ - FallbackSkill +from ovos_workshop.skills.fallback import FallbackSkill -class SimpleFallback(FallbackSkillV1): - """Simple fallback skill used for test.""" - def initialize(self): - self.register_fallback(self.fallback_handler, 42) - - def fallback_handler(self, _): - pass - - -class V2FallbackSkill(FallbackSkillV2): +class V2FallbackSkill(FallbackSkill): def __init__(self): - FallbackSkillV2.__init__(self, FakeBus(), "fallback_v2") + super().__init__(FakeBus(), "fallback_v2") @fallback_handler def handle_fallback(self, message): @@ -33,168 +22,27 @@ def high_prio_fallback(self, message): pass -class TestFallbackSkill(TestCase): - # TODO: Test `__new__` logic - pass - - def test_class_inheritance(self): - from ovos_workshop.skills.ovos import OVOSSkill - from ovos_workshop.skills.mycroft_skill import MycroftSkill - fallback = FallbackSkill("test") - self.assertIsInstance(fallback, BaseSkill) - self.assertIsInstance(fallback, OVOSSkill) - self.assertIsInstance(fallback, MycroftSkill) - self.assertIsInstance(fallback, FallbackSkillV1) - self.assertIsInstance(fallback, FallbackSkillV2) - self.assertIsInstance(fallback, FallbackSkill) - - -class TestFallbackSkillV1(TestCase): - @staticmethod - def setup_fallback(fb_class): - fb_skill = fb_class() - fb_skill.bind(FakeBus()) - fb_skill.initialize() - return fb_skill - - def test_inheritance(self): - from ovos_workshop.skills.ovos import OVOSSkill - from ovos_workshop.skills.mycroft_skill import MycroftSkill - fallback = FallbackSkillV1("test") - self.assertIsInstance(fallback, BaseSkill) - self.assertIsInstance(fallback, OVOSSkill) - self.assertIsInstance(fallback, MycroftSkill) - self.assertIsInstance(fallback, FallbackSkillV1) - self.assertIsInstance(fallback, FallbackSkillV2) - self.assertIsInstance(fallback, FallbackSkill) - - def test_make_intent_failure_handler(self): - # TODO - pass - - def test_report_timing(self): - # TODO - pass - - def test__register_fallback(self): - # TODO - pass - - def test_register_fallback(self): - # TODO - pass - - def test_remove_registered_handler(self): - # TODO - pass - - @patch("ovos_workshop.skills.fallback.FallbackSkillV1." - "_remove_registered_handler") - def test_remove_fallback(self, remove_handler): - def wrapper(handler): - def wrapped(): - if handler(): - return True - return False - return wrapped - - def _mock_1(): - pass - - def _mock_2(): - pass - - FallbackSkillV1.wrapper_map.append((_mock_1, wrapper(_mock_1))) - self.assertEqual(len(FallbackSkillV1.wrapper_map), 1) - - FallbackSkillV1.wrapper_map.append((_mock_2, wrapper(_mock_2))) - self.assertEqual(len(FallbackSkillV1.wrapper_map), 2) - - # Successful remove existing wrapper - remove_handler.return_value = True - self.assertTrue(FallbackSkillV1.remove_fallback(_mock_1)) - self.assertEqual(len(FallbackSkillV1.wrapper_map), 1) - self.assertFalse(FallbackSkillV1.remove_fallback(_mock_1)) - self.assertEqual(len(FallbackSkillV1.wrapper_map), 1) - - # Failed remove existing wrapper - remove_handler.return_value = False - self.assertFalse(FallbackSkillV1.remove_fallback( - FallbackSkillV1.wrapper_map[0][1])) - self.assertEqual(FallbackSkillV1.wrapper_map, []) - - def test_remove_instance_handlers(self): - # TODO - pass - - def test_default_shutdown(self): - # TODO - pass - - def test_register_decorated(self): - # TODO - pass - - def test_life_cycle(self): - """ - Test startup and shutdown of a fallback skill. - Ensure that an added handler is removed as part of default shutdown. - """ - self.assertEqual(len(FallbackSkillV1.fallback_handlers), 0) - fb_skill = self.setup_fallback(SimpleFallback) - self.assertEqual(len(FallbackSkillV1.fallback_handlers), 1) - self.assertEqual(FallbackSkillV1.wrapper_map[0][0], - fb_skill.fallback_handler) - self.assertEqual(len(FallbackSkillV1.wrapper_map), 1) - - fb_skill.default_shutdown() - self.assertEqual(len(FallbackSkillV1.fallback_handlers), 0) - self.assertEqual(len(FallbackSkillV1.wrapper_map), 0) - - def test_manual_removal(self): - """ - Test that the call to remove_fallback() removes the handler - """ - self.assertEqual(len(FallbackSkillV1.fallback_handlers), 0) - - # Create skill adding a single handler - fb_skill = self.setup_fallback(SimpleFallback) - self.assertEqual(len(FallbackSkillV1.fallback_handlers), 1) - - self.assertTrue(fb_skill.remove_fallback(fb_skill.fallback_handler)) - # Both internal trackers of handlers should be cleared now - self.assertEqual(len(FallbackSkillV1.fallback_handlers), 0) - self.assertEqual(len(FallbackSkillV1.wrapper_map), 0) - - # Removing after it's already been removed should fail - self.assertFalse(fb_skill.remove_fallback(fb_skill.fallback_handler)) - class TestFallbackSkillV2(TestCase): - fallback_skill = FallbackSkillV2(FakeBus(), "test_fallback_v2") + fallback_skill = FallbackSkill(FakeBus(), "test_fallback_v2") def test_class_inheritance(self): from ovos_workshop.skills.ovos import OVOSSkill - from ovos_workshop.skills.mycroft_skill import MycroftSkill - self.assertIsInstance(self.fallback_skill, BaseSkill) self.assertIsInstance(self.fallback_skill, OVOSSkill) - self.assertIsInstance(self.fallback_skill, MycroftSkill) - self.assertIsInstance(self.fallback_skill, FallbackSkillV1) - self.assertIsInstance(self.fallback_skill, FallbackSkillV2) self.assertIsInstance(self.fallback_skill, FallbackSkill) def test_00_init(self): - self.assertIsInstance(self.fallback_skill, FallbackSkillV2) + from ovos_workshop.skills.ovos import OVOSSkill self.assertIsInstance(self.fallback_skill, FallbackSkill) - self.assertIsInstance(self.fallback_skill, BaseSkill) + self.assertIsInstance(self.fallback_skill, OVOSSkill) def test_priority(self): - FallbackSkillV2.fallback_config = {} + FallbackSkill.fallback_config = {} # No config or handlers self.assertEqual(self.fallback_skill.priority, 101) # Config override - FallbackSkillV2.fallback_config = \ + FallbackSkill.fallback_config = \ {"fallback_priorities": {"test_fallback_v2": 10}} self.assertEqual(self.fallback_skill.priority, 10, self.fallback_skill.fallback_config) @@ -204,11 +52,11 @@ def test_priority(self): # Minimum handler self.assertEqual(fallback_skill.priority, 10) # Config override - FallbackSkillV2.fallback_config['fallback_priorities'][ + FallbackSkill.fallback_config['fallback_priorities'][ fallback_skill.skill_id] = 80 self.assertEqual(fallback_skill.priority, 80) - FallbackSkillV2.fallback_config = {} + FallbackSkill.fallback_config = {} def test_can_answer(self): self.assertFalse(self.fallback_skill.can_answer([""], "en-us")) diff --git a/test/unittests/skills/test_idle_display_skill.py b/test/unittests/skills/test_idle_display_skill.py index a145db78..e187bd6d 100644 --- a/test/unittests/skills/test_idle_display_skill.py +++ b/test/unittests/skills/test_idle_display_skill.py @@ -1,7 +1,7 @@ import unittest from ovos_utils.messagebus import FakeBus -from ovos_workshop.skills.base import BaseSkill +from ovos_workshop.skills.ovos import OVOSSkill from ovos_workshop.skills.idle_display_skill import IdleDisplaySkill @@ -15,6 +15,6 @@ class TestIdleDisplaySkill(unittest.TestCase): skill = TestSkill(bus=FakeBus(), skill_id="test_idle_skill") def test_00_skill_init(self): - self.assertIsInstance(self.skill, BaseSkill) + self.assertIsInstance(self.skill, OVOSSkill) self.assertIsInstance(self.skill, IdleDisplaySkill) # TODO: Implement more tests diff --git a/test/unittests/skills/test_mycroft_skill/__init__.py b/test/unittests/skills/test_mycroft_skill/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test.intent b/test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test.intent deleted file mode 100644 index e69de29b..00000000 diff --git a/test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test_ent.entity b/test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test_ent.entity deleted file mode 100644 index e69de29b..00000000 diff --git a/test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off2_test.voc b/test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off2_test.voc deleted file mode 100644 index d0633409..00000000 --- a/test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off2_test.voc +++ /dev/null @@ -1 +0,0 @@ -(turn off|switch off) diff --git a/test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off_test.voc b/test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off_test.voc deleted file mode 100644 index 8dde4209..00000000 --- a/test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off_test.voc +++ /dev/null @@ -1,3 +0,0 @@ -turn off -switch off - diff --git a/test/unittests/skills/test_mycroft_skill/mocks.py b/test/unittests/skills/test_mycroft_skill/mocks.py deleted file mode 100644 index 2d292975..00000000 --- a/test/unittests/skills/test_mycroft_skill/mocks.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2019 Mycroft AI Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from copy import deepcopy -from unittest.mock import Mock - -from ovos_config import LocalConf, DEFAULT_CONFIG - -__CONFIG = LocalConf(DEFAULT_CONFIG) - - -class AnyCallable: - """Class matching any callable. - - Useful for assert_called_with arguments. - """ - def __eq__(self, other): - return callable(other) - - -def base_config(): - """Base config used when mocking. - - Preload to skip hitting the disk each creation time but make a copy - so modifications don't mutate it. - - Returns: - (dict) Mycroft default configuration - """ - return deepcopy(__CONFIG) - - -def mock_config(temp_dir): - """Supply a reliable return value for the Configuration.get() method.""" - get_config_mock = Mock() - config = base_config() - config['skills']['priority_skills'] = ['foobar'] - config['data_dir'] = str(temp_dir) - config['server']['metrics'] = False - config['enclosure'] = {} - - get_config_mock.return_value = config - return get_config_mock - - -class MessageBusMock: - """Replaces actual message bus calls in unit tests. - - The message bus should not be running during unit tests so mock it - out in a way that makes it easy to test code that calls it. - """ - def __init__(self): - self.message_types = [] - self.message_data = [] - self.event_handlers = [] - - def emit(self, message): - self.message_types.append(message.msg_type) - self.message_data.append(message.data) - - def on(self, event, _): - self.event_handlers.append(event) - - def once(self, event, _): - self.event_handlers.append(event) diff --git a/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py deleted file mode 100644 index 08cfdb4b..00000000 --- a/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py +++ /dev/null @@ -1,673 +0,0 @@ -# -# Copyright 2017 Mycroft AI Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import sys -import unittest -from datetime import datetime -from os.path import join, dirname, abspath -from unittest.mock import MagicMock, patch - -from ovos_workshop.intents import IntentBuilder -from ovos_bus_client import Message -from ovos_config.config import Configuration - -from ovos_workshop.decorators import intent_handler, resting_screen_handler, \ - intent_file_handler -from ovos_workshop.skills.mycroft_skill import MycroftSkill -from .mocks import base_config - -BASE_CONF = base_config() - - -class MockEmitter(object): - def __init__(self): - self.types = [] - self.results = [] - self.reset() - - def emit(self, message): - self.types.append(message.msg_type) - self.results.append(message.data) - - def get_types(self): - return self.types - - def get_results(self): - return self.results - - def on(self, event, f): - pass - - def reset(self): - self.types = [] - self.results = [] - - -def vocab_base_path(): - return join(dirname(__file__), '..', 'vocab_test') - - -class TestFunction(unittest.TestCase): - def test_resting_screen_handler(self): - class T(MycroftSkill): - def __init__(self): - self.name = 'TestObject' - - @resting_screen_handler('humbug') - def f(self): - pass - - test_class = T() - self.assertTrue('resting_handler' in dir(test_class.f)) - self.assertEqual(test_class.f.resting_handler, 'humbug') - - -class TestMycroftSkill(unittest.TestCase): - emitter = MockEmitter() - regex_path = abspath(join(dirname(__file__), '../regex_test')) - vocab_path = abspath(join(dirname(__file__), '../vocab_test')) - - def setUp(self): - self.emitter.reset() - - def check_detach_intent(self): - self.assertTrue(len(self.emitter.get_types()) > 0) - for msg_type in self.emitter.get_types(): - self.assertEqual(msg_type, 'detach_intent') - self.emitter.reset() - - def check_register_intent(self, result_list): - for msg_type in self.emitter.get_types(): - self.assertEqual(msg_type, 'register_intent') - self.assertEqual(sorted(self.emitter.get_results()), - sorted(result_list)) - self.emitter.reset() - - def check_register_vocabulary(self, result_list): - for msg_type in self.emitter.get_types(): - self.assertEqual(msg_type, 'register_vocab') - self.assertEqual(sorted(self.emitter.get_results()), - sorted(result_list)) - self.emitter.reset() - - def test_register_intent(self): - # Test register Intent object - s = SimpleSkill1() - s._startup(self.emitter, "A") - expected = [{'at_least_one': [], - 'name': 'A:a', - 'excludes': [], - 'optional': [], - 'requires': [('AKeyword', 'AKeyword')]}] - msg_data = self.emitter.get_results() - self.assertTrue(expected[0] in msg_data) - self.emitter.reset() - - # Test register IntentBuilder object - s = SimpleSkill2() - s._startup(self.emitter, "A") - expected = [{'at_least_one': [], - 'name': 'A:a', - 'optional': [], - 'excludes': [], - 'requires': [('AKeyword', 'AKeyword')]}] - - msg_data = self.emitter.get_results() - self.assertTrue(expected[0] in msg_data) - self.emitter.reset() - - # Test register IntentBuilder object - with self.assertRaises(ValueError): - s = SimpleSkill3() - s._startup(self.emitter, "A") - - def test_enable_disable_intent(self): - """Test disable/enable intent.""" - # Setup basic test - s = SimpleSkill1() - s._startup(self.emitter, "A") - - # check that intent was registered - expected = [{'at_least_one': [], - 'name': 'A:a', - 'optional': [], - 'excludes': [], - 'requires': [('AKeyword', 'AKeyword')]}] - msg_data = self.emitter.get_results() - self.assertTrue(expected[0] in msg_data) - self.emitter.reset() - - # Test disable/enable cycle - s.disable_intent('a') - self.check_detach_intent() - s.enable_intent('a') - self.check_register_intent(expected) - - def test_enable_disable_intent_handlers(self): - """Test disable/enable intent.""" - # Setup basic test - s = SimpleSkill1() - s._startup(self.emitter, "A") - expected = [{'at_least_one': [], - 'name': 'A:a', - 'optional': [], - 'excludes': [], - 'requires': [('AKeyword', 'AKeyword')]}] - msg_data = self.emitter.get_results() - self.assertTrue(expected[0] in msg_data) - self.emitter.reset() - - # Test disable/enable cycle - msg = Message('test.msg', data={'intent_name': 'a'}) - intent_disabled = s.handle_disable_intent(msg) - self.assertTrue(intent_disabled) - self.check_detach_intent() - intent_enabled = s.handle_enable_intent(msg) - self.assertTrue(intent_enabled) - self.check_register_intent(expected) - - def test_register_vocab(self): - """Test disable/enable intent.""" - # Setup basic test - s = SimpleSkill1() - s._startup(self.emitter, "A") - - # Normal vocaubulary - self.emitter.reset() - expected = [ - { - 'start': 'hello', - 'end': 'AHelloKeyword', - 'entity_value': 'hello', - 'lang': 'en-us', - 'entity_type': 'AHelloKeyword' - } - ] - s.register_vocabulary('hello', 'HelloKeyword') - self.check_register_vocabulary(expected) - # Regex - s.register_regex('weird (?P.+) stuff') - expected = [{'lang': 'en-us', 'regex': 'weird (?P.+) stuff'}] - self.check_register_vocabulary(expected) - - def check_register_object_file(self, types_list, result_list): - self.assertEqual(sorted(self.emitter.get_types()), - sorted(types_list)) - self.assertEqual(sorted(self.emitter.get_results(), - key=lambda d: sorted(d.items())), - sorted(result_list, key=lambda d: sorted(d.items()))) - self.emitter.reset() - - def test_register_intent_file(self): - self._test_intent_file(SimpleSkill4()) - - def test_register_intent_intent_file(self): - """Test register intent files using register_intent.""" - self._test_intent_file(SimpleSkill6()) - - def _test_intent_file(self, s): - s.res_dir = abspath(join(dirname(__file__), 'intent_file')) - s._startup(self.emitter, "A") - - expected_types = [ - 'padatious:register_intent', - 'padatious:register_entity' - ] - - expected_results = [ - { - 'file_name': join(dirname(__file__), 'intent_file', - 'vocab', 'en-us', 'test.intent'), - 'lang': 'en-us', - 'name': str(s.skill_id) + ':test.intent', - 'samples': [] - }, - { - 'file_name': join(dirname(__file__), 'intent_file', - 'vocab', 'en-us', 'test_ent.entity'), - 'lang': 'en-us', - 'name': str(s.skill_id) + ':test_ent_87af9db6c8402bcfaa8ebc719ae4427c', - 'samples': [] - } - ] - self.check_register_object_file(expected_types, expected_results) - - def check_register_decorators(self, result_list): - self.assertEqual(sorted(self.emitter.get_results(), - key=lambda d: sorted(d.items())), - sorted(result_list, key=lambda d: sorted(d.items()))) - self.emitter.reset() - - def test_register_decorators(self): - """ Test decorated intents """ - path_orig = sys.path - sys.path.append(abspath(dirname(__file__))) - - s = SimpleSkill5() - s.res_dir = abspath(join(dirname(__file__), 'intent_file')) - s._startup(self.emitter, "A") - - expected = [{'at_least_one': [], - 'name': 'A:a', - 'optional': [], - 'excludes': [], - 'requires': [('AKeyword', 'AKeyword')]}, - { - 'file_name': join(dirname(__file__), 'intent_file', - 'vocab', 'en-us', 'test.intent'), - 'lang': 'en-us', - 'samples': [], - 'name': str(s.skill_id) + ':test.intent'} - ] - - self.check_register_decorators(expected) - # Restore sys.path - sys.path = path_orig - - def test_failing_set_context(self): - s = SimpleSkill1() - s._startup(self.emitter, "A") - with self.assertRaises(ValueError): - s.set_context(1) - with self.assertRaises(ValueError): - s.set_context(1, 1) - with self.assertRaises(ValueError): - s.set_context('Kowabunga', 1) - - def test_set_context(self): - def check_set_context(result_list): - for msg_type in self.emitter.get_types(): - self.assertEqual(msg_type, 'add_context') - self.assertEqual(sorted(self.emitter.get_results()), - sorted(result_list)) - self.emitter.reset() - - s = SimpleSkill1() - s._startup(self.emitter, "A") - self.emitter.reset() - # No context content - s.set_context('TurtlePower') - expected = [{'context': 'ATurtlePower', 'origin': '', 'word': ''}] - check_set_context(expected) - - # context with content - s.set_context('Technodrome', 'Shredder') - expected = [{'context': 'ATechnodrome', 'origin': '', - 'word': 'Shredder'}] - check_set_context(expected) - - # UTF-8 context - s.set_context('Smörgåsbord€15') - expected = [{'context': 'ASmörgåsbord€15', 'origin': '', 'word': ''}] - check_set_context(expected) - - self.emitter.reset() - - def test_failing_remove_context(self): - s = SimpleSkill1() - s._startup(self.emitter, "A") - with self.assertRaises(ValueError): - s.remove_context(1) - - def test_remove_context(self): - def check_remove_context(result_list): - for type in self.emitter.get_types(): - self.assertEqual(type, 'remove_context') - self.assertEqual(sorted(self.emitter.get_results()), - sorted(result_list)) - self.emitter.reset() - - s = SimpleSkill1() - s._startup(self.emitter, "A") - self.emitter.reset() - s.remove_context('Donatello') - expected = [{'context': 'ADonatello'}] - check_remove_context(expected) - - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_skill_location(self): - s = SimpleSkill1() - self.assertEqual(s.location, BASE_CONF.get('location')) - self.assertEqual(s.location_pretty, - BASE_CONF['location']['city']['name']) - self.assertEqual(s.location_timezone, - BASE_CONF['location']['timezone']['code']) - - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_add_event(self): - emitter = MagicMock() - s = SimpleSkill1() - s._startup(emitter, "A") - s.add_event('handler1', s.handler) - # Check that the handler was registered with the emitter - self.assertEqual(emitter.on.call_args[0][0], 'handler1') - # Check that the handler was stored in the skill - self.assertTrue('handler1' in [e[0] for e in s.events]) - - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_remove_event(self): - emitter = MagicMock() - s = SimpleSkill1() - s._startup(emitter, "A") - s.add_event('handler1', s.handler) - self.assertTrue('handler1' in [e[0] for e in s.events]) - # Remove event handler - s.remove_event('handler1') - # make sure it's not in the event list anymore - self.assertTrue('handler1' not in [e[0] for e in s.events]) - # Check that the handler was registered with the emitter - self.assertEqual(emitter.remove_all_listeners.call_args[0][0], - 'handler1') - - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_add_scheduled_event(self): - emitter = MagicMock() - s = SimpleSkill1() - s._startup(emitter, "A") - - s.schedule_event(s.handler, datetime.now(), name='datetime_handler') - # Check that the handler was registered with the emitter - self.assertEqual(emitter.once.call_args[0][0], 'A:datetime_handler') - sched_events = [e[0] for e in s.event_scheduler.events] - self.assertTrue('A:datetime_handler' in sched_events) - - s.schedule_event(s.handler, 1, name='int_handler') - # Check that the handler was registered with the emitter - self.assertEqual(emitter.once.call_args[0][0], 'A:int_handler') - sched_events = [e[0] for e in s.event_scheduler.events] - self.assertTrue('A:int_handler' in sched_events) - - s.schedule_event(s.handler, .5, name='float_handler') - # Check that the handler was registered with the emitter - self.assertEqual(emitter.once.call_args[0][0], 'A:float_handler') - sched_events = [e[0] for e in s.event_scheduler.events] - self.assertTrue('A:float_handler' in sched_events) - - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_remove_scheduled_event(self): - emitter = MagicMock() - s = SimpleSkill1() - s._startup(emitter, "A") - s.schedule_event(s.handler, datetime.now(), name='sched_handler1') - # Check that the handler was registered with the emitter - events = [e[0] for e in s.event_scheduler.events] - print(events) - self.assertTrue('A:sched_handler1' in events) - s.cancel_scheduled_event('sched_handler1') - # Check that the handler was removed - self.assertEqual(emitter.remove_all_listeners.call_args[0][0], - 'A:sched_handler1') - events = [e[0] for e in s.event_scheduler.events] - self.assertTrue('A:sched_handler1' not in events) - - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_run_scheduled_event(self): - emitter = MagicMock() - s = SimpleSkill1() - with patch.object(s, '_settings', - create=True, value=MagicMock()): - s._startup(emitter, "A") - s.schedule_event(s.handler, datetime.now(), name='sched_handler1') - # Check that the handler was registered with the emitter - emitter.once.call_args[0][1](Message('message')) - # Check that the handler was run - self.assertTrue(s.handler_run) - # Check that the handler was removed from the list of registred - # handler - self.assertTrue('A:sched_handler1' not in [e[0] for e in s.events]) - - def test_voc_match(self): - s = SimpleSkill1() - s.root_dir = abspath(dirname(__file__)) - - self.assertTrue(s.voc_match("turn off the lights", "turn_off_test")) - self.assertTrue(s.voc_match("would you please turn off the lights", - "turn_off_test")) - self.assertFalse(s.voc_match("return office", "turn_off_test")) - self.assertTrue(s.voc_match("switch off the lights", "turn_off_test")) - self.assertFalse(s.voc_match("", "turn_off_test")) - self.assertFalse(s.voc_match("switch", "turn_off_test")) - self.assertFalse(s.voc_match("My hovercraft is full of eels", - "turn_off_test")) - - self.assertTrue(s.voc_match("turn off the lights", "turn_off2_test")) - self.assertFalse(s.voc_match("return office", "turn_off2_test")) - self.assertTrue(s.voc_match("switch off the lights", "turn_off2_test")) - self.assertFalse(s.voc_match("", "turn_off_test")) - self.assertFalse(s.voc_match("switch", "turn_off_test")) - self.assertFalse(s.voc_match("My hovercraft is full of eels", - "turn_off_test")) - - def test_voc_match_exact(self): - s = SimpleSkill1() - s.root_dir = abspath(dirname(__file__)) - - self.assertTrue(s.voc_match("yes", "yes", exact=True)) - self.assertFalse(s.voc_match("yes please", "yes", exact=True)) - self.assertTrue(s.voc_match("switch off", "turn_off_test", - exact=True)) - self.assertFalse(s.voc_match("would you please turn off the lights", - "turn_off_test", exact=True)) - - def test_voc_list(self): - s = SimpleSkill1() - s.root_dir = abspath(dirname(__file__)) - - self.assertEqual(s.voc_list("turn_off_test"), - ["turn off", "switch off"]) - cache_key = s.lang + "turn_off_test" - self.assertIn(cache_key, s._voc_cache) - - def test_translate_locations(self): - """Assert that the a translatable list can be loaded from dialog and - locale. - """ - # Check that translatables can be loaded from the dialog directory - s = SimpleSkill1() - s.res_dir = abspath(join(dirname(__file__), - 'translate', 'in-dialog/')) - self.assertEqual(s.lang, "en-us") - lst = s.translate_list('good_things') - self.assertIsInstance(lst, list) - vals = s.translate_namedvalues('named_things') - self.assertIsInstance(vals, dict) - template = s.translate_template('test', - data={'thing': 'test framework'}) - self.assertEqual(template, - ['Oh look it\'s my favourite test framework']) - # Check that translatables can be loaded from locale folder - s = SimpleSkill1() - s.res_dir = abspath(join(dirname(__file__), - 'translate', 'in-locale')) - lst = s.translate_list('good_things') - self.assertIsInstance(lst, list) - vals = s.translate_namedvalues('named_things') - self.assertIsInstance(vals, dict) - template = s.translate_template('test', - data={'thing': 'test framework'}) - self.assertEqual(template, - ['Oh look it\'s my favourite test framework']) - - # Check loading in a non-en-us language - s = SimpleSkill1() - s.config_core['lang'] = 'de-de' - s.res_dir = abspath(join(dirname(__file__), - 'translate', 'in-locale')) - lst = s.translate_list('good_things') - self.assertEqual(lst, ['sonne', 'mycroft', 'zahne']) - vals = s.translate_namedvalues('named_things') - self.assertEqual(vals['blau'], '2') - template = s.translate_template('test', - data={'thing': 'test framework'}) - self.assertEqual(template, - ['Aber setzen sie sich herr test framework']) - - # Restore lang to en-us - s.config_core['lang'] = 'en-us' - - def test_native_langs(self): - s = _TestSkill() - lang = s.config_core['lang'] - secondary = s.config_core['secondary_langs'] - - s.config_core['lang'] = 'en-US' - s.config_core['secondary_langs'] = ['en', 'en-us', 'en-AU', - 'es', 'pt-PT'] - self.assertEqual(s.lang, 'en-us') - self.assertEqual(s.secondary_langs, ['en', 'en-au', 'es', - 'pt-pt']) - self.assertEqual(len(s.native_langs), len(set(s.native_langs))) - self.assertEqual(set(s.native_langs), {'en-us', 'en-au', 'pt-pt'}) - s.config_core['lang'] = lang - s.config_core['secondary_langs'] = secondary - - -class TestIntentCollisions(unittest.TestCase): - def test_two_intents_with_same_name(self): - emitter = MockEmitter() - skill = SameIntentNameSkill() - skill.bind(emitter) - with self.assertRaises(ValueError): - skill.initialize() - - def test_two_anonymous_intent_decorators(self): - """Two anonymous intent handlers should be ok.""" - emitter = MockEmitter() - skill = SameAnonymousIntentDecoratorsSkill() - skill.bind(emitter) - skill._register_decorated() - self.assertEqual(len(skill.intent_service.registered_intents), 2) - - -class _TestSkill(MycroftSkill): - def __init__(self): - super().__init__() - self.config_core['lang'] = "en-us" - self.config_core['secondary_langs'] = [] - self.skill_id = 'A' - - -class SimpleSkill1(_TestSkill): - def __init__(self): - super(SimpleSkill1, self).__init__() - self.handler_run = False - - """ Test skill for normal intent builder syntax """ - - def initialize(self): - i = IntentBuilder('a').require('Keyword').build() - self.register_intent(i, self.handler) - - def handler(self, message): - self.handler_run = True - - def stop(self): - pass - - -class SimpleSkill2(_TestSkill): - """ Test skill for intent builder without .build() """ - skill_id = 'A' - - def initialize(self): - i = IntentBuilder('a').require('Keyword') - self.register_intent(i, self.handler) - - def handler(self, message): - pass - - def stop(self): - pass - - -class SimpleSkill3(_TestSkill): - """ Test skill for invalid Intent for register_intent """ - skill_id = 'A' - - def initialize(self): - self.register_intent('string', self.handler) - - def handler(self, message): - pass - - def stop(self): - pass - - -class SimpleSkill4(_TestSkill): - """ Test skill for padatious intent """ - skill_id = 'A' - - def initialize(self): - self.register_intent_file('test.intent', self.handler) - self.register_entity_file('test_ent.entity') - - def handler(self, message): - pass - - def stop(self): - pass - - -class SimpleSkill5(MycroftSkill): - """ Test skill for intent_handler decorator. """ - - @intent_handler(IntentBuilder('a').require('Keyword').build()) - def handler(self, message): - pass - - @intent_file_handler('test.intent') - def handler2(self, message): - pass - - def stop(self): - pass - - -class SimpleSkill6(_TestSkill): - """ Test skill for padatious intent """ - skill_id = 'A' - - def initialize(self): - self.register_intent('test.intent', self.handler) - self.register_entity_file('test_ent.entity') - - def handler(self, message): - pass - - -class SameIntentNameSkill(_TestSkill): - """Test skill for duplicate intent namesr.""" - skill_id = 'A' - - def initialize(self): - intent = IntentBuilder('TheName').require('Keyword') - intent2 = IntentBuilder('TheName').require('Keyword') - self.register_intent(intent, self.handler) - self.register_intent(intent2, self.handler) - - def handler(self, message): - pass - - -class SameAnonymousIntentDecoratorsSkill(_TestSkill): - """Test skill for duplicate anonymous intent handlers.""" - skill_id = 'A' - - @intent_handler(IntentBuilder('').require('Keyword')) - @intent_handler(IntentBuilder('').require('OtherKeyword')) - def handler(self, message): - pass diff --git a/test/unittests/skills/test_mycroft_skill/test_mycroft_skill_get_response.py b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill_get_response.py deleted file mode 100644 index ebe8126b..00000000 --- a/test/unittests/skills/test_mycroft_skill/test_mycroft_skill_get_response.py +++ /dev/null @@ -1,302 +0,0 @@ -"""Tests for the mycroft skill's get_response variations.""" - -from os.path import dirname, join -from threading import Thread -import time -from unittest import TestCase, mock, skip - -from lingua_franca import load_language - -from ovos_workshop.skills.mycroft_skill import MycroftSkill -from ovos_bus_client import Message - -from .mocks import base_config, AnyCallable - - -load_language("en-us") - - -def create_converse_responder(response, skill): - """Create a function to inject a response into the converse method. - - The function waits for the converse method to be replaced by the - _wait_response logic and afterwards injects the provided response. - - Args: - response (str): Sentence to inject. - skill (MycroftSkill): skill to monitor. - """ - default_converse = skill.converse - converse_return = None - - def wait_for_new_converse(): - """Wait until there is a new converse handler then send sentence. - """ - nonlocal converse_return - start_time = time.monotonic() - while time.monotonic() < start_time + 5: - if skill.converse != default_converse: - skill.converse([response]) - break - - time.sleep(0.1) - - return wait_for_new_converse - - -@mock.patch('ovos_workshop.skills.ovos.Configuration') -def create_skill(mock_conf, lang='en-us'): - cfg = base_config() - cfg["lang"] = lang - mock_conf.get.return_value = cfg - mock_conf.return_value = cfg - bus = mock.Mock() - skill = MycroftSkill(name='test_skill') - skill.root_dir = join(dirname(__file__), 'test_skill') - skill._startup(bus) - return skill - - -@skip("TODO - update/fix me") -class TestMycroftSkillWaitResponse(TestCase): - - def test_wait(self): - """Ensure that _wait_response() returns the response from converse.""" - skill = create_skill() - - expected_response = 'Yes I do, very much' - - converser = Thread(target=create_converse_responder(expected_response, - skill)) - converser.start() - validator = mock.Mock() - validator.return_value = True - is_cancel = mock.Mock() - is_cancel.return_value = False - on_fail = mock.Mock() - - response = skill._wait_response(is_cancel, validator, on_fail, 1) - self.assertEqual(response, expected_response) - converser.join() - - def test_wait_cancel(self): - """Test that a matching cancel function cancels the wait.""" - skill = create_skill() - - converser = Thread(target=create_converse_responder('cancel', skill)) - converser.start() - validator = mock.Mock() - validator.return_value = False - on_fail = mock.Mock() - - def is_cancel(utterance): - return utterance == 'cancel' - - response = skill._wait_response(is_cancel, validator, on_fail, 1, Message("")) - self.assertEqual(response, None) - converser.join() - - -@skip("TODO - update/fix me") -class TestMycroftSkillGetResponse(TestCase): - def test_get_response(self): - """Test response using a dialog file.""" - skill = create_skill() - skill._wait_response = mock.Mock() - skill.speak_dialog = mock.Mock() - - expected_response = 'ice creamr please' - skill._wait_response.return_value = expected_response - response = skill.get_response('what do you want') - self.assertEqual(response, expected_response) - self.assertTrue(skill.speak_dialog.called) - - def test_get_response_text(self): - """Assert that text is used if no dialog exists.""" - skill = create_skill() - skill._wait_response = mock.Mock() - skill.speak_dialog = mock.Mock() - - expected_response = 'green' - skill._wait_response.return_value = expected_response - response = skill.get_response('tell me a color') - self.assertEqual(response, expected_response) - self.assertTrue(skill.speak_dialog.called) - skill.speak_dialog.assert_called_with('tell me a color', - {}, - expect_response=True, - wait=True) - - def test_get_response_no_dialog(self): - """Check that when no dialog/text is provided listening is triggered. - """ - skill = create_skill() - skill._wait_response = mock.Mock() - skill.speak_dialog = mock.Mock() - - expected_response = 'ice creamr please' - skill._wait_response.return_value = expected_response - response = skill.get_response() - self.assertEqual(response, expected_response) - self.assertFalse(skill.speak_dialog.called) - self.assertTrue(skill.bus.emit.called) - sent_message = skill.bus.emit.call_args[0][0] - self.assertEqual(sent_message.msg_type, 'mycroft.mic.listen') - - def test_get_response_validator(self): - """Ensure validator is passed on.""" - skill = create_skill() - skill._wait_response = mock.Mock() - skill.speak_dialog = mock.Mock() - - def validator(*args, **kwargs): - return True - - expected_response = 'ice creamr please' - skill._wait_response.return_value = expected_response - response = skill.get_response('what do you want', - validator=validator) - skill._wait_response.assert_called_with(AnyCallable(), validator, - AnyCallable(), -1) - - def test_converse_detection(self): - """Ensure validator is passed on.""" - skill = create_skill() - skill._wait_response = mock.Mock() - skill.speak_dialog = mock.Mock() - - def validator(*args, **kwargs): - self.assertTrue(skill._converse_is_implemented) - - self.assertFalse(skill.converse_is_implemented) - skill.get_response('what do you want', validator=validator) - skill._wait_response.assert_called_with(AnyCallable(), validator, - AnyCallable(), -1) - self.assertFalse(skill.converse_is_implemented) - - -class TestMycroftSkillAskYesNo(TestCase): - def test_ask_yesno_no(self): - """Check that a negative response is interpreted as a no.""" - skill = create_skill() - skill.get_response = mock.Mock() - - for ans in ['nope', - "i think not", - "don't think so", - "no", - "no, I obviously hate it", - "yes, but actually, no", - "wrong answer", - "yes, yes, yes, but actually, no", - "please don't", - "no! please! I beg you", - "yes, i don't want it for sure", - "it's a lie", - "that's certainly undesirable", - "he is lying", - "no, it's a lie", - "you are mistaken", - "that's a mistake"]: - skill.get_response.return_value = ans - response = skill.ask_yesno('Do you like breakfast') - self.assertEqual(response, 'no') - - def test_ask_yesno_yes(self): - """Check that an affirmative response is interpreted as a yes.""" - skill = create_skill() - skill.get_response = mock.Mock() - - for ans in ['yes', - "that's affirmative", - "no, but actually, yes", - "i want it for sure", "obviously", - "please", - "yes, it's a lie", - "indeed", - "tou are not wrong" - "correct, he is lying", - "he is not lying", - "you are not mistaken" - "it's not a lie", - "do I hate it when companies sell my data? yes, that's certainly undesirable", - "please! I beg you"]: - skill.get_response.return_value = ans - response = skill.ask_yesno('Do you like breakfast') - self.assertEqual(response, 'yes') - - def test_ask_yesno_other(self): - """Check that non yes no response gets returned.""" - skill = create_skill() - skill.get_response = mock.Mock() - skill.get_response.return_value = 'I am a fish' - - response = skill.ask_yesno('Do you like breakfast') - self.assertEqual(response, 'I am a fish') - - @mock.patch('ovos_bus_client.message.dig_for_message') - def test_ask_yesno_german(self, dig_mock): - """Check that when the skill is set to german it responds to "ja".""" - # lang is session based, it comes from originating message in ovos-core - dig_mock.return_value = Message("", {"lang": "de-de"}) - - load_language("de-de") - skill = create_skill(lang='de-de') - skill.get_response = mock.Mock() - skill.get_response.return_value = 'ja' - - response = skill.ask_yesno('Do you like breakfast') - self.assertEqual(response, 'yes') - - -class TestMycroftAskSelection(TestCase): - def test_selection_number(self): - """Test selection by number.""" - skill = create_skill() - skill.speak = mock.Mock() - skill.get_response = mock.Mock() - - skill.get_response.return_value = 'the third' - - options = ['a balloon', 'an octopus', 'a piano'] - response = skill.ask_selection(options, 'which is better') - self.assertEqual(options[2], response) - - # Assert that the spoken sentence contains all options. - spoken_sentence = skill.speak.call_args[0][0] - for opt in options: - self.assertTrue(opt in spoken_sentence) - - def test_selection_last(self): - """Test selection by "last".""" - skill = create_skill() - skill.speak = mock.Mock() - skill.get_response = mock.Mock() - - skill.get_response.return_value = 'last one' - - options = ['a balloon', 'an octopus', 'a piano'] - response = skill.ask_selection(options, 'which is better') - self.assertEqual(options[2], response) - - # Assert that the spoken sentence contains all options. - spoken_sentence = skill.speak.call_args[0][0] - for opt in options: - self.assertTrue(opt in spoken_sentence) - - def test_selection_name(self): - """Test selection by name.""" - skill = create_skill() - skill.speak = mock.Mock() - skill.get_response = mock.Mock() - - skill.get_response.return_value = 'octopus' - - options = ['a balloon', 'an octopus', 'a piano'] - response = skill.ask_selection(options, 'which is better') - self.assertEqual(options[1], response) - - # Assert that the spoken sentence contains all options. - spoken_sentence = skill.speak.call_args[0][0] - for opt in options: - self.assertTrue(opt in spoken_sentence) diff --git a/test/unittests/skills/test_mycroft_skill/test_skill/__init__.py b/test/unittests/skills/test_mycroft_skill/test_skill/__init__.py deleted file mode 100644 index 2dff65b1..00000000 --- a/test/unittests/skills/test_mycroft_skill/test_skill/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2017 Mycroft AI Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from ovos_workshop.skills import MycroftSkill - - -class LoadTestSkill(MycroftSkill): - pass - - -def create_skill(): - return LoadTestSkill() diff --git a/test/unittests/skills/test_mycroft_skill/test_skill/dialog/en-us/what do you want.dialog b/test/unittests/skills/test_mycroft_skill/test_skill/dialog/en-us/what do you want.dialog deleted file mode 100644 index c44a2c2c..00000000 --- a/test/unittests/skills/test_mycroft_skill/test_skill/dialog/en-us/what do you want.dialog +++ /dev/null @@ -1 +0,0 @@ -What do you want diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/good_things.list b/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/good_things.list deleted file mode 100644 index 78a4efa0..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/good_things.list +++ /dev/null @@ -1,3 +0,0 @@ -sunshine -mycroft -licorice diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/named_things.value b/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/named_things.value deleted file mode 100644 index 5e44c5cc..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/named_things.value +++ /dev/null @@ -1,4 +0,0 @@ -dot,0 -line,1 -circle,2 -ball,3 diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/test.template b/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/test.template deleted file mode 100644 index 8a6a6b84..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/test.template +++ /dev/null @@ -1 +0,0 @@ -Oh look it's my favourite {thing} diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/good_things.list b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/good_things.list deleted file mode 100644 index 1ca06804..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/good_things.list +++ /dev/null @@ -1,3 +0,0 @@ -sonne -mycroft -zahne diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/named_things.value b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/named_things.value deleted file mode 100644 index 3d7ac324..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/named_things.value +++ /dev/null @@ -1,3 +0,0 @@ -gelb,0 -rot,1 -blau,2 diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/test.template b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/test.template deleted file mode 100644 index c7d26b08..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/test.template +++ /dev/null @@ -1 +0,0 @@ -Aber setzen sie sich herr {thing} diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/good_things.list b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/good_things.list deleted file mode 100644 index 78a4efa0..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/good_things.list +++ /dev/null @@ -1,3 +0,0 @@ -sunshine -mycroft -licorice diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/named_things.value b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/named_things.value deleted file mode 100644 index 5e44c5cc..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/named_things.value +++ /dev/null @@ -1,4 +0,0 @@ -dot,0 -line,1 -circle,2 -ball,3 diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/not_in_german.list b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/not_in_german.list deleted file mode 100644 index 5755f36c..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/not_in_german.list +++ /dev/null @@ -1,3 +0,0 @@ -not -in -German diff --git a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/test.template b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/test.template deleted file mode 100644 index 8a6a6b84..00000000 --- a/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/test.template +++ /dev/null @@ -1 +0,0 @@ -Oh look it's my favourite {thing} diff --git a/test/unittests/skills/test_ovos.py b/test/unittests/skills/test_ovos.py index fbdcf8d9..1b2246e6 100644 --- a/test/unittests/skills/test_ovos.py +++ b/test/unittests/skills/test_ovos.py @@ -3,7 +3,7 @@ from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.messagebus import FakeBus from ovos_utils import classproperty -from ovos_workshop import IntentLayers +from ovos_workshop.decorators.layers import IntentLayers from ovos_workshop.resource_files import SkillResources from ovos_workshop.settings import SkillSettingsManager @@ -143,14 +143,10 @@ def test_runtime_requirements(self): RuntimeRequirements()) def test_class_inheritance(self): - from ovos_workshop.skills.base import BaseSkill from ovos_workshop.skills.ovos import OVOSSkill - from ovos_workshop.skills.mycroft_skill import MycroftSkill from ovos_workshop.app import OVOSAbstractApplication skill = MockSkill() - self.assertIsInstance(skill, BaseSkill) self.assertIsInstance(skill, OVOSSkill) - self.assertIsInstance(skill, MycroftSkill) self.assertNotIsInstance(skill, OVOSAbstractApplication) diff --git a/test/unittests/test_abstract_app.py b/test/unittests/test_abstract_app.py index 2b630f92..216f510f 100644 --- a/test/unittests/test_abstract_app.py +++ b/test/unittests/test_abstract_app.py @@ -1,13 +1,14 @@ import unittest - -from os.path import join, dirname from os import remove +from os.path import join, dirname from unittest.mock import Mock, patch +from json_database import JsonStorage from ovos_bus_client.apis.gui import GUIInterface from ovos_utils.messagebus import FakeBus + from ovos_workshop.app import OVOSAbstractApplication -from json_database import JsonStorage +from ovos_workshop.skills.ovos import OVOSSkill class Application(OVOSAbstractApplication): @@ -54,19 +55,11 @@ def test_settings_path(self): # Test settings path conflicts test_app = OVOSAbstractApplication(skill_id="test", bus=self.bus) - from ovos_workshop.skills import OVOSSkill - from ovos_workshop.skills.mycroft_skill import MycroftSkill test_skill = OVOSSkill(skill_id="test", bus=self.bus) - mycroft_skill = MycroftSkill(skill_id="test", bus=self.bus) # Test app vs skill base directories self.assertIn("/apps/", test_app.settings_path) self.assertIn("/skills/", test_skill.settings_path) - self.assertEqual(test_skill.settings_path, - mycroft_skill.settings_path) - self.assertEqual(test_skill.settings.path, - mycroft_skill.settings.path) - self.assertEqual(test_skill.settings, mycroft_skill.settings) # Test settings changes test_skill.settings['is_skill'] = True @@ -101,12 +94,8 @@ def test_clear_intents(self): pass def test_class_inheritance(self): - from ovos_workshop.skills.base import BaseSkill from ovos_workshop.skills.ovos import OVOSSkill - from ovos_workshop.skills.mycroft_skill import MycroftSkill from ovos_workshop.app import OVOSAbstractApplication - self.assertIsInstance(self.app, BaseSkill) self.assertIsInstance(self.app, OVOSSkill) - self.assertNotIsInstance(self.app, MycroftSkill) self.assertIsInstance(self.app, OVOSAbstractApplication) diff --git a/test/unittests/test_imports.py b/test/unittests/test_imports.py deleted file mode 100644 index 8158af10..00000000 --- a/test/unittests/test_imports.py +++ /dev/null @@ -1,12 +0,0 @@ -import unittest - - -class TestImports(unittest.TestCase): - """ - These tests are only valid if `mycroft` package is available - """ - def test_skills(self): - import ovos_workshop.skills - self.assertIsNotNone(ovos_workshop.skills.MycroftSkill) - self.assertIsNotNone(ovos_workshop.skills.OVOSSkill) - self.assertIsNotNone(ovos_workshop.skills.OVOSFallbackSkill) diff --git a/test/unittests/test_skill.py b/test/unittests/test_skill.py index 18d9621a..571d038b 100644 --- a/test/unittests/test_skill.py +++ b/test/unittests/test_skill.py @@ -5,42 +5,11 @@ from ovos_bus_client import Message from ovos_workshop.skills.ovos import OVOSSkill -from ovos_workshop.skills.mycroft_skill import MycroftSkill -from ovos_workshop.skills import MycroftSkill as CoreSkill from ovos_utils.messagebus import FakeBus from os.path import dirname from ovos_workshop.skill_launcher import SkillLoader -class LegacySkill(CoreSkill): - def __init__(self, skill_name="LegacySkill", bus=None, **kwargs): - self.inited = True - self.initialized = False - self.startup_called = False - super().__init__(skill_name, bus, **kwargs) - # __new__ calls `_startup` so this should be defined in __init__ - assert self.skill_id is not None - - def initialize(self): - self.initialized = True - - def _startup(self, bus, skill_id=""): - self.startup_called = True - self.initialize() - - -class BadLegacySkill(LegacySkill): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - print(self.bus) # not set, exception in property - - -class GoodLegacySkill(CoreSkill): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - print(self.bus) # maybe not set, exception in property - - class SpecificArgsSkill(OVOSSkill): def __init__(self, skill_id="SpecificArgsSkill", bus=None, **kwargs): self.inited = True @@ -91,14 +60,9 @@ def get_msg(msg): def test_skill_id(self): self.assertTrue(isinstance(self.skill.instance, OVOSSkill)) - self.assertTrue(isinstance(self.skill.instance, MycroftSkill)) self.assertEqual(self.skill.skill_id, "abort.test") - # the metaclass ensures this returns True under ovos-core - # but we have no control over mycroft-core so can not patch isinstance checks there - self.assertTrue(isinstance(self.skill.instance, CoreSkill)) - # if running in ovos-core every message will have the skill_id in context for msg in self.bus.emitted_msgs: if msg["type"] == 'mycroft.skills.loaded': # emitted by SkillLoader, not by skill @@ -133,7 +97,6 @@ def test_registered_events(self): for event in default_skill: self.assertTrue(event in registered_events) - # base skill class events exclusive to ovos-core default_ovos = [f"{self.skill.skill_id}.converse.ping", f"{self.skill.skill_id}.converse.request", "intent.service.skills.activated", @@ -162,31 +125,6 @@ def tearDown(self) -> None: class TestSkillNew(unittest.TestCase): - def test_legacy(self): - bus = FakeBus() - - # a legacy skill accepts wrong args, but accepts kwargs - legacy = LegacySkill("LegacyName", bus, skill_id="legacy.mycroft") - self.assertTrue(legacy.inited) - self.assertTrue(legacy.initialized) - self.assertTrue(legacy.startup_called) - self.assertIsNotNone(legacy.skill_id) - self.assertEqual(legacy.bus, bus) - - # a legacy skill not accepting args at all - with self.assertRaises(Exception) as ctxt: - BadLegacySkill() # accesses self.bus in __init__ - self.assertTrue("Accessed OVOSSkill.bus in __init__" in str(ctxt.exception)) - - legacynoargs = LegacySkill() # no exception this time because bus is not used in init - self.assertTrue(legacynoargs.inited) - self.assertFalse(legacynoargs.initialized) - self.assertFalse(legacynoargs.startup_called) - - # a legacy skill fully inited at once - legacy = GoodLegacySkill(skill_id="legacy.mycroft", bus=bus) # accesses self.bus in __init__ - self.assertEqual(legacy.skill_id, "legacy.mycroft") - self.assertEqual(legacy.bus, bus) def test_load(self): bus = FakeBus() diff --git a/test/unittests/test_skill_launcher.py b/test/unittests/test_skill_launcher.py index 274ff7ef..5f147163 100644 --- a/test/unittests/test_skill_launcher.py +++ b/test/unittests/test_skill_launcher.py @@ -38,14 +38,12 @@ def test_load_skill_module(self): def test_get_skill_class(self): from ovos_workshop.skill_launcher import get_skill_class, \ load_skill_module - from ovos_workshop.skills.ovos import _OVOSSkillMetaclass test_path = join(dirname(__file__), "ovos_tskill_abort", "__init__.py") skill_id = "test_skill.test" module = load_skill_module(test_path, skill_id) skill = get_skill_class(module) self.assertIsNotNone(skill) - self.assertEqual(skill.__class__, _OVOSSkillMetaclass, skill.__class__) # Test invalid request with self.assertRaises(ValueError): From 66e5d8ad455c9aa5dfc153f1e8bd7aa5c3765b99 Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 15 Oct 2024 02:51:41 +0100 Subject: [PATCH 2/3] tests --- test/unittests/skills/test_base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/unittests/skills/test_base.py b/test/unittests/skills/test_base.py index 2570fe3e..01fa8886 100644 --- a/test/unittests/skills/test_base.py +++ b/test/unittests/skills/test_base.py @@ -6,7 +6,7 @@ from logging import Logger from threading import Event, Thread from time import time -from unittest.mock import Mock, patch +from unittest.mock import Mock from os.path import join, dirname, isdir from ovos_workshop.skills.ovos import OVOSSkill @@ -324,7 +324,6 @@ def test_register_intent(self): pass def test_register_intent_file(self): - from ovos_workshop.skills.ovos import OVOSSkill skill = OVOSSkill(bus=self.bus, skill_id=self.skill_id) skill._lang_resources = dict() skill.intent_service = Mock() @@ -351,7 +350,6 @@ def test_register_intent_file(self): f"{skill.skill_id}:time.intent", uk_intent_file, "uk-ua") def test_register_entity_file(self): - from ovos_workshop.skills.ovos import OVOSSkill skill = OVOSSkill(bus=self.bus, skill_id=self.skill_id) skill._lang_resources = dict() skill.intent_service = Mock() From edcc0ea0acefac75e34dfb594c4b2e1833d9fc27 Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 15 Oct 2024 03:03:12 +0100 Subject: [PATCH 3/3] port extra tests from ovos-core --- test/unittests/test_common_query_skill.py | 61 ++++++ test/unittests/test_context.py | 40 ++++ .../test_intent_service_interface.py | 145 +++++++++++++ test/unittests/test_skill_api.py | 152 ++++++++++++++ test/unittests/test_skill_loader.py | 192 ++++++++++++++++++ 5 files changed, 590 insertions(+) create mode 100644 test/unittests/test_common_query_skill.py create mode 100644 test/unittests/test_context.py create mode 100644 test/unittests/test_intent_service_interface.py create mode 100644 test/unittests/test_skill_api.py create mode 100644 test/unittests/test_skill_loader.py diff --git a/test/unittests/test_common_query_skill.py b/test/unittests/test_common_query_skill.py new file mode 100644 index 00000000..f1cf9bad --- /dev/null +++ b/test/unittests/test_common_query_skill.py @@ -0,0 +1,61 @@ +from unittest import TestCase, mock + +from ovos_bus_client.message import Message + +from ovos_workshop.skills.common_query_skill import CommonQuerySkill + + +class AnyCallable: + """Class matching any callable. + + Useful for assert_called_with arguments. + """ + def __eq__(self, other): + return callable(other) + + + +class TestCommonQuerySkill(TestCase): + def setUp(self): + self.skill = CQSTest() + self.bus = mock.Mock(name='bus') + self.skill.bind(self.bus) + self.skill.config_core = {'enclosure': {'platform': 'mycroft_mark_1'}} + + def test_lifecycle(self): + """Test startup and shutdown.""" + skill = CQSTest() + bus = mock.Mock(name='bus') + skill.bind(bus) + bus.on.assert_any_call('question:query', AnyCallable()) + bus.on.assert_any_call('question:action', AnyCallable()) + skill.shutdown() + + def test_common_test_skill_action(self): + """Test that the optional action is triggered.""" + query_action = self.bus.on.call_args_list[-2][0][1] + query_action(Message('query:action', data={ + 'phrase': 'What\'s the meaning of life', + 'skill_id': 'asdf'})) + self.skill.CQS_action.assert_not_called() + query_action(Message('query:action', data={ + 'phrase': 'What\'s the meaning of life', + 'skill_id': 'CQSTest'})) + self.skill.CQS_action.assert_called_once_with( + 'What\'s the meaning of life', {}) + + +class CQSTest(CommonQuerySkill): + """Simple skill for testing the CommonQuerySkill""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.CQS_match_query_phrase = mock.Mock(name='match_phrase') + self.CQS_action = mock.Mock(name='selected_action') + self.skill_id = 'CQSTest' + + def CQS_match_query_phrase(self, phrase): + pass + + def CQS_action(self, phrase, data): + pass diff --git a/test/unittests/test_context.py b/test/unittests/test_context.py new file mode 100644 index 00000000..549fce6b --- /dev/null +++ b/test/unittests/test_context.py @@ -0,0 +1,40 @@ +from unittest import TestCase, mock + +# TODO - move to ovos-workshop +from ovos_workshop.decorators import adds_context, removes_context + + +class ContextSkillMock(mock.Mock): + """Mock class to apply decorators on.""" + @adds_context('DestroyContext') + def handler_adding_context(self): + pass + + @adds_context('DestroyContext', 'exterminate') + def handler_adding_context_with_words(self): + pass + + @removes_context('DestroyContext') + def handler_removing_context(self): + pass + + +class TestContextDecorators(TestCase): + def test_adding_context(self): + """Check that calling handler adds the correct Keyword.""" + skill = ContextSkillMock() + skill.handler_adding_context() + skill.set_context.assert_called_once_with('DestroyContext', '') + + def test_adding_context_with_words(self): + """Ensure that decorated handler adds Keyword and content.""" + skill = ContextSkillMock() + skill.handler_adding_context_with_words() + skill.set_context.assert_called_once_with('DestroyContext', + 'exterminate') + + def test_removing_context(self): + """Make sure the decorated handler removes the specified context.""" + skill = ContextSkillMock() + skill.handler_removing_context() + skill.remove_context.assert_called_once_with('DestroyContext') diff --git a/test/unittests/test_intent_service_interface.py b/test/unittests/test_intent_service_interface.py new file mode 100644 index 00000000..61fea26e --- /dev/null +++ b/test/unittests/test_intent_service_interface.py @@ -0,0 +1,145 @@ +import unittest +# TODO - move test to ovos-workshop +from ovos_workshop.intents import IntentBuilder, IntentServiceInterface + + +class MockEmitter: + def __init__(self): + self.reset() + + def emit(self, message): + self.types.append(message.msg_type) + self.results.append(message.data) + + def get_types(self): + return self.types + + def get_results(self): + return self.results + + def on(self, event, f): + pass + + def reset(self): + self.types = [] + self.results = [] + + +class KeywordRegistrationTest(unittest.TestCase): + def check_emitter(self, expected_message_data): + """Verify that the registration messages matches the expected.""" + for msg_type in self.emitter.get_types(): + self.assertEqual(msg_type, 'register_vocab') + self.assertEqual( + sorted(self.emitter.get_results(), + key=lambda d: sorted(d.items())), + sorted(expected_message_data, key=lambda d: sorted(d.items()))) + self.emitter.reset() + + def setUp(self): + self.emitter = MockEmitter() + + def test_register_keyword(self): + intent_service = IntentServiceInterface(self.emitter) + intent_service.register_adapt_keyword('test_intent', 'test', lang='en-US') + entity_data = {'entity_value': 'test', 'entity_type': 'test_intent', 'lang': 'en-US'} + compatibility_data = {'start': 'test', 'end': 'test_intent'} + expected_data = {**entity_data, **compatibility_data} + self.check_emitter([expected_data]) + + def test_register_keyword_with_aliases(self): + # TODO 22.02: Remove compatibility data + intent_service = IntentServiceInterface(self.emitter) + intent_service.register_adapt_keyword('test_intent', 'test', + ['test2', 'test3'], + lang='en-US') + + entity_data = {'entity_value': 'test', 'entity_type': 'test_intent', 'lang': 'en-US'} + compatibility_data = {'start': 'test', 'end': 'test_intent'} + expected_initial_vocab = {**entity_data, **compatibility_data} + + alias_data = { + 'entity_value': 'test2', + 'entity_type': 'test_intent', + 'alias_of': 'test', + 'lang': 'en-US' + } + alias_compatibility = {'start': 'test2', 'end': 'test_intent'} + expected_alias1 = {**alias_data, **alias_compatibility} + + alias_data2 = { + 'entity_value': 'test3', + 'entity_type': 'test_intent', + 'alias_of': 'test', + 'lang': 'en-US' + } + alias_compatibility2 = {'start': 'test3', 'end': 'test_intent'} + expected_alias2 = {**alias_data2, **alias_compatibility2} + + self.check_emitter([expected_initial_vocab, + expected_alias1, + expected_alias2]) + + def test_register_regex(self): + intent_service = IntentServiceInterface(self.emitter) + intent_service.register_adapt_regex('.*', lang="en-us") + self.check_emitter([{'regex': '.*', 'lang': 'en-us'}]) + + +class KeywordIntentRegistrationTest(unittest.TestCase): + def check_emitter(self, expected_message_data): + """Verify that the registration messages matches the expected.""" + for msg_type in self.emitter.get_types(): + self.assertEqual(msg_type, 'register_intent') + self.assertEqual( + sorted(self.emitter.get_results(), + key=lambda d: sorted(d.items())), + sorted(expected_message_data, key=lambda d: sorted(d.items()))) + self.emitter.reset() + + def setUp(self): + self.emitter = MockEmitter() + + def test_register_intent(self): + intent_service = IntentServiceInterface(self.emitter) + intent_service.register_adapt_keyword('testA', 'testA', lang='en-US') + intent_service.register_adapt_keyword('testB', 'testB', lang='en-US') + self.emitter.reset() + + intent = IntentBuilder("test").require("testA").optionally("testB") + intent_service.register_adapt_intent("test", intent) + expected_data = {'at_least_one': [], + 'name': 'test', + 'excludes': [], + 'optional': [('testB', 'testB')], + 'requires': [('testA', 'testA')]} + self.check_emitter([expected_data]) + + + +class UtteranceIntentRegistrationTest(unittest.TestCase): + def check_emitter(self, expected_message_data): + """Verify that the registration messages matches the expected.""" + for msg_type in self.emitter.get_types(): + self.assertEqual(msg_type, 'padatious:register_intent') + + self.assertEqual( + sorted(self.emitter.get_results(), + key=lambda d: sorted(d.items())), + sorted(expected_message_data, key=lambda d: sorted(d.items()))) + self.emitter.reset() + + def setUp(self): + self.emitter = MockEmitter() + + def test_register_intent(self): + intent_service = IntentServiceInterface(self.emitter) + filename = "/tmp/test.intent" + with open(filename, "w") as f: + f.write("this is a test\ntest the intent") + + intent_service.register_padatious_intent('test', filename, lang='en-US') + expected_data = {'file_name': '/tmp/test.intent', 'lang': 'en-US', 'name': 'test', + 'samples': ['this is a test', 'test the intent']} + self.check_emitter([expected_data]) + diff --git a/test/unittests/test_skill_api.py b/test/unittests/test_skill_api.py new file mode 100644 index 00000000..3f23cf04 --- /dev/null +++ b/test/unittests/test_skill_api.py @@ -0,0 +1,152 @@ +from unittest import TestCase, mock + +from ovos_workshop.skills.ovos import OVOSSkill +from ovos_bus_client.message import Message +from ovos_workshop.decorators import skill_api_method +from ovos_workshop.skills.api import SkillApi + + +class Skill(OVOSSkill): + """Test skill with registered API methods.""" + def __init__(self, *args, **kwargs): + self.registered_methods = {} + super().__init__(*args, **kwargs) + + def add_event(self, event_type, func, **kwargs): + """Mock handler of add_event, simply storing type and method. + + Used in testing to verify the wrapped methods + """ + self.registered_methods[event_type] = func + + @skill_api_method + def test_method(self): + """Documentation.""" + return True + + @skill_api_method + def test_method2(self, arg): + """Documentation.""" + return 'TestResult' + + +def load_test_skill(): + """Helper for setting up the test skill. + + Returns: + (MycroftSkill): created test skill + """ + bus = mock.Mock() + test_skill = Skill(skill_id = 'test_skill', bus=bus) + return test_skill + + +def create_skill_api_from_skill(skill): + """Helper creating an api from a skill. + + Args: + skill (MycroftSkill): Skill to create api from. + + Returns: + (SkillApi): API for the skill. + """ + SkillApi.connect_bus(skill.bus) + return SkillApi(skill.public_api) + + +class testSkillMethod(TestCase): + """Tests for the MycroftSkill class API setup.""" + def test_public_api_event(self): + """Test that public api event handler is created.""" + test_skill = load_test_skill() + self.assertTrue( + 'test_skill.public_api' in test_skill.registered_methods + ) + + def test_public_api(self): + """Test that the public_api structure matches the decorators.""" + test_skill = load_test_skill() + # Check that methods has been added + self.assertTrue('test_method' in test_skill.public_api) + self.assertTrue('test_method2' in test_skill.public_api) + # Test docstring + self.assertEqual(test_skill.public_api['test_method']['help'], + 'Documentation.') + # Test type + self.assertEqual(test_skill.public_api['test_method']['type'], + '{}.{}'.format(test_skill.skill_id, 'test_method')) + + def test_public_api_method(self): + """Verify message from wrapped api method.""" + test_skill = load_test_skill() + api_method = test_skill.registered_methods['test_skill.test_method'] + + # Call method + call_msg = Message('test_skill.test_method', + data={'args': [], 'kwargs': {}}) + api_method(call_msg) + # Check response sent on the bus is the same as the method's return + # value + response = test_skill.bus.emit.call_args[0][0] + self.assertEqual(response.data['result'], test_skill.test_method()) + + def test_public_api_request(self): + """Test public api request handling. + + Ensures that a request for the skill's available public api returns + expected content. + """ + test_skill = load_test_skill() + sent_message = None + + def capture_sent_message(message): + """Capture sent message.""" + nonlocal sent_message + sent_message = message + + test_skill.bus.emit.side_effect = capture_sent_message + get_api_method = test_skill.registered_methods['test_skill.public_api'] + request_api_msg = Message('test_skill.public_api') + + # Ensure that the sent public api contains the correct items + get_api_method(request_api_msg) + public_api = sent_message.data + self.assertTrue('test_method' in public_api) + self.assertTrue('test_method2' in public_api) + self.assertEqual(len(public_api), 2) + + +class TestApiObject(TestCase): + """Tests for the generated SkillApi objects.""" + def test_create_api_object(self): + """Check that expected methods are available.""" + test_skill = load_test_skill() + test_api = create_skill_api_from_skill(test_skill) + + hasattr(test_api, 'test_method') + hasattr(test_api, 'test_method2') + + def test_call_api_method(self): + """Ensure that calling the methods works as expected.""" + test_skill = load_test_skill() + test_api = create_skill_api_from_skill(test_skill) + + expected_response = 'all is good' + sent_message = None + + def capture_sent_message(message, timeout=3): + """Capture sent message and return expected response message.""" + nonlocal sent_message + sent_message = message + return Message('', data={'result': expected_response}) + + test_api.bus.wait_for_response.side_effect = capture_sent_message + + response = test_api.test_method('hello', person='you') + + # Verify response + self.assertEqual(response, expected_response) + # Verify sent message + self.assertEqual(sent_message.msg_type, 'test_skill.test_method') + self.assertEqual(sent_message.data['args'], ('hello',)) + self.assertEqual(sent_message.data['kwargs'], {'person': 'you'}) diff --git a/test/unittests/test_skill_loader.py b/test/unittests/test_skill_loader.py new file mode 100644 index 00000000..001299f3 --- /dev/null +++ b/test/unittests/test_skill_loader.py @@ -0,0 +1,192 @@ +# Copyright 2019 Mycroft AI Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Unit tests for the SkillLoader class.""" +import json +import unittest +from pathlib import Path +from unittest.mock import Mock + +from ovos_utils import classproperty +from ovos_utils.messagebus import FakeBus +from ovos_utils.process_utils import RuntimeRequirements +from ovos_workshop.skill_launcher import SkillLoader + +from ovos_workshop.skills.ovos import OVOSSkill + +ONE_MINUTE = 60 + + +class OfflineSkill(OVOSSkill): + @classproperty + def runtime_requirements(self): + return RuntimeRequirements(internet_before_load=False, + network_before_load=False, + requires_internet=False, + requires_network=False, + no_internet_fallback=True, + no_network_fallback=True) + + +class LANSkill(OVOSSkill): + @classproperty + def runtime_requirements(self): + scans_on_init = True + return RuntimeRequirements(internet_before_load=False, + network_before_load=scans_on_init, + requires_internet=False, + requires_network=True, + no_internet_fallback=True, + no_network_fallback=False) + + +class TestSkillNetwork(unittest.TestCase): + + def test_class_property(self): + self.assertEqual(OfflineSkill.runtime_requirements, + RuntimeRequirements(internet_before_load=False, + network_before_load=False, + requires_internet=False, + requires_network=False, + no_internet_fallback=True, + no_network_fallback=True) + ) + self.assertEqual(LANSkill.runtime_requirements, + RuntimeRequirements(internet_before_load=False, + network_before_load=True, + requires_internet=False, + requires_network=True, + no_internet_fallback=True, + no_network_fallback=False) + ) + self.assertEqual(OVOSSkill.runtime_requirements, + RuntimeRequirements() + ) + + +msgs = [] +bus = FakeBus() +bus.msgs = [] + + +def _handle(msg): + global bus + bus.msgs.append(json.loads(msg)) + + +bus.on("message", _handle) + + +class TestSkillLoader(unittest.TestCase): + skill_directory = Path('/tmp/test_skill') + skill_directory.mkdir(exist_ok=True) + for file_name in ('__init__.py', 'bar.py', '.foobar', 'bar.pyc'): + skill_directory.joinpath(file_name).touch() + + def test_skill_reload(self): + """Test reloading a skill that was modified.""" + bus.msgs = [] + loader = SkillLoader(bus, str(self.skill_directory)) + loader.instance = Mock() + loader.loaded = True + loader.load_attempted = False + loader.last_loaded = 10 + loader.instance.reload_skill = True + loader.instance.name = "MySkill" + loader.skill_id = 'test_skill' + + # Mock to return a known (Mock) skill instance + real_create_skill_instance = loader._create_skill_instance + + def _update_skill_instance(*args, **kwargs): + loader.instance = Mock() + loader.loaded = True + loader.load_attempted = True + loader.last_loaded = 100 + loader.skill_id = 'test_skill' + loader.instance.name = "MySkill" + return True + + loader._create_skill_instance = _update_skill_instance + + loader.reload() + + self.assertTrue(loader.load_attempted) + self.assertTrue(loader.loaded) + + self.assertListEqual( + ['mycroft.skills.shutdown', 'mycroft.skills.loaded'], + [m["type"] for m in bus.msgs] + ) + loader._create_skill_instance = real_create_skill_instance + + def test_skill_load(self): + loader = SkillLoader(bus, str(self.skill_directory)) + bus.msgs = [] + loader.instance = None + loader.loaded = False + loader.last_loaded = 0 + + # Mock to return a known (Mock) skill instance + real_create_skill_instance = loader._create_skill_instance + + def _update_skill_instance(*args, **kwargs): + loader.instance = Mock() + loader.loaded = True + loader.last_loaded = 100 + loader.skill_id = 'test_skill' + loader.instance.name = "MySkill" + return True + + loader._create_skill_instance = _update_skill_instance + + loader.load() + + self.assertTrue(loader.load_attempted) + self.assertTrue(loader.loaded) + + self.assertListEqual( + ['mycroft.skills.loaded'], + [m["type"] for m in bus.msgs] + ) + loader._create_skill_instance = real_create_skill_instance + + def test_skill_load_blacklisted(self): + """Skill should not be loaded if it is blacklisted""" + loader = SkillLoader(bus, str(self.skill_directory)) + loader.instance = Mock() + loader.loaded = False + loader.last_loaded = 0 + loader.skill_id = 'test_skill' + loader.name = "MySkill" + bus.msgs = [] + + config = dict(loader.config) + config['skills']['blacklisted_skills'] = ['test_skill'] + loader.config = config + self.assertEqual(loader.config['skills']['blacklisted_skills'], + ['test_skill']) + loader.skill_id = 'test_skill' + + loader.load() + + self.assertTrue(loader.load_attempted) + self.assertFalse(loader.loaded) + + self.assertListEqual( + ['mycroft.skills.loading_failure'], + [m["type"] for m in bus.msgs] + ) + + loader.config['skills']['blacklisted_skills'].remove('test_skill')