From 3fd6af1f6bab97d5bddce0c9b12cfe72fac5006d Mon Sep 17 00:00:00 2001 From: miro Date: Wed, 11 Sep 2024 20:14:44 +0100 Subject: [PATCH 1/4] feat:fallback_plugins similar to WakeWords, allows defining a plugin to load if the primary fails this is DIFFERENT from fallback TTS/STT, it isnt loaded at same time but instead when main plugin fails to load for any reason --- ovos_plugin_manager/g2p.py | 19 ++++---- ovos_plugin_manager/language.py | 74 ++++++++----------------------- ovos_plugin_manager/microphone.py | 10 ++++- ovos_plugin_manager/vad.py | 16 +++---- ovos_plugin_manager/wakewords.py | 10 ----- 5 files changed, 41 insertions(+), 88 deletions(-) diff --git a/ovos_plugin_manager/g2p.py b/ovos_plugin_manager/g2p.py index 64497a15..3d4c33f6 100644 --- a/ovos_plugin_manager/g2p.py +++ b/ovos_plugin_manager/g2p.py @@ -77,12 +77,6 @@ def get_g2p_config(config: Optional[dict] = None) -> dict: class OVOSG2PFactory: - """ replicates the base mycroft class, but uses only OPM enabled plugins""" - MAPPINGS = { - "dummy": "ovos-g2p-plugin-dummy", - "phoneme_guesser": "neon-g2p-plugin-phoneme-guesser", - "gruut": "neon-g2p-plugin-gruut" - } @staticmethod def get_class(config=None): @@ -97,15 +91,13 @@ def get_class(config=None): """ config = get_g2p_config(config) g2p_module = config.get("module") or 'dummy' - if g2p_module in OVOSG2PFactory.MAPPINGS: - g2p_module = OVOSG2PFactory.MAPPINGS[g2p_module] - if g2p_module == 'ovos-g2p-plugin-dummy': + if g2p_module == 'dummy': return Grapheme2PhonemePlugin return load_g2p_plugin(g2p_module) - @staticmethod - def create(config=None): + @classmethod + def create(cls, config=None): """Factory method to create a G2P engine based on configuration. The configuration file ``mycroft.conf`` contains a ``g2p`` section with @@ -117,11 +109,16 @@ def create(config=None): """ g2p_config = get_g2p_config(config) g2p_module = g2p_config.get('module', 'dummy') + fallback = g2p_config.get("fallback_module") try: clazz = OVOSG2PFactory.get_class(g2p_config) g2p = clazz(g2p_config) LOG.debug(f'Loaded plugin {g2p_module}') except Exception: LOG.exception('The selected G2P plugin could not be loaded.') + if fallback in config and fallback != g2p_module: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["module"] = fallback + return cls.create(config) raise return g2p diff --git a/ovos_plugin_manager/language.py b/ovos_plugin_manager/language.py index ebf2a25c..ebf02078 100644 --- a/ovos_plugin_manager/language.py +++ b/ovos_plugin_manager/language.py @@ -83,24 +83,7 @@ def get_lang_detect_module_configs(module_name: str): return load_plugin_configs(module_name, PluginConfigTypes.LANG_DETECT) -_fallback_lang_detect_plugin = "ovos-lang-detect-ngram-lm" -_fallback_translate_plugin = "ovos-translate-plugin-server" - - class OVOSLangDetectionFactory: - """ - replicates the base neon class, but uses only OPM enabled plugins - """ - MAPPINGS = { - "libretranslate": "libretranslate_detection_plug", - "google": "googletranslate_detection_plug", - "amazon": "amazontranslate_detection_plug", - "cld2": "cld2_plug", - "cld3": "cld3_plug", - "langdetect": "langdetect_plug", - "fastlang": "fastlang_plug", - "lingua_podre": "lingua_podre_plug" - } @staticmethod def get_class(config=None): @@ -120,12 +103,10 @@ def get_class(config=None): lang_module = config.get("detection_module", config.get("module")) if not lang_module: raise ValueError("`language.detection_module` not configured") - if lang_module in OVOSLangDetectionFactory.MAPPINGS: - lang_module = OVOSLangDetectionFactory.MAPPINGS[lang_module] return load_lang_detect_plugin(lang_module) - @staticmethod - def create(config=None) -> LanguageDetector: + @classmethod + def create(cls, config=None) -> LanguageDetector: """ Factory method to create a LangDetection engine based on configuration @@ -140,37 +121,25 @@ def create(config=None) -> LanguageDetector: if "language" in config: config = config["language"] lang_module = config.get("detection_module", config.get("module")) + cfg = config.get(lang_module, {}) + fallback = cfg.get("fallback_module") try: clazz = OVOSLangDetectionFactory.get_class(config) if clazz is None: raise ValueError(f"Failed to load module: {lang_module}") LOG.info(f'Loaded the Language Detection plugin {lang_module}') - if lang_module in OVOSLangDetectionFactory.MAPPINGS: - lang_module = OVOSLangDetectionFactory.MAPPINGS[lang_module] return clazz(config=get_plugin_config(config, "language", lang_module)) except Exception: - # The Language Detection backend failed to start, fall back if appropriate. - if lang_module != _fallback_lang_detect_plugin: - lang_module = _fallback_lang_detect_plugin - LOG.error(f'Language Detection plugin {lang_module} not found. ' - f'Falling back to {_fallback_lang_detect_plugin}') - clazz = load_lang_detect_plugin(_fallback_lang_detect_plugin) - if clazz: - return clazz(config=get_plugin_config(config, "language", - lang_module)) - + LOG.exception(f'Language Detection plugin {lang_module} could not be loaded!') + if fallback in config and fallback != lang_module: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["detection_module"] = fallback + return cls.create(config) raise class OVOSLangTranslationFactory: - """ replicates the base neon class, but uses only OPM enabled plugins""" - MAPPINGS = { - "libretranslate": "libretranslate_plug", - "google": "googletranslate_plug", - "amazon": "amazontranslate_plug", - "apertium": "apertium_plug" - } @staticmethod def get_class(config=None): @@ -190,12 +159,10 @@ def get_class(config=None): lang_module = config.get("translation_module", config.get("module")) if not lang_module: raise ValueError("`language.translation_module` not configured") - if lang_module in OVOSLangTranslationFactory.MAPPINGS: - lang_module = OVOSLangTranslationFactory.MAPPINGS[lang_module] return load_tx_plugin(lang_module) - @staticmethod - def create(config=None) -> LanguageTranslator: + @classmethod + def create(cls, config=None) -> LanguageTranslator: """ Factory method to create a LangTranslation engine based on configuration @@ -210,24 +177,19 @@ def create(config=None) -> LanguageTranslator: if "language" in config: config = config["language"] lang_module = config.get("translation_module", config.get("module")) + cfg = config.get(lang_module, {}) + fallback = cfg.get("fallback_module") try: clazz = OVOSLangTranslationFactory.get_class(config) if clazz is None: raise ValueError(f"Failed to load module: {lang_module}") LOG.info(f'Loaded the Language Translation plugin {lang_module}') - if lang_module in OVOSLangTranslationFactory.MAPPINGS: - lang_module = OVOSLangTranslationFactory.MAPPINGS[lang_module] return clazz(config=get_plugin_config(config, "language", lang_module)) except Exception: - # The Language Translation backend failed to start, fall back if appropriate. - if lang_module != _fallback_translate_plugin: - lang_module = _fallback_translate_plugin - LOG.error(f'Language Translation plugin {lang_module} ' - f'not found. Falling back to {_fallback_translate_plugin}') - clazz = load_tx_plugin(_fallback_translate_plugin) - if clazz: - return clazz(config=get_plugin_config(config, "language", - lang_module)) - + LOG.exception(f'Language Translation plugin {lang_module} could not be loaded!') + if fallback in config and fallback != lang_module: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["translation_module"] = fallback + return cls.create(config) raise diff --git a/ovos_plugin_manager/microphone.py b/ovos_plugin_manager/microphone.py index 2e8f0c84..f1982b9c 100644 --- a/ovos_plugin_manager/microphone.py +++ b/ovos_plugin_manager/microphone.py @@ -49,8 +49,8 @@ def get_class(config=None): microphone_module = config.get("module") return load_microphone_plugin(microphone_module) - @staticmethod - def create(config=None): + @classmethod + def create(cls, config=None): """Factory method to create a microphone engine based on configuration. The configuration file ``mycroft.conf`` contains a ``microphone`` section with @@ -62,6 +62,7 @@ def create(config=None): """ microphone_config = get_microphone_config(config) microphone_module = microphone_config.get('module') + fallback = microphone_config.get("fallback_module") try: clazz = OVOSMicrophoneFactory.get_class(microphone_config) # Note that configuration is expanded for this class of plugins @@ -69,9 +70,14 @@ def create(config=None): # as other plugin types microphone_config.pop('lang') microphone_config.pop('module') + microphone_config.pop('fallback_module') microphone = clazz(**microphone_config) LOG.debug(f'Loaded microphone plugin {microphone_module}') except Exception: LOG.exception('The selected microphone plugin could not be loaded.') + if fallback in config and fallback != microphone_module: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["module"] = fallback + return cls.create(config) raise return microphone diff --git a/ovos_plugin_manager/vad.py b/ovos_plugin_manager/vad.py index 966c3edf..5fbf93fb 100644 --- a/ovos_plugin_manager/vad.py +++ b/ovos_plugin_manager/vad.py @@ -61,11 +61,6 @@ def get_vad_config(config: dict = None) -> dict: class OVOSVADFactory: - """ replicates the base mycroft class, but uses only OPM enabled plugins""" - MAPPINGS = { - "silero": "ovos-vad-plugin-silero", - "webrtcvad": "ovos-vad-plugin-webrtcvad" - } @staticmethod def get_class(config=None): @@ -84,12 +79,10 @@ def get_class(config=None): raise ValueError(f"VAD Plugin not configured in: {config}") if vad_module == "dummy": return VADEngine - if vad_module in OVOSVADFactory.MAPPINGS: - vad_module = OVOSVADFactory.MAPPINGS[vad_module] return load_vad_plugin(vad_module) - @staticmethod - def create(config=None): + @classmethod + def create(cls, config=None): """Factory method to create a VAD engine based on configuration. The configuration file ``mycroft.conf`` contains a ``VAD`` section with @@ -101,6 +94,7 @@ def create(config=None): """ vad_config = get_vad_config(config) plugin = vad_config.get("module") + fallback = vad_config.get("fallback_module") if not plugin: raise ValueError(f"VAD Plugin not configured in: {vad_config}") try: @@ -111,4 +105,8 @@ def create(config=None): return clazz(plugin_config) except Exception: LOG.exception(f'VAD plugin {plugin} could not be loaded!') + if fallback in config and fallback != plugin: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["module"] = fallback + return cls.create(config) raise diff --git a/ovos_plugin_manager/wakewords.py b/ovos_plugin_manager/wakewords.py index f098b64a..fa777217 100644 --- a/ovos_plugin_manager/wakewords.py +++ b/ovos_plugin_manager/wakewords.py @@ -105,14 +105,6 @@ def get_wws(scan=False): class OVOSWakeWordFactory: - """ replicates the base mycroft class, but uses only OPM enabled plugins""" - MAPPINGS = { - "dummy": "ovos-ww-plugin-dummy", - "pocketsphinx": "ovos-ww-plugin-pocketsphinx", - "precise": "ovos-ww-plugin-precise", - "snowboy": "ovos-ww-plugin-snowboy", - "porcupine": "porcupine_wakeword_plug" - } @staticmethod def get_class(hotword: str, config: Optional[dict] = None) -> type: @@ -128,8 +120,6 @@ def get_class(hotword: str, config: Optional[dict] = None) -> type: f"Returning base HotWordEngine") return HotWordEngine ww_module = hotword_config[hotword]["module"] - if ww_module in OVOSWakeWordFactory.MAPPINGS: - ww_module = OVOSWakeWordFactory.MAPPINGS[ww_module] return load_wake_word_plugin(ww_module) @staticmethod From 311cc3a9635020129eb117cddf0f8f46cb6bd872 Mon Sep 17 00:00:00 2001 From: miro Date: Wed, 11 Sep 2024 21:48:10 +0100 Subject: [PATCH 2/4] unittests --- ovos_plugin_manager/g2p.py | 2 + ovos_plugin_manager/microphone.py | 5 ++- ovos_plugin_manager/vad.py | 19 ++++---- test/unittests/test_g2p.py | 75 +++++++++++++++++++++++++++++-- test/unittests/test_microphone.py | 46 +++++++++++++++++++ test/unittests/test_vad.py | 43 +++++++++++++++--- 6 files changed, 171 insertions(+), 19 deletions(-) diff --git a/ovos_plugin_manager/g2p.py b/ovos_plugin_manager/g2p.py index 3d4c33f6..8a8bcdfe 100644 --- a/ovos_plugin_manager/g2p.py +++ b/ovos_plugin_manager/g2p.py @@ -107,6 +107,8 @@ def create(cls, config=None): "module": } """ + if "g2p" in config: + config = config["g2p"] g2p_config = get_g2p_config(config) g2p_module = g2p_config.get('module', 'dummy') fallback = g2p_config.get("fallback_module") diff --git a/ovos_plugin_manager/microphone.py b/ovos_plugin_manager/microphone.py index f1982b9c..f649b130 100644 --- a/ovos_plugin_manager/microphone.py +++ b/ovos_plugin_manager/microphone.py @@ -60,6 +60,8 @@ def create(cls, config=None): "module": } """ + if "microphone" in config: + config = config["microphone"] microphone_config = get_microphone_config(config) microphone_module = microphone_config.get('module') fallback = microphone_config.get("fallback_module") @@ -70,7 +72,8 @@ def create(cls, config=None): # as other plugin types microphone_config.pop('lang') microphone_config.pop('module') - microphone_config.pop('fallback_module') + if fallback: + microphone_config.pop('fallback_module') microphone = clazz(**microphone_config) LOG.debug(f'Loaded microphone plugin {microphone_module}') except Exception: diff --git a/ovos_plugin_manager/vad.py b/ovos_plugin_manager/vad.py index 5fbf93fb..5d4b2589 100644 --- a/ovos_plugin_manager/vad.py +++ b/ovos_plugin_manager/vad.py @@ -92,16 +92,19 @@ def create(cls, config=None): "module": } """ - vad_config = get_vad_config(config) - plugin = vad_config.get("module") - fallback = vad_config.get("fallback_module") + if "listener" in config: + config = config["listener"] + if "VAD" in config: + config = config["VAD"] + plugin = config.get("module") if not plugin: - raise ValueError(f"VAD Plugin not configured in: {vad_config}") + raise ValueError(f"VAD Plugin not configured in: {config}") + + plugin_config = config.get(plugin, {}) + fallback = plugin_config.get("fallback_module") + try: - clazz = OVOSVADFactory.get_class(vad_config) - # module name not expected in config; don't change passed config - plugin_config = dict(vad_config) - plugin_config.pop("module") + clazz = OVOSVADFactory.get_class(config) return clazz(plugin_config) except Exception: LOG.exception(f'VAD plugin {plugin} could not be loaded!') diff --git a/test/unittests/test_g2p.py b/test/unittests/test_g2p.py index 40b1964a..50289fbe 100644 --- a/test/unittests/test_g2p.py +++ b/test/unittests/test_g2p.py @@ -1,9 +1,24 @@ import unittest - -from unittest.mock import patch +from copy import deepcopy from enum import Enum +from unittest.mock import patch, Mock + from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes +_TEST_CONFIG = { + "g2p": { + "module": "good", + "good": {"a": "b"} + } +} +_FALLBACK_CONFIG = { + "g2p": { + "module": "bad", + "bad": {"fallback_module": "good"}, + "good": {"a": "b"} + } +} + class TestG2PTemplate(unittest.TestCase): def test_phoneme_alphabet(self): @@ -72,5 +87,57 @@ def test_get_config(self, get_config): class TestG2PFactory(unittest.TestCase): - from ovos_plugin_manager.g2p import OVOSG2PFactory - # TODO + def test_create_g2p(self): + from ovos_plugin_manager.g2p import OVOSG2PFactory + real_get_class = OVOSG2PFactory.get_class + mock_class = Mock() + call_args = None + + def _copy_args(*args): + nonlocal call_args + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSG2PFactory.get_class = mock_get_class + + OVOSG2PFactory.create(config=_TEST_CONFIG) + mock_get_class.assert_called_once() + self.assertEqual(call_args, ({**_TEST_CONFIG['g2p']['good'], + **{"module": "good", + "lang": "en-us"}},)) + mock_class.assert_called_once_with({**_TEST_CONFIG['g2p']['good'], + **{"module": "good", + "lang": "en-us"}}) + OVOSG2PFactory.get_class = real_get_class + + def test_create_fallback(self): + from ovos_plugin_manager.g2p import OVOSG2PFactory + real_get_class = OVOSG2PFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSG2PFactory.get_class = mock_get_class + + OVOSG2PFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args, ({**_FALLBACK_CONFIG['g2p']['good'], + **{"module": "good", + "lang": "en-us"}},)) + self.assertEqual(bad_call_args, ({**_FALLBACK_CONFIG['g2p']['bad'], + **{"module": "bad", + "lang": "en-us"}},)) + mock_class.assert_called_once_with({**_FALLBACK_CONFIG['g2p']['good'], + **{"module": "good", + "lang": "en-us"}}) + OVOSG2PFactory.get_class = real_get_class diff --git a/test/unittests/test_microphone.py b/test/unittests/test_microphone.py index dbc203ef..ff690542 100644 --- a/test/unittests/test_microphone.py +++ b/test/unittests/test_microphone.py @@ -19,6 +19,23 @@ } } } +_FALLBACK_CONFIG = { + "lang": "en-us", + "microphone": { + "module": "bad", + "bad": { + "sample_width": 1, + "sample_channels": 1, + "chunk_size": 2048, + "fallback_module": "dummy" + }, + "dummy": { + "sample_width": 1, + "sample_channels": 1, + "chunk_size": 2048 + }, + } +} class TestMicrophoneTemplate(unittest.TestCase): @@ -145,6 +162,35 @@ def _copy_args(*args): mock_class.assert_called_once_with(**_TEST_CONFIG['microphone']['dummy']) OVOSMicrophoneFactory.get_class = real_get_class + def test_create_microphone_fallback(self): + from ovos_plugin_manager.microphone import OVOSMicrophoneFactory + real_get_class = OVOSMicrophoneFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSMicrophoneFactory.get_class = mock_get_class + + OVOSMicrophoneFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args, ({**_FALLBACK_CONFIG['microphone']['dummy'], + **{"module": "dummy", + "lang": "en-us"}},)) + self.assertEqual(bad_call_args, ({**_FALLBACK_CONFIG['microphone']['bad'], + **{"module": "bad", + "lang": "en-us"}},)) + mock_class.assert_called_once_with(**_FALLBACK_CONFIG['microphone']['dummy']) + OVOSMicrophoneFactory.get_class = real_get_class + @patch("ovos_plugin_manager.utils.load_plugin") def test_get_class(self, load_plugin): mock = Mock() diff --git a/test/unittests/test_vad.py b/test/unittests/test_vad.py index e2664a00..7545fd38 100644 --- a/test/unittests/test_vad.py +++ b/test/unittests/test_vad.py @@ -1,6 +1,7 @@ import unittest -from unittest.mock import patch, Mock from copy import copy, deepcopy +from unittest.mock import patch, Mock + from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes _TEST_CONFIG = { @@ -17,6 +18,13 @@ } } } +_FALLBACK_CONFIG = { + "VAD": { + "module": "bad", + "bad": {"fallback_module": "good"}, + "good": {"a": "b"} + } +} class TestVADTemplate(unittest.TestCase): @@ -74,10 +82,8 @@ def test_create(self): OVOSVADFactory.get_class = mock_get_class OVOSVADFactory.create(config=_TEST_CONFIG) - mock_get_class.assert_called_once_with( - {**_TEST_CONFIG['listener']['VAD']['dummy'], **{"module": "dummy"}}) - mock_class.assert_called_once_with( - _TEST_CONFIG['listener']["VAD"]['dummy']) + mock_get_class.assert_called_once_with(_TEST_CONFIG['listener']['VAD']) + mock_class.assert_called_once_with(_TEST_CONFIG['listener']["VAD"]['dummy']) # Test invalid config with self.assertRaises(ValueError): @@ -85,6 +91,31 @@ def test_create(self): OVOSVADFactory.get_class = real_get_class + def test_create_fallback(self): + from ovos_plugin_manager.vad import OVOSVADFactory + real_get_class = OVOSVADFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSVADFactory.get_class = mock_get_class + + OVOSVADFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args[0]["module"], 'good') + self.assertEqual(bad_call_args[0]["module"], 'bad') + mock_class.assert_called_once_with(_FALLBACK_CONFIG['VAD']['good']) + OVOSVADFactory.get_class = real_get_class + @patch("ovos_plugin_manager.utils.load_plugin") def test_get_class(self, load_plugin): mock = Mock() @@ -121,7 +152,7 @@ def test_get_vad_config(self): webrtc_config = get_vad_config(config) self.assertEqual(webrtc_config, {**_TEST_CONFIG['listener']['VAD'] - ['ovos-vad-plugin-webrtcvad'], + ['ovos-vad-plugin-webrtcvad'], **{'module': 'ovos-vad-plugin-webrtcvad'}}) config = copy(_TEST_CONFIG) config['VAD'] = {'module': 'fake'} From 2c50258e6f7cd17f9421aa6cfe32317b6755a0ba Mon Sep 17 00:00:00 2001 From: miro Date: Wed, 11 Sep 2024 22:16:58 +0100 Subject: [PATCH 3/4] unittests --- ovos_plugin_manager/language.py | 2 + test/unittests/test_g2p.py | 8 +- test/unittests/test_language.py | 204 ++++++++++++++---------------- test/unittests/test_microphone.py | 8 +- 4 files changed, 102 insertions(+), 120 deletions(-) diff --git a/ovos_plugin_manager/language.py b/ovos_plugin_manager/language.py index ebf02078..3caee3e3 100644 --- a/ovos_plugin_manager/language.py +++ b/ovos_plugin_manager/language.py @@ -124,6 +124,7 @@ def create(cls, config=None) -> LanguageDetector: cfg = config.get(lang_module, {}) fallback = cfg.get("fallback_module") try: + config["module"] = lang_module clazz = OVOSLangDetectionFactory.get_class(config) if clazz is None: raise ValueError(f"Failed to load module: {lang_module}") @@ -180,6 +181,7 @@ def create(cls, config=None) -> LanguageTranslator: cfg = config.get(lang_module, {}) fallback = cfg.get("fallback_module") try: + config["module"] = lang_module clazz = OVOSLangTranslationFactory.get_class(config) if clazz is None: raise ValueError(f"Failed to load module: {lang_module}") diff --git a/test/unittests/test_g2p.py b/test/unittests/test_g2p.py index 50289fbe..51a068e8 100644 --- a/test/unittests/test_g2p.py +++ b/test/unittests/test_g2p.py @@ -131,12 +131,8 @@ def _copy_args(*args): OVOSG2PFactory.create(config=_FALLBACK_CONFIG) mock_get_class.assert_called() - self.assertEqual(call_args, ({**_FALLBACK_CONFIG['g2p']['good'], - **{"module": "good", - "lang": "en-us"}},)) - self.assertEqual(bad_call_args, ({**_FALLBACK_CONFIG['g2p']['bad'], - **{"module": "bad", - "lang": "en-us"}},)) + self.assertEqual(call_args[0]["module"], 'good') + self.assertEqual(bad_call_args[0]["module"], 'bad') mock_class.assert_called_once_with({**_FALLBACK_CONFIG['g2p']['good'], **{"module": "good", "lang": "en-us"}}) diff --git a/test/unittests/test_language.py b/test/unittests/test_language.py index ed46e1e3..ebd3195b 100644 --- a/test/unittests/test_language.py +++ b/test/unittests/test_language.py @@ -3,6 +3,22 @@ from unittest.mock import patch, Mock from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes +_TEST_CONFIG = { + "language": { + "detection_module": "good", + "translation_module": "good", + "good": {"a": "b"} + } +} +_FALLBACK_CONFIG = { + "language": { + "detection_module": "bad", + "translation_module": "bad", + "bad": {"fallback_module": "good"}, + "good": {"a": "b"} + } +} + class TestLanguageTemplate(unittest.TestCase): def test_language_detector(self): @@ -83,39 +99,15 @@ def test_get_module_configs(self, load_plugin_configs): class TestLangDetectionFactory(unittest.TestCase): - def test_mappings(self): - from ovos_plugin_manager.language import OVOSLangDetectionFactory - self.assertIsInstance(OVOSLangDetectionFactory.MAPPINGS, dict) - for conf in OVOSLangDetectionFactory.MAPPINGS: - self.assertIsInstance(conf, str) - self.assertIsInstance(OVOSLangDetectionFactory.MAPPINGS[conf], - str) - self.assertNotEqual(conf, OVOSLangDetectionFactory.MAPPINGS[conf]) - @patch("ovos_plugin_manager.language.load_lang_detect_plugin") - @patch("ovos_plugin_manager.language.Configuration") - def test_get_class(self, config, load_plugin): + def test_get_class(self, load_plugin): from ovos_plugin_manager.language import OVOSLangDetectionFactory - test_config = {"language": { - "detection_module": "libretranslate" - }} + mock_class = Mock() - config.return_value = test_config load_plugin.return_value = mock_class - # Test mapped plugin from config - self.assertEquals(OVOSLangDetectionFactory.get_class(), mock_class) - load_plugin.assert_called_with("libretranslate_detection_plug") - - # Test explicitly specified mapped plugin - conf = {"module": "google"} - self.assertEquals(OVOSLangDetectionFactory.get_class(conf), mock_class) - load_plugin.assert_called_with("googletranslate_detection_plug") - - # Test unmapped plugin - conf = {"language": {"detection_module": "real-detect-plug"}} - self.assertEquals(OVOSLangDetectionFactory.get_class(conf), mock_class) - load_plugin.assert_called_with("real-detect-plug") + self.assertEqual(OVOSLangDetectionFactory.get_class(_TEST_CONFIG), mock_class) + load_plugin.assert_called_with("good") # Test invalid module config conf = {"language": {}} @@ -128,78 +120,64 @@ def test_create(self, config, load_plugin): from ovos_plugin_manager.language import OVOSLangDetectionFactory plug_instance = Mock() mock_plugin = Mock(return_value=plug_instance) - default_config = { - "lang": "core_lang", - "language": { - "detection_module": "google", - "lang": "detect" - } - } - config.return_value = default_config + + config.return_value = _TEST_CONFIG load_plugin.return_value = mock_plugin # Create from core config plug = OVOSLangDetectionFactory.create() - load_plugin.assert_called_once_with('googletranslate_detection_plug') - mock_plugin.assert_called_once_with( - config={'lang': "detect", - "module": "googletranslate_detection_plug"}) + load_plugin.assert_called_once_with('good') + mock_plugin.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) self.assertEquals(plug_instance, plug) # Create plugin fully specified in passed config - config_with_module = {"detection_module": "detect-plugin", - "lang": "lang"} - plug = OVOSLangDetectionFactory.create(config_with_module) - load_plugin.assert_called_with("detect-plugin") - mock_plugin.assert_called_with(config={"module": "detect-plugin", - "lang": "lang"}) + mock_plugin.reset_mock() + plug = OVOSLangDetectionFactory.create(_TEST_CONFIG) + load_plugin.assert_called_with("good") + mock_plugin.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) self.assertEquals(plug_instance, plug) - # Create plugin fallback module config parsing - config_with_fallback_module = {"module": "test-detect-plugin", - "lang": "lang"} - plug = OVOSLangDetectionFactory.create(config_with_fallback_module) - load_plugin.assert_called_with("test-detect-plugin") - mock_plugin.assert_called_with(config=config_with_fallback_module) - self.assertEquals(plug_instance, plug) - # TODO: Test exception handling fallback to libretranslate + def test_create_fallback(self): + from ovos_plugin_manager.language import OVOSLangDetectionFactory + real_get_class = OVOSLangDetectionFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + from copy import deepcopy + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSLangDetectionFactory.get_class = mock_get_class + + OVOSLangDetectionFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args[0]["module"], 'good') + self.assertEqual(bad_call_args[0]["module"], 'bad') + mock_class.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) + OVOSLangDetectionFactory.get_class = real_get_class class TestLangTranslationFactory(unittest.TestCase): - def test_mappings(self): - from ovos_plugin_manager.language import OVOSLangTranslationFactory - self.assertIsInstance(OVOSLangTranslationFactory.MAPPINGS, dict) - for conf in OVOSLangTranslationFactory.MAPPINGS: - self.assertIsInstance(conf, str) - self.assertIsInstance(OVOSLangTranslationFactory.MAPPINGS[conf], - str) - self.assertNotEqual(conf, OVOSLangTranslationFactory.MAPPINGS[conf]) @patch("ovos_plugin_manager.language.load_tx_plugin") - @patch("ovos_plugin_manager.language.Configuration") - def test_get_class(self, config, load_plugin): + def test_get_class(self, load_plugin): from ovos_plugin_manager.language import OVOSLangTranslationFactory - test_config = {"language": { - "translation_module": "libretranslate" - }} + mock_class = Mock() - config.return_value = test_config load_plugin.return_value = mock_class - # Test mapped plugin from config - self.assertEquals(OVOSLangTranslationFactory.get_class(), mock_class) - load_plugin.assert_called_with("libretranslate_plug") - - # Test explicitly specified mapped plugin - conf = {"module": "google"} - self.assertEquals(OVOSLangTranslationFactory.get_class(conf), - mock_class) - load_plugin.assert_called_with("googletranslate_plug") - - # Test unmapped plugin - conf = {"language": {"translation_module": "real-detect-plug"}} - self.assertEquals(OVOSLangTranslationFactory.get_class(conf), mock_class) - load_plugin.assert_called_with("real-detect-plug") + self.assertEqual(OVOSLangTranslationFactory.get_class(_TEST_CONFIG), mock_class) + load_plugin.assert_called_with("good") # Test invalid module config conf = {"language": {}} @@ -212,38 +190,48 @@ def test_create(self, config, load_plugin): from ovos_plugin_manager.language import OVOSLangTranslationFactory plug_instance = Mock() mock_plugin = Mock(return_value=plug_instance) - default_config = { - "lang": "core_lang", - "language": { - "translation_module": "google", - "lang": "tx" - } - } - config.return_value = default_config + + config.return_value = _TEST_CONFIG load_plugin.return_value = mock_plugin # Create from core config plug = OVOSLangTranslationFactory.create() - load_plugin.assert_called_once_with('googletranslate_plug') - mock_plugin.assert_called_once_with( - config={'lang': "tx", "module": "googletranslate_plug"}) + load_plugin.assert_called_once_with('good') + mock_plugin.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) self.assertEquals(plug_instance, plug) # Create plugin fully specified in passed config - config_with_module = {"translation_module": "translate-plugin", - "lang": "lang"} - plug = OVOSLangTranslationFactory.create(config_with_module) - load_plugin.assert_called_with("translate-plugin") - mock_plugin.assert_called_with(config={"module": "translate-plugin", - "lang": "lang"}) - self.assertEquals(plug_instance, plug) - - # Create plugin fallback module config parsing - config_with_fallback_module = {"module": "test-translate-plugin", - "lang": "lang"} - plug = OVOSLangTranslationFactory.create(config_with_fallback_module) - load_plugin.assert_called_with("test-translate-plugin") - mock_plugin.assert_called_with(config=config_with_fallback_module) + mock_plugin.reset_mock() + plug = OVOSLangTranslationFactory.create(_TEST_CONFIG) + load_plugin.assert_called_with("good") + mock_plugin.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) self.assertEquals(plug_instance, plug) - # TODO: Test exception handling fallback to libretranslate + def test_create_fallback(self): + from ovos_plugin_manager.language import OVOSLangTranslationFactory + real_get_class = OVOSLangTranslationFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + from copy import deepcopy + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSLangTranslationFactory.get_class = mock_get_class + + OVOSLangTranslationFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args[0]["module"], 'good') + self.assertEqual(bad_call_args[0]["module"], 'bad') + mock_class.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) + OVOSLangTranslationFactory.get_class = real_get_class diff --git a/test/unittests/test_microphone.py b/test/unittests/test_microphone.py index ff690542..50001136 100644 --- a/test/unittests/test_microphone.py +++ b/test/unittests/test_microphone.py @@ -182,12 +182,8 @@ def _copy_args(*args): OVOSMicrophoneFactory.create(config=_FALLBACK_CONFIG) mock_get_class.assert_called() - self.assertEqual(call_args, ({**_FALLBACK_CONFIG['microphone']['dummy'], - **{"module": "dummy", - "lang": "en-us"}},)) - self.assertEqual(bad_call_args, ({**_FALLBACK_CONFIG['microphone']['bad'], - **{"module": "bad", - "lang": "en-us"}},)) + self.assertEqual(call_args[0]["module"], 'dummy') + self.assertEqual(bad_call_args[0]["module"], 'bad') mock_class.assert_called_once_with(**_FALLBACK_CONFIG['microphone']['dummy']) OVOSMicrophoneFactory.get_class = real_get_class From 7fd0b6fc6279c919f83b579f828921d5c08e8419 Mon Sep 17 00:00:00 2001 From: miro Date: Wed, 11 Sep 2024 22:20:04 +0100 Subject: [PATCH 4/4] unittests --- test/unittests/test_wakewords.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unittests/test_wakewords.py b/test/unittests/test_wakewords.py index a07eb2c8..ca54aa5d 100644 --- a/test/unittests/test_wakewords.py +++ b/test/unittests/test_wakewords.py @@ -11,7 +11,7 @@ "active": True }, "hey_mycroft": { - "module": "precise", + "module": "ovos-ww-plugin-precise", "listen": True, "active": True } @@ -103,7 +103,7 @@ def test_create_hotword(self): OVOSWakeWordFactory.load_module = mock_load OVOSWakeWordFactory.create_hotword(config=_TEST_CONFIG) - mock_load.assert_called_once_with("precise", "hey_mycroft", + mock_load.assert_called_once_with("ovos-ww-plugin-precise", "hey_mycroft", _TEST_CONFIG["hotwords"] ['hey_mycroft'], "en-us", None) @@ -148,7 +148,7 @@ def test_load_module(self): mock_return = Mock() mock_get_class.return_value = mock_return module = OVOSWakeWordFactory.load_module( - "precise", "hey_mycroft", _TEST_CONFIG['hotwords']['hey_mycroft'], + "ovos-ww-plugin-precise", "hey_mycroft", _TEST_CONFIG['hotwords']['hey_mycroft'], 'en-us') mock_get_class.assert_called_once_with( "hey_mycroft", {"lang": "en-us", "hotwords": {