From decb77a4378bbb7d445154cb4799b265346ae2b2 Mon Sep 17 00:00:00 2001 From: Joshua Moore Date: Sun, 1 Mar 2020 20:56:54 -0700 Subject: [PATCH 01/96] Fixed typo "Devloper" to "Developer" --- dev_setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_setup.sh b/dev_setup.sh index 55c067a0beb8..3c59bc427b0d 100755 --- a/dev_setup.sh +++ b/dev_setup.sh @@ -274,7 +274,7 @@ fi" > ~/.profile_mycroft # Add PEP8 pre-commit hook sleep 0.5 echo ' -(Devloper) Do you want to automatically check code-style when submitting code. +(Developer) Do you want to automatically check code-style when submitting code. If unsure answer yes. ' if get_YN ; then From 58f0ac8b9e8277bab1fb93a03e0a604d48275a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 24 Feb 2020 14:05:33 +0100 Subject: [PATCH 02/96] Simplify the converse callings Remove the use of the separate error message and use the wait_for_reply method to get the converse result. The error message is left to guarantee compatibility. --- mycroft/skills/intent_service.py | 34 +++--------- mycroft/skills/skill_manager.py | 22 +++++--- test/unittests/skills/test_intent_service.py | 58 +++++++++----------- test/unittests/skills/test_skill_manager.py | 5 +- 4 files changed, 52 insertions(+), 67 deletions(-) diff --git a/mycroft/skills/intent_service.py b/mycroft/skills/intent_service.py index c7d2b14e6f31..93f348109b61 100644 --- a/mycroft/skills/intent_service.py +++ b/mycroft/skills/intent_service.py @@ -175,8 +175,6 @@ def __init__(self, bus): self.bus.on('remove_context', self.handle_remove_context) self.bus.on('clear_context', self.handle_clear_context) # Converse method - self.bus.on('skill.converse.response', self.handle_converse_response) - self.bus.on('skill.converse.error', self.handle_converse_error) self.bus.on('mycroft.speech.recognition.unknown', self.reset_converse) self.bus.on('mycroft.skills.loaded', self.update_skill_name_dict) @@ -186,9 +184,6 @@ def add_active_skill_handler(message): self.bus.on('active_skill_request', add_active_skill_handler) self.active_skills = [] # [skill_id , timestamp] self.converse_timeout = 5 # minutes to prune active_skills - self.waiting_for_converse = False - self.converse_result = False - self.converse_skill_id = "" # Intents API self.registered_intents = [] @@ -228,33 +223,20 @@ def reset_converse(self, message): self.do_converse(None, skill[0], lang, message) def do_converse(self, utterances, skill_id, lang, message): - self.waiting_for_converse = True - self.converse_result = False - self.converse_skill_id = skill_id - self.bus.emit(message.reply("skill.converse.request", { + converse_msg = (message.reply("skill.converse.request", { "skill_id": skill_id, "utterances": utterances, "lang": lang})) - start_time = time.time() - t = 0 - while self.waiting_for_converse and t < 5: - t = time.time() - start_time - time.sleep(0.1) - self.waiting_for_converse = False - self.converse_skill_id = "" - return self.converse_result + result = self.bus.wait_for_response(converse_msg, + 'skill.converse.response') + if result and 'error' in result.data: + self.handle_converse_error(result) + return False + else: + return result.data.get('result', False) def handle_converse_error(self, message): skill_id = message.data["skill_id"] if message.data["error"] == "skill id does not exist": self.remove_active_skill(skill_id) - if skill_id == self.converse_skill_id: - self.converse_result = False - self.waiting_for_converse = False - - def handle_converse_response(self, message): - skill_id = message.data["skill_id"] - if skill_id == self.converse_skill_id: - self.converse_result = message.data.get("result", False) - self.waiting_for_converse = False def remove_active_skill(self, skill_id): for skill in self.active_skills: diff --git a/mycroft/skills/skill_manager.py b/mycroft/skills/skill_manager.py index 6ddb6e9412c8..63ef27e75c12 100644 --- a/mycroft/skills/skill_manager.py +++ b/mycroft/skills/skill_manager.py @@ -406,7 +406,10 @@ def handle_converse_request(self, message): self._emit_converse_error(message, skill_id, error_message) break try: - self._emit_converse_response(message, skill_loader) + utterances = message.data['utterances'] + lang = message.data['lang'] + result = skill_loader.instance.converse(utterances, lang) + self._emit_converse_response(result, message, skill_loader) except Exception: error_message = 'exception in converse method' LOG.exception(error_message) @@ -419,16 +422,17 @@ def handle_converse_request(self, message): self._emit_converse_error(message, skill_id, error_message) def _emit_converse_error(self, message, skill_id, error_msg): - reply = message.reply( - 'skill.converse.error', - data=dict(skill_id=skill_id, error=error_msg) - ) + """Emit a message reporting the error back to the intent service.""" + reply = message.reply('skill.converse.response', + data=dict(skill_id=skill_id, error=error_msg)) + self.bus.emit(reply) + # Also emit the old error message to keep compatibility + # TODO Remove in 20.08 + reply = message.reply('skill.converse.error', + data=dict(skill_id=skill_id, error=error_msg)) self.bus.emit(reply) - def _emit_converse_response(self, message, skill_loader): - utterances = message.data['utterances'] - lang = message.data['lang'] - result = skill_loader.instance.converse(utterances, lang) + def _emit_converse_response(self, result, message, skill_loader): reply = message.reply( 'skill.converse.response', data=dict(skill_id=skill_loader.skill_id, result=result) diff --git a/test/unittests/skills/test_intent_service.py b/test/unittests/skills/test_intent_service.py index 402057e4b2ab..e2c340108382 100644 --- a/test/unittests/skills/test_intent_service.py +++ b/test/unittests/skills/test_intent_service.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from threading import Thread -import time from unittest import TestCase, mock from mycroft.messagebus import Message @@ -89,27 +87,22 @@ def test_converse(self): Also check that the skill that handled the query is moved to the top of the active skill list. """ - result = None + def response(message, return_msg_type): + c64 = Message(return_msg_type, {'skill_id': 'c64_skill', + 'result': False}) + atari = Message(return_msg_type, {'skill_id': 'atari_skill', + 'result': True}) + msgs = {'c64_skill': c64, 'atari_skill': atari} - def runner(utterances, lang, message): - nonlocal result - result = self.intent_service._converse(utterances, lang, message) + return msgs[message.data['skill_id']] + + self.intent_service.bus.wait_for_response.side_effect = response hello = ['hello old friend'] utterance_msg = Message('recognizer_loop:utterance', data={'lang': 'en-US', 'utterances': hello}) - t = Thread(target=runner, args=(hello, 'en-US', utterance_msg)) - t.start() - time.sleep(0.5) - self.intent_service.handle_converse_response( - Message('converse.response', {'skill_id': 'c64_skill', - 'result': False})) - time.sleep(0.5) - self.intent_service.handle_converse_response( - Message('converse.response', {'skill_id': 'atari_skill', - 'result': True})) - t.join() + result = self.intent_service._converse(hello, 'en-US', utterance_msg) # Check that the active skill list was updated to set the responding # Skill first. @@ -121,23 +114,26 @@ def runner(utterances, lang, message): def test_reset_converse(self): """Check that a blank stt sends the reset signal to the skills.""" - print(self.intent_service.active_skills) + def response(message, return_msg_type): + c64 = Message(return_msg_type, + {'skill_id': 'c64_skill', + 'error': 'skill id does not exist'}) + atari = Message(return_msg_type, {'skill_id': 'atari_skill', + 'result': False}) + msgs = {'c64_skill': c64, 'atari_skill': atari} + + return msgs[message.data['skill_id']] + reset_msg = Message('mycroft.speech.recognition.unknown', data={'lang': 'en-US'}) - t = Thread(target=self.intent_service.reset_converse, - args=(reset_msg,)) - t.start() - time.sleep(0.5) - self.intent_service.handle_converse_error( - Message('converse.error', {'skill_id': 'c64_skill', - 'error': 'skill id does not exist'})) - time.sleep(0.5) - self.intent_service.handle_converse_response( - Message('converse.response', {'skill_id': 'atari_skill', - 'result': False})) + self.intent_service.bus.wait_for_response.side_effect = response + self.intent_service.reset_converse(reset_msg) # Check send messages - c64_message = self.intent_service.bus.emit.call_args_list[0][0][0] + wait_for_response_mock = self.intent_service.bus.wait_for_response + c64_message = wait_for_response_mock.call_args_list[0][0][0] self.assertTrue(check_converse_request(c64_message, 'c64_skill')) - atari_message = self.intent_service.bus.emit.call_args_list[1][0][0] + atari_message = wait_for_response_mock.call_args_list[1][0][0] self.assertTrue(check_converse_request(atari_message, 'atari_skill')) + first_active_skill = self.intent_service.active_skills[0][0] + self.assertEqual(first_active_skill, 'atari_skill') diff --git a/test/unittests/skills/test_skill_manager.py b/test/unittests/skills/test_skill_manager.py index 697755e2d8e7..7d84f001e0a8 100644 --- a/test/unittests/skills/test_skill_manager.py +++ b/test/unittests/skills/test_skill_manager.py @@ -83,6 +83,7 @@ def _mock_skill_loader_instance(self): self.skill_loader_mock.instance = Mock() self.skill_loader_mock.instance.default_shutdown = Mock() self.skill_loader_mock.instance.converse = Mock() + self.skill_loader_mock.instance.converse.return_value = True self.skill_loader_mock.skill_id = 'test_skill' self.skill_manager.skill_loaders = { str(self.skill_dir): self.skill_loader_mock @@ -217,7 +218,8 @@ def test_stop(self): def test_handle_converse_request(self): message = Mock() - message.data = dict(skill_id='test_skill') + message.data = dict(skill_id='test_skill', utterances=['hey you'], + lang='en-US') self.skill_loader_mock.loaded = True converse_response_mock = Mock() self.skill_manager._emit_converse_response = converse_response_mock @@ -226,6 +228,7 @@ def test_handle_converse_request(self): self.skill_manager.handle_converse_request(message) converse_response_mock.assert_called_once_with( + True, message, self.skill_loader_mock ) From 003f2920a331506f9528455a99318b07201055c9 Mon Sep 17 00:00:00 2001 From: dalgwen Date: Wed, 11 Mar 2020 08:34:50 +0100 Subject: [PATCH 03/96] Enable snowboy to use several wakeword models Snowboy could use several wakeword models, not only the first one. --- mycroft/client/speech/hotword_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycroft/client/speech/hotword_factory.py b/mycroft/client/speech/hotword_factory.py index 41fccc691e39..32011be0095d 100644 --- a/mycroft/client/speech/hotword_factory.py +++ b/mycroft/client/speech/hotword_factory.py @@ -299,7 +299,7 @@ def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"): def found_wake_word(self, frame_data): wake_word = self.snowboy.detector.RunDetection(frame_data) - return wake_word == 1 + return wake_word >= 1 class PorcupineHotWord(HotWordEngine): From 9256672264f83aa710416d26a1b61e31f53a8846 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2020 17:18:53 +0000 Subject: [PATCH 04/96] Bump psutil from 5.2.1 to 5.6.6 Bumps [psutil](https://github.com/giampaolo/psutil) from 5.2.1 to 5.6.6. - [Release notes](https://github.com/giampaolo/psutil/releases) - [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) - [Commits](https://github.com/giampaolo/psutil/compare/release-5.2.1...release-5.6.6) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 26ad94823c15..dc87f486d5ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ requests-futures==0.9.5 pyalsaaudio==0.8.2 xmlrunner==1.7.7 pyserial==3.0 -psutil==5.2.1 +psutil==5.6.6 pocketsphinx==0.1.0 inflection==0.3.1 pillow==6.2.1 From 8e61019451510b7b988850c6598787c10184b744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Sun, 15 Mar 2020 19:11:01 +0100 Subject: [PATCH 05/96] Copy the list of active skills during operations A skill can be missed if a skill is removed (due to an error) from the list during iteration --- mycroft/skills/intent_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mycroft/skills/intent_service.py b/mycroft/skills/intent_service.py index 93f348109b61..e0bcb364231c 100644 --- a/mycroft/skills/intent_service.py +++ b/mycroft/skills/intent_service.py @@ -234,6 +234,7 @@ def do_converse(self, utterances, skill_id, lang, message): return result.data.get('result', False) def handle_converse_error(self, message): + LOG.error(message.data['error']) skill_id = message.data["skill_id"] if message.data["error"] == "skill id does not exist": self.remove_active_skill(skill_id) @@ -401,7 +402,7 @@ def _converse(self, utterances, lang, message): 1] <= self.converse_timeout * 60] # check if any skill wants to handle utterance - for skill in self.active_skills: + for skill in copy(self.active_skills): if self.do_converse(utterances, skill[0], lang, message): # update timestamp, or there will be a timeout where # intent stops conversing whether its being used or not From af50fb5e59488b9e3be656b3c932d017721d83a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Sun, 15 Mar 2020 19:13:04 +0100 Subject: [PATCH 06/96] Override make_active for padatious Padatious doesn't shouldn't report active like other skills, this only sent an empty skill entry to the active skills list. --- mycroft/skills/padatious_service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mycroft/skills/padatious_service.py b/mycroft/skills/padatious_service.py index 04b18c5a755c..5d0652228a89 100644 --- a/mycroft/skills/padatious_service.py +++ b/mycroft/skills/padatious_service.py @@ -82,6 +82,10 @@ def __init__(self, bus, service): self.registered_intents = [] self.registered_entities = [] + def make_active(self): + """Override the make active since this is not a real fallback skill.""" + pass + def train(self, message=None): padatious_single_thread = Configuration.get()[ 'padatious']['single_thread'] From 4982af46b122367014c5974b5bef435dafe76f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Sun, 15 Mar 2020 19:15:22 +0100 Subject: [PATCH 07/96] Add check for empty skill IDs Only allow non-empty skill IDs into the list of active skills --- mycroft/skills/intent_service.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mycroft/skills/intent_service.py b/mycroft/skills/intent_service.py index e0bcb364231c..d087ef814e77 100644 --- a/mycroft/skills/intent_service.py +++ b/mycroft/skills/intent_service.py @@ -247,9 +247,13 @@ def remove_active_skill(self, skill_id): def add_active_skill(self, skill_id): # search the list for an existing entry that already contains it # and remove that reference - self.remove_active_skill(skill_id) - # add skill with timestamp to start of skill_list - self.active_skills.insert(0, [skill_id, time.time()]) + if skill_id != '': + self.remove_active_skill(skill_id) + # add skill with timestamp to start of skill_list + self.active_skills.insert(0, [skill_id, time.time()]) + else: + LOG.warning('Skill ID was empty, won\'t add to list of ' + 'active skills.') def update_context(self, intent): """ Updates context with keyword from the intent. From f0a6d1a714fa03169c361b8b20d038f15026b9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Sun, 15 Mar 2020 19:22:03 +0100 Subject: [PATCH 08/96] Minor clean up of intent_service.py - Remove print statement - Remove unused import - Updated some docstrings --- mycroft/skills/intent_service.py | 34 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/mycroft/skills/intent_service.py b/mycroft/skills/intent_service.py index d087ef814e77..68ca9da36aec 100644 --- a/mycroft/skills/intent_service.py +++ b/mycroft/skills/intent_service.py @@ -19,7 +19,6 @@ from adapt.intent import IntentBuilder from mycroft.configuration import Configuration -from mycroft.messagebus.message import Message from mycroft.util.lang import set_active_lang from mycroft.util.log import LOG from mycroft.util.parse import normalize @@ -124,7 +123,6 @@ def get_context(self, max_frames=None, missing_entities=None): if entity['origin'] != last or entity['origin'] == '': depth += 1 last = entity['origin'] - print(depth) result = [] if len(missing_entities) > 0: @@ -198,14 +196,11 @@ def add_active_skill_handler(message): self.handle_vocab_manifest) def update_skill_name_dict(self, message): - """ - Messagebus handler, updates dictionary of if to skill name - conversions. - """ + """Messagebus handler, updates dict of id to skill name conversions.""" self.skill_names[message.data['id']] = message.data['name'] def get_skill_name(self, skill_id): - """ Get skill name from skill ID. + """Get skill name from skill ID. Args: skill_id: a skill id as encoded in Intent handlers. @@ -245,6 +240,14 @@ def remove_active_skill(self, skill_id): self.active_skills.remove(skill) def add_active_skill(self, skill_id): + """Add a skill or update the position of an active skill. + + The skill is added to the front of the list, if it's already in the + list it's removed so there is only a single entry of it. + + Arguments: + skill_id (str): identifier of skill to be added. + """ # search the list for an existing entry that already contains it # and remove that reference if skill_id != '': @@ -256,7 +259,7 @@ def add_active_skill(self, skill_id): 'active skills.') def update_context(self, intent): - """ Updates context with keyword from the intent. + """Updates context with keyword from the intent. NOTE: This method currently won't handle one_of intent keywords since it's not using quite the same format as other intent @@ -275,8 +278,7 @@ def update_context(self, intent): self.context_manager.inject_context(context_entity) def send_metrics(self, intent, context, stopwatch): - """ - Send timing metrics to the backend. + """Send timing metrics to the backend. NOTE: This only applies to those with Opt In. """ @@ -294,7 +296,7 @@ def send_metrics(self, intent, context, stopwatch): {'intent_type': 'intent_failure'}) def handle_utterance(self, message): - """ Main entrypoint for handling user utterances with Mycroft skills + """Main entrypoint for handling user utterances with Mycroft skills Monitor the messagebus for 'recognizer_loop:utterance', typically generated by a spoken interaction but potentially also from a CLI @@ -389,7 +391,7 @@ def handle_utterance(self, message): LOG.exception(e) def _converse(self, utterances, lang, message): - """ Give active skills a chance at the utterance + """Give active skills a chance at the utterance Args: utterances (list): list of utterances @@ -415,7 +417,7 @@ def _converse(self, utterances, lang, message): return False def _adapt_intent_match(self, raw_utt, norm_utt, lang): - """ Run the Adapt engine to search for an matching intent + """Run the Adapt engine to search for an matching intent Args: raw_utt (list): list of utterances @@ -487,7 +489,7 @@ def handle_detach_skill(self, message): self.engine.intent_parsers = new_parsers def handle_add_context(self, message): - """ Add context + """Add context Args: message: data contains the 'context' item to add @@ -508,7 +510,7 @@ def handle_add_context(self, message): self.context_manager.inject_context(entity) def handle_remove_context(self, message): - """ Remove specific context + """Remove specific context Args: message: data contains the 'context' item to remove @@ -518,7 +520,7 @@ def handle_remove_context(self, message): self.context_manager.remove_context(context) def handle_clear_context(self, message): - """ Clears all keywords from context """ + """Clears all keywords from context """ self.context_manager.clear_context() def handle_get_adapt(self, message): From 6dce1096e39bcf4d563910ee957aaa4d8d0b2659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Sun, 15 Mar 2020 22:18:25 +0100 Subject: [PATCH 09/96] Test that all skill_id's are processed Including a case where an error occur. --- test/unittests/skills/test_intent_service.py | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/unittests/skills/test_intent_service.py b/test/unittests/skills/test_intent_service.py index e2c340108382..2272a5bad04e 100644 --- a/test/unittests/skills/test_intent_service.py +++ b/test/unittests/skills/test_intent_service.py @@ -112,6 +112,45 @@ def response(message, return_msg_type): # Check that a skill responded that it could handle the message self.assertTrue(result) + def test_converse_error(self): + """Check that all skill IDs in the active_skills list are called. + even if there's an error. + """ + def response(message, return_msg_type): + c64 = Message(return_msg_type, {'skill_id': 'c64_skill', + 'result': False}) + amiga = Message(return_msg_type, + {'skill_id': 'amiga_skill', + 'error': 'skill id does not exist'}) + atari = Message(return_msg_type, {'skill_id': 'atari_skill', + 'result': False}) + msgs = {'c64_skill': c64, + 'atari_skill': atari, + 'amiga_skill': amiga} + + return msgs[message.data['skill_id']] + + self.intent_service.add_active_skill('amiga_skill') + self.intent_service.bus.wait_for_response.side_effect = response + + hello = ['hello old friend'] + utterance_msg = Message('recognizer_loop:utterance', + data={'lang': 'en-US', + 'utterances': hello}) + result = self.intent_service._converse(hello, 'en-US', utterance_msg) + + # Check that the active skill list was updated to set the responding + # Skill first. + + # Check that a skill responded that it couldn't handle the message + self.assertFalse(result) + + # Check that each skill in the list of active skills were called + call_args = self.intent_service.bus.wait_for_response.call_args_list + sent_skill_ids = [call[0][0].data['skill_id'] for call in call_args] + self.assertEqual(sent_skill_ids, + ['amiga_skill', 'c64_skill', 'atari_skill']) + def test_reset_converse(self): """Check that a blank stt sends the reset signal to the skills.""" def response(message, return_msg_type): From b4507b7866a28303d9b7128540a23230f90d501b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 6 Feb 2020 09:36:18 +0100 Subject: [PATCH 10/96] Add Voight Kampff test The Voight kampff test is an integration test collecting and running behave test of skills. --- test-requirements.txt | 1 + test/integrationtests/voight_kampff/README.md | 58 +++++ .../voight_kampff/__init__.py | 14 ++ .../voight_kampff/default.yml | 9 + .../voight_kampff/features/environment.py | 71 ++++++ .../features/steps/utterance_responses.py | 217 ++++++++++++++++++ .../voight_kampff/generate_feature.py | 65 ++++++ .../voight_kampff/test_setup.py | 149 ++++++++++++ 8 files changed, 584 insertions(+) create mode 100644 test/integrationtests/voight_kampff/README.md create mode 100644 test/integrationtests/voight_kampff/__init__.py create mode 100644 test/integrationtests/voight_kampff/default.yml create mode 100644 test/integrationtests/voight_kampff/features/environment.py create mode 100644 test/integrationtests/voight_kampff/features/steps/utterance_responses.py create mode 100644 test/integrationtests/voight_kampff/generate_feature.py create mode 100644 test/integrationtests/voight_kampff/test_setup.py diff --git a/test-requirements.txt b/test-requirements.txt index 935cc6d808e9..85845974854d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,3 +5,4 @@ pytest-cov==2.8.1 cov-core==1.15.0 sphinx==2.2.1 sphinx-rtd-theme==0.4.3 +behave==1.2.6 diff --git a/test/integrationtests/voight_kampff/README.md b/test/integrationtests/voight_kampff/README.md new file mode 100644 index 000000000000..3efe9d589b64 --- /dev/null +++ b/test/integrationtests/voight_kampff/README.md @@ -0,0 +1,58 @@ +# Voight Kampff tester + +> You’re watching television. Suddenly you realize there’s a wasp crawling on your arm. + +The Voight Kampff tester is an integration test system based on the "behave" framework using human readable test cases. The tester connects to the running mycroft-core instance and performs tests. Checking that user utterances returns a correct response. + +## Test setup +`test_setup` collects feature files for behave and installs any skills that should be present during the test. + +## Running the test +After the test has been setup run `behave` to start the test. + +## Feature file +Feature files is the way tests are specified for behave (Read more [here](https://behave.readthedocs.io/en/latest/tutorial.html)) + +Below is an example of a feature file that can be used with the test suite. +```feature +Feature: mycroft-weather + Scenario Outline: current local weather question + Given an english speaking user + When the user says "" + Then "mycroft-weather" should reply with "Right now, it's overcast clouds and 32 degrees." + + Examples: local weather questions + | current local weather | + | what's the weather like | + | current weather | + | tell me the weather | + + Scenario: Temperature in paris + Given an english speaking user + When the user says "how hot will it be in paris" + Then "mycroft-weather" should reply with dialog from "current.high.temperature.dialog" +``` + +### Given ... + +Given is used to perform initial setup for the test case. currently this has little effect and the test will always be performed in english but need to be specified in each test as + +```Given an english speaking user``` + +### When ... +The When is the start of the test and will inject a message on the running mycroft instance. The current available When is + +`When the user says ""` + +where utterance is the sentence to test. + +### Then ... +The "Then" step will verify the response of the user, currently there is two ways of specifying the response. + +Expected dialog: +`"" should reply with dialog from ""` + +Example phrase: +`Then "" should reply with "" + +This will try to map the example phrase to a dialog file and will allow any response from that dialog file. diff --git a/test/integrationtests/voight_kampff/__init__.py b/test/integrationtests/voight_kampff/__init__.py new file mode 100644 index 000000000000..5ccd4d62e4e1 --- /dev/null +++ b/test/integrationtests/voight_kampff/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2020 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. +# diff --git a/test/integrationtests/voight_kampff/default.yml b/test/integrationtests/voight_kampff/default.yml new file mode 100644 index 000000000000..354f09124dfc --- /dev/null +++ b/test/integrationtests/voight_kampff/default.yml @@ -0,0 +1,9 @@ +extra_skills: +- cocktails +platform: mycroft_mark_1 +tested_skills: +- mycroft-date-time +- mycroft-weather +- mycroft-hello-world +- mycroft-pairing +- mycroft-wiki diff --git a/test/integrationtests/voight_kampff/features/environment.py b/test/integrationtests/voight_kampff/features/environment.py new file mode 100644 index 000000000000..62e842e60d8f --- /dev/null +++ b/test/integrationtests/voight_kampff/features/environment.py @@ -0,0 +1,71 @@ +# 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 threading import Event, Lock +from time import sleep, monotonic + +from msm import MycroftSkillsManager +from mycroft.messagebus.client import MessageBusClient +from mycroft.messagebus import Message +from mycroft.util import create_daemon + + +def before_all(context): + bus = MessageBusClient() + bus_connected = Event() + bus.once('open', bus_connected.set) + + context.speak_messages = [] + context.speak_lock = Lock() + + def on_speak(message): + with context.speak_lock: + context.speak_messages.append(message) + + bus.on('speak', on_speak) + create_daemon(bus.run_forever) + + context.msm = MycroftSkillsManager() + # Wait for connection + print('Waiting for messagebus connection...') + bus_connected.wait() + + print('Waiting for skills to be loaded...') + start = monotonic() + while True: + response = bus.wait_for_response(Message('mycroft.skills.all_loaded')) + if response and response.data['status']: + break + elif monotonic() - start >= 2 * 60: + raise Exception('Timeout waiting for skills to become ready.') + else: + sleep(1) + + context.bus = bus + context.matched_message = None + + +def after_all(context): + context.bus.close() + + +def after_feature(context, feature): + sleep(2) + + +def after_scenario(context, scenario): + with context.speak_lock: + context.speak_messages = [] + context.matched_message = None + sleep(0.5) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py new file mode 100644 index 000000000000..62bd9b32b39b --- /dev/null +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -0,0 +1,217 @@ +# 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. +# +""" +Predefined step definitions for handling dialog interaction with Mycroft for +use with behave. +""" +from os.path import join, exists, basename +from glob import glob +import re +import time + +from behave import given, when, then + +from mycroft.messagebus import Message + + +TIMEOUT = 10 + + +def find_dialog(skill_path, dialog): + """Check the usual location for dialogs. + + TODO: subfolders + """ + if exists(join(skill_path, 'dialog')): + return join(skill_path, 'dialog', 'en-us', dialog) + else: + return join(skill_path, 'locale', 'en-us', dialog) + + +def load_dialog_file(dialog_path): + """Load dialog files and get the contents.""" + with open(dialog_path) as f: + lines = f.readlines() + return [l.strip().lower() for l in lines + if l.strip() != '' and l.strip()[0] != '#'] + + +def load_dialog_list(skill_path, dialog): + """Load dialog from files into a single list. + + Arguments: + skill (MycroftSkill): skill to load dialog from + dialog (list): Dialog names (str) to load + + Returns: + tuple (list of Expanded dialog strings, debug string) + """ + dialog_path = find_dialog(skill_path, dialog) + + debug = 'Opening {}\n'.format(dialog_path) + return load_dialog_file(dialog_path), debug + + +def dialog_from_sentence(sentence, skill_path): + """Find dialog file from example sentence.""" + dialog_paths = join(skill_path, 'dialog', 'en-us', '*.dialog') + best = (None, 0) + for path in glob(dialog_paths): + patterns = load_dialog_file(path) + match, _ = _match_dialog_patterns(patterns, sentence.lower()) + if match is not False: + if len(patterns[match]) > best[1]: + best = (path, len(patterns[match])) + if best[0] is not None: + return basename(best[0]) + else: + return None + + +def _match_dialog_patterns(dialogs, sentence): + """Match sentence against a list of dialog patterns. + + Returns index of found match. + """ + # Allow custom fields to be anything + dialogs = [re.sub(r'{.*?\}', r'.*', dia) for dia in dialogs] + # Remove left over '}' + dialogs = [re.sub(r'\}', r'', dia) for dia in dialogs] + dialogs = [re.sub(r' .* ', r' .*', dia) for dia in dialogs] + # Merge consequtive .*'s into a single .* + dialogs = [re.sub(r'\.\*( \.\*)+', r'.*', dia) for dia in dialogs] + # Remove double whitespaces + dialogs = ['^' + ' '.join(dia.split()) for dia in dialogs] + debug = 'MATCHING: {}\n'.format(sentence) + for index, regex in enumerate(dialogs): + match = re.match(regex, sentence) + debug += '---------------\n' + debug += '{} {}\n'.format(regex, match is not None) + if match: + return index, debug + else: + return False, debug + + +def match_dialog_patterns(dialogs, sentence): + """Match sentence against a list of dialog patterns. + + Returns simple True/False and not index + """ + index, debug = _match_dialog_patterns(dialogs, sentence) + return index is not False, debug + + +def expected_dialog_check(utterance, skill_path, dialog): + """Check that expected dialog file is used. """ + dialogs, load_debug = load_dialog_list(skill_path, dialog) + match, match_debug = match_dialog_patterns(dialogs, utterance) + return match, load_debug + match_debug + + +@given('an english speaking user') +def given_impl(context): + context.lang = 'en-us' + + +@when('the user says "{text}"') +def when_impl(context, text): + context.bus.emit(Message('recognizer_loop:utterance', + data={'utterances': [text], + 'lang': 'en-us', + 'session': '', + 'ident': time.time()}, + context={'client_name': 'mycroft_listener'})) + + +def then_timeout(then_func, context, timeout=TIMEOUT): + count = 0 + debug = '' + while count < TIMEOUT: + for message in context.speak_messages: + status, test_dbg = then_func(message) + debug += test_dbg + if status: + context.matched_message = message + return True, debug + time.sleep(1) + count += 1 + # Timed out return debug from test + return False, debug + + +@then('"{skill}" should reply with dialog from "{dialog}"') +def then_dialog(context, skill, dialog): + skill_path = context.msm.find_skill(skill).path + + def check_dialog(message): + utt = message.data['utterance'].lower() + return expected_dialog_check(utt, skill_path, dialog) + + passed, debug = then_timeout(check_dialog, context) + if not passed: + print(debug) + assert passed + + +@then('"{skill}" should reply with "{example}"') +def then_example(context, skill, example): + skill_path = context.msm.find_skill(skill).path + dialog = dialog_from_sentence(example, skill_path) + print('Matching with the dialog file: {}'.format(dialog)) + assert dialog is not None + then_dialog(context, skill, dialog) + + +@then('"{skill}" should reply with anything') +def then_anything(context, skill): + def check_any_messages(message): + debug = '' + result = message is not None + return (result, debug) + + passed = then_timeout(check_any_messages, context) + if not passed: + print('No speech recieved at all.') + assert passed + + +@then('"{skill}" should reply with exactly "{text}"') +def then_exactly(context, skill, text): + def check_exact_match(message): + utt = message.data['utterance'].lower() + debug = 'Comparing {} with expected {}\n'.format(utt, text) + result = utt == text.lower() + return (result, debug) + + passed, debug = then_timeout(check_exact_match, context) + if not passed: + print(debug) + assert passed + + +@then('mycroft reply should contain "{text}"') +def then_contains(context, text): + def check_contains(message): + utt = message.data['utterance'].lower() + debug = 'Checking if "{}" contains "{}"\n'.format(utt, text) + result = text.lower() in utt + return (result, debug) + + passed, debug = then_timeout(check_contains, context) + + if not passed: + print('No speech contained the expected content') + assert passed diff --git a/test/integrationtests/voight_kampff/generate_feature.py b/test/integrationtests/voight_kampff/generate_feature.py new file mode 100644 index 000000000000..eeaee230ad0a --- /dev/null +++ b/test/integrationtests/voight_kampff/generate_feature.py @@ -0,0 +1,65 @@ +# Copyright 2020 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 glob import glob +import json +from pathlib import Path +import sys + +"""Convert existing intent tests to behave tests.""" + +TEMPLATE = """ + Scenario: {scenario} + Given an english speaking user + When the user says "{utterance}" + Then "{skill}" should reply with dialog from "{dialog_file}.dialog" +""" + + +def json_files(path): + """Generator function returning paths of all json files in a folder.""" + for json_file in sorted(glob(str(Path(path, '*.json')))): + yield Path(json_file) + + +def generate_feature(skill, skill_path): + """Generate a feature file provided a skill name and a path to the skill. + """ + test_path = Path(skill_path, 'test', 'intent') + case = [] + if test_path.exists() and test_path.is_dir(): + for json_file in json_files(test_path): + with open(str(json_file)) as test_file: + test = json.load(test_file) + if 'utterance' and 'expected_dialog' in test: + utt = test['utterance'] + dialog = test['expected_dialog'] + # Simple handling of multiple accepted dialogfiles + if isinstance(dialog, list): + dialog = dialog[0] + + case.append((json_file.name, utt, dialog)) + + output = '' + if case: + output += 'Feature: {}\n'.format(skill) + for c in case: + output += TEMPLATE.format(skill=skill, scenario=c[0], + utterance=c[1], dialog_file=c[2]) + + return output + + +if __name__ == '__main__': + print(generate_feature(*sys.argv[1:])) diff --git a/test/integrationtests/voight_kampff/test_setup.py b/test/integrationtests/voight_kampff/test_setup.py new file mode 100644 index 000000000000..3d0d618d13b3 --- /dev/null +++ b/test/integrationtests/voight_kampff/test_setup.py @@ -0,0 +1,149 @@ +# Copyright 2020 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 argparse +from glob import glob +from os.path import join, dirname, expanduser, basename +from pathlib import Path +from random import shuffle +import shutil +import sys + +import yaml + +from msm import MycroftSkillsManager + +from .generate_feature import generate_feature + +"""Test environment setup for voigt kampff test + +The script sets up the selected tests in the feature directory so they can +be found and executed by the behave framework. + +The script also ensures that the tested skills are installed and that any +specified extra skills also gets installed into the environment. +""" + +FEATURE_DIR = join(dirname(__file__), 'features') + '/' + + +def copy_feature_files(source, destination): + """Copy all feature files from source to destination.""" + # Copy feature files to the feature directory + for f in glob(join(source, '*.feature')): + shutil.copyfile(f, join(destination, basename(f))) + + +def copy_step_files(source, destination): + """Copy all python files from source to destination.""" + # Copy feature files to the feature directory + for f in glob(join(source, '*.py')): + shutil.copyfile(f, join(destination, basename(f))) + + +def load_config(config, args): + """Load config and add to unset arguments.""" + with open(expanduser(config)) as f: + conf_dict = yaml.safe_load(f) + + if not args.tested_skills: + args.tested_skills = conf_dict['tested_skills'] + if not args.extra_skills: + args.extra_skills = conf_dict['extra_skills'] + if not args.platform: + args.platform = conf_dict['platform'] + return + + +def main(cmdline_args): + """Parse arguments and run environment setup.""" + platforms = list(MycroftSkillsManager.SKILL_GROUPS) + parser = argparse.ArgumentParser() + parser.add_argument('-p', '--platform', choices=platforms) + parser.add_argument('-t', '--tested-skills', default=[]) + parser.add_argument('-s', '--extra-skills', default=[]) + parser.add_argument('-r', '--random-skills', default=0) + parser.add_argument('-d', '--skills-dir') + parser.add_argument('-c', '--config') + + args = parser.parse_args(cmdline_args) + if args.tested_skills: + args.tested_skills = args.tested_skills.replace(',', ' ').split() + if args.extra_skills: + args.extra_skills = args.extra_skills.replace(',', ' ').split() + + if args.config: + load_config(args.config, args) + + if args.platform is None: + args.platform = "mycroft_mark_1" + + msm = MycroftSkillsManager(args.platform, args.skills_dir) + run_setup(msm, args.tested_skills, args.extra_skills, args.random_skills) + + +def run_setup(msm, test_skills, extra_skills, num_random_skills): + """Install needed skills and collect feature files for the test.""" + skills = [msm.find_skill(s) for s in test_skills + extra_skills] + # Install test skills + for s in skills: + if not s.is_local: + print('Installing {}'.format(s)) + msm.install(s) + + # collect feature files + for skill_name in test_skills: + skill = msm.find_skill(skill_name) + behave_dir = join(skill.path, 'test', 'behave') + if Path(behave_dir).exists(): + copy_feature_files(behave_dir, FEATURE_DIR) + + step_dir = join(behave_dir, 'steps') + if Path().exists(): + copy_step_files(step_dir, join(FEATURE_DIR, 'steps')) + else: + # Generate feature file if no data exists yet + print('No feature files exists for {}, ' + 'generating...'.format(skill_name)) + # No feature files setup, generate automatically + feature = generate_feature(skill_name, skill.path) + with open(join(FEATURE_DIR, skill_name + '.feature'), 'w') as f: + f.write(feature) + + # Install random skills from uninstalled skill list + random_skills = [s for s in msm.all_skills if s not in msm.local_skills] + shuffle(random_skills) # Make them random + random_skills = random_skills[:num_random_skills] + for s in random_skills: + msm.install(s) + + print_install_report(msm.platform, test_skills, + extra_skills + [s.name for s in random_skills]) + return + + +def print_install_report(platform, test_skills, extra_skills): + """Print in nice format.""" + print('-------- TEST SETUP --------') + yml = yaml.dump({ + 'platform': platform, + 'tested_skills': test_skills, + 'extra_skills': extra_skills + }) + print(yml) + print('----------------------------') + + +if __name__ == '__main__': + main(sys.argv[1:]) From 4a1dbd7e043d952e930cce276c67e3a0747accf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 6 Feb 2020 09:48:24 +0100 Subject: [PATCH 11/96] Add dockerfile for running voight_kampff test --- test/Dockerfile.test | 39 +++++++++++++++++++ .../voight_kampff/run_test_suite.sh | 23 +++++++++++ 2 files changed, 62 insertions(+) create mode 100644 test/Dockerfile.test create mode 100755 test/integrationtests/voight_kampff/run_test_suite.sh diff --git a/test/Dockerfile.test b/test/Dockerfile.test new file mode 100644 index 000000000000..05fc0a72e02c --- /dev/null +++ b/test/Dockerfile.test @@ -0,0 +1,39 @@ +FROM ubuntu:18.04 as builder +ENV TERM linux +ENV DEBIAN_FRONTEND noninteractive +COPY . /opt/mycroft/mycroft-core +# Install Server Dependencies for Mycroft +RUN set -x \ + # Un-comment any package sources that include a multiverse + && sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list \ + && apt-get update \ + # Install packages specific to Docker implementation + && apt-get -y install locales sudo\ + && mkdir /opt/mycroft/skills \ + && CI=true bash -x /opt/mycroft/mycroft-core/dev_setup.sh --allow-root -sm \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +# Set the locale +RUN locale-gen en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 +# Add the local configuration directory +RUN mkdir ~/.mycroft +EXPOSE 8181 + +# Integration Test Suite +FROM builder as voight_kampff +# Add the mycroft core virtual environment to the system path. +ENV PATH /opt/mycroft/mycroft-core/.venv/bin:$PATH +WORKDIR /opt/mycroft/mycroft-core/test/integrationtests/voight_kampff +# Install Mark I default skills +RUN msm -p mycroft_mark_1 default +# The behave feature files for a skill are defined within the skill's +# repository. Copy those files into the local feature file directory +# for test discovery. +RUN python -m test.integrationtests.voight_kampff.skill_setup -c default.yml +RUN mkdir ~/.mycroft/allure-result +# Setup and run the integration tests +ENTRYPOINT "./run_test_suite.sh" diff --git a/test/integrationtests/voight_kampff/run_test_suite.sh b/test/integrationtests/voight_kampff/run_test_suite.sh new file mode 100755 index 000000000000..c23105435b2a --- /dev/null +++ b/test/integrationtests/voight_kampff/run_test_suite.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Script to setup the integration test environment and run the tests. +# +# The comands runing in this script are those that need to be executed at +# runtime. Assumes running within a Docker container where the PATH environment +# variable has been set to include the virtual envionrment's bin directory + +# Start all mycroft core services. +pwd +/opt/mycroft/mycroft-core/start-mycroft.sh all +# Run the integration test suite. Results will be formatted for input into +# the Allure reporting tool. +behave -f allure_behave.formatter:AllureFormatter -o allure-results +RESULT=$? +# Stop all mycroft core services. +/opt/mycroft/mycroft-core/stop-mycroft.sh all + +# Remove temporary skill files +rm -rf ~/.mycroft/skills +# Remove intent cache +rm -rf ~/.mycroft/intent_cache + +exit $RESULT From 5bfe8694249e4a4c37a4da4c9887945dd7a4b3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 6 Feb 2020 10:32:12 +0100 Subject: [PATCH 12/96] Jenkinsfile --- Jenkinsfile | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000000..4b9020600a90 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,39 @@ +pipeline { + agent any + options { + // Running builds concurrently could cause a race condition with + // building the Docker image. + disableConcurrentBuilds() + } + stages { + // Run the build in the against the dev branch to check for compile errors + stage('Run Integration Tests') { + when { + anyOf { + branch 'dev' + branch 'master' + changeRequest target: 'dev' + } + } + steps { + echo 'Building Test Docker Image' + sh 'cp test/Dockerfile.test Dockerfile' + sh 'docker build --target voigt_kampff -t mycroft-core:latest .' + echo 'Running Tests' + timeout(time: 10, unit: 'MINUTES') + { + sh 'docker run \ + -v "$HOME/voigtmycroft:/root/.mycroft" \ + mycroft-core:latest' + } + } + } + } + post { + always('Important stuff') { + echo 'Cleaning up docker containers and images' + sh 'docker container prune --force' + sh 'docker image prune --force' + } + } +} From fc9c462a37bec1e6ca4b879f8559b0f8b67c9042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 11 Feb 2020 09:24:07 +0100 Subject: [PATCH 13/96] Add possibility to send a user response as Then step --- .../features/steps/utterance_responses.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index 62bd9b32b39b..9252f2f2cdf3 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -24,6 +24,7 @@ from behave import given, when, then from mycroft.messagebus import Message +from mycroft.audio import wait_while_speaking TIMEOUT = 10 @@ -215,3 +216,15 @@ def check_contains(message): if not passed: print('No speech contained the expected content') assert passed + + +@then('the user says "{text}"') +def when_impl(context, text): + time.sleep(2) + wait_while_speaking() + context.bus.emit(Message('recognizer_loop:utterance', + data={'utterances': [text], + 'lang': 'en-us', + 'session': '', + 'ident': time.time()}, + context={'client_name': 'mycroft_listener'})) From 51f03fc0ce41a439c30aea9791596b325c5c85d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 13 Feb 2020 08:35:47 +0100 Subject: [PATCH 14/96] Add Branch alias Some branches have a "/" in their name (e.g. feature/new-and-cool) Some commands, such as those tha deal with directories, don't play nice with this naming convention. Define an alias for the branch name that can be used in these scenarios. --- Jenkinsfile | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4b9020600a90..1ba5f9d3addd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,6 +5,16 @@ pipeline { // building the Docker image. disableConcurrentBuilds() } + environment { + // Some branches have a "/" in their name (e.g. feature/new-and-cool) + // Some commands, such as those tha deal with directories, don't + // play nice with this naming convention. Define an alias for the + // branch name that can be used in these scenarios. + BRANCH_ALIAS = sh( + script: 'echo $BRANCH_NAME | sed -e "s#/#_#g"', + returnStdout: true + ).trim() + } stages { // Run the build in the against the dev branch to check for compile errors stage('Run Integration Tests') { @@ -18,13 +28,13 @@ pipeline { steps { echo 'Building Test Docker Image' sh 'cp test/Dockerfile.test Dockerfile' - sh 'docker build --target voigt_kampff -t mycroft-core:latest .' + sh 'docker build --target voigt_kampff -t mycroft-core:${BRANCH_ALIAS} .' echo 'Running Tests' timeout(time: 10, unit: 'MINUTES') { sh 'docker run \ -v "$HOME/voigtmycroft:/root/.mycroft" \ - mycroft-core:latest' + mycroft-core:${BRANCH_ALIAS}' } } } From b436e5575ac8de2da7401f212ba0e65a6984069f Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 13 Feb 2020 11:29:34 +0100 Subject: [PATCH 15/96] Add allure test reports Behave will generate test results in the allure format, this will be picked up by Jenkins and sent of to a standalone webserver. --- Jenkinsfile | 39 ++++++++++++++++--- test-requirements.txt | 1 + test/Dockerfile.test | 3 +- .../voight_kampff/run_test_suite.sh | 7 ++-- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1ba5f9d3addd..07877724a5cb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -28,7 +28,7 @@ pipeline { steps { echo 'Building Test Docker Image' sh 'cp test/Dockerfile.test Dockerfile' - sh 'docker build --target voigt_kampff -t mycroft-core:${BRANCH_ALIAS} .' + sh 'docker build --no-cache --target voight_kampff -t mycroft-core:${BRANCH_ALIAS} .' echo 'Running Tests' timeout(time: 10, unit: 'MINUTES') { @@ -37,13 +37,42 @@ pipeline { mycroft-core:${BRANCH_ALIAS}' } } + post { + always { + echo 'Report Test Results' + sh 'mv $HOME/voigtmycroft/allure-result allure-result' + script { + allure([ + includeProperties: false, + jdk: '', + properties: [], + reportBuildPolicy: 'ALWAYS', + results: [[path: 'allure-result']] + ]) + } + unarchive mapping:['allure-report.zip': 'allure-report.zip'] + sh ( + label: 'Publish Report to Web Server', + script: '''scp allure-report.zip root@157.245.127.234:~; + ssh root@157.245.127.234 "unzip -o ~/allure-report.zip"; + ssh root@157.245.127.234 "rm -rf /var/www/voigt-kampff/${BRANCH_ALIAS}"; + ssh root@157.245.127.234 "mv allure-report /var/www/voigt-kampff/${BRANCH_ALIAS}" + ''' + ) + echo 'Report Published' + } + } } } post { - always('Important stuff') { - echo 'Cleaning up docker containers and images' - sh 'docker container prune --force' - sh 'docker image prune --force' + cleanup { + sh( + label: 'Docker Container and Image Cleanup', + script: ''' + docker container prune --force; + docker image prune --force; + ''' + ) } } } diff --git a/test-requirements.txt b/test-requirements.txt index 85845974854d..dca9a4b830ba 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,3 +6,4 @@ cov-core==1.15.0 sphinx==2.2.1 sphinx-rtd-theme==0.4.3 behave==1.2.6 +allure-behave==2.8.10 diff --git a/test/Dockerfile.test b/test/Dockerfile.test index 05fc0a72e02c..e850d386beb6 100644 --- a/test/Dockerfile.test +++ b/test/Dockerfile.test @@ -21,6 +21,7 @@ ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 # Add the local configuration directory RUN mkdir ~/.mycroft +VOLUME /root/.mycroft EXPOSE 8181 # Integration Test Suite @@ -33,7 +34,7 @@ RUN msm -p mycroft_mark_1 default # The behave feature files for a skill are defined within the skill's # repository. Copy those files into the local feature file directory # for test discovery. -RUN python -m test.integrationtests.voight_kampff.skill_setup -c default.yml +RUN python -m test.integrationtests.voight_kampff.test_setup -c default.yml RUN mkdir ~/.mycroft/allure-result # Setup and run the integration tests ENTRYPOINT "./run_test_suite.sh" diff --git a/test/integrationtests/voight_kampff/run_test_suite.sh b/test/integrationtests/voight_kampff/run_test_suite.sh index c23105435b2a..542e14c9573c 100755 --- a/test/integrationtests/voight_kampff/run_test_suite.sh +++ b/test/integrationtests/voight_kampff/run_test_suite.sh @@ -6,15 +6,16 @@ # variable has been set to include the virtual envionrment's bin directory # Start all mycroft core services. -pwd /opt/mycroft/mycroft-core/start-mycroft.sh all # Run the integration test suite. Results will be formatted for input into # the Allure reporting tool. -behave -f allure_behave.formatter:AllureFormatter -o allure-results +behave -f allure_behave.formatter:AllureFormatter -o ~/.mycroft/allure-result RESULT=$? # Stop all mycroft core services. /opt/mycroft/mycroft-core/stop-mycroft.sh all - +# Make the jenkins user the owner of the allure results. This allows the +# jenkins job to build a report from the results +chown --recursive 110:116 ~/.mycroft/allure-result # Remove temporary skill files rm -rf ~/.mycroft/skills # Remove intent cache From 00acf2b10c7f0dae5c0dd28271d266c021447c89 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 13 Feb 2020 11:43:03 +0100 Subject: [PATCH 16/96] Update docker build Docker build will now perform most actions of the dev-setup making it possible to use caches in a greater extent speeding up the build --- Jenkinsfile | 2 +- test/Dockerfile.test | 91 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 07877724a5cb..0d98a3dc28d0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -28,7 +28,7 @@ pipeline { steps { echo 'Building Test Docker Image' sh 'cp test/Dockerfile.test Dockerfile' - sh 'docker build --no-cache --target voight_kampff -t mycroft-core:${BRANCH_ALIAS} .' + sh 'docker build --target voight_kampff -t mycroft-core:${BRANCH_ALIAS} .' echo 'Running Tests' timeout(time: 10, unit: 'MINUTES') { diff --git a/test/Dockerfile.test b/test/Dockerfile.test index e850d386beb6..e067101c6e6f 100644 --- a/test/Dockerfile.test +++ b/test/Dockerfile.test @@ -1,40 +1,95 @@ +# Build an Ubuntu-based container to run Mycroft +# +# The steps in this build are ordered from least likely to change to most +# likely to change. The intent behind this is to reduce build time so things +# like Jenkins jobs don't spend a lot of time re-building things that did not +# change from one build to the next. +# FROM ubuntu:18.04 as builder ENV TERM linux ENV DEBIAN_FRONTEND noninteractive -COPY . /opt/mycroft/mycroft-core +# Un-comment any package sources that include a multiverse +RUN sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list # Install Server Dependencies for Mycroft -RUN set -x \ - # Un-comment any package sources that include a multiverse - && sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list \ - && apt-get update \ - # Install packages specific to Docker implementation - && apt-get -y install locales sudo\ - && mkdir /opt/mycroft/skills \ - && CI=true bash -x /opt/mycroft/mycroft-core/dev_setup.sh --allow-root -sm \ - && apt-get -y autoremove \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt-get update && apt-get install -y \ + autoconf \ + automake \ + bison \ + build-essential \ + curl \ + flac \ + git \ + jq \ + libfann-dev \ + libffi-dev \ + libicu-dev \ + libjpeg-dev \ + libglib2.0-dev \ + libssl-dev \ + libtool \ + locales \ + mpg123 \ + pkg-config \ + portaudio19-dev \ + pulseaudio \ + pulseaudio-utils \ + python3 \ + python3-dev \ + python3-pip \ + python3-setuptools \ + python3-venv \ + screen \ + sudo \ + swig \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + # Set the locale RUN locale-gen en_US.UTF-8 ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 -# Add the local configuration directory -RUN mkdir ~/.mycroft -VOLUME /root/.mycroft + +# Setup the virtual environment +# This may not be the most efficient way to do this in terms of number of +# steps, but it is built to take advantage of Docker's caching mechanism +# to only rebuild things that have changed since the last build. +RUN mkdir /opt/mycroft +RUN mkdir /var/log/mycroft +WORKDIR /opt/mycroft +RUN mkdir mycroft-core skills ~/.mycroft +RUN python3 -m venv "/opt/mycroft/mycroft-core/.venv" +RUN mycroft-core/.venv/bin/python -m pip install pip==20.0.2 +COPY requirements.txt mycroft-core +RUN mycroft-core/.venv/bin/python -m pip install -r mycroft-core/requirements.txt +COPY . mycroft-core EXPOSE 8181 + # Integration Test Suite +# +# Build against this target to set the container up as an executable that +# will run the "voight_kampff" integration test suite. +# FROM builder as voight_kampff # Add the mycroft core virtual environment to the system path. ENV PATH /opt/mycroft/mycroft-core/.venv/bin:$PATH -WORKDIR /opt/mycroft/mycroft-core/test/integrationtests/voight_kampff +# Install required packages for test environments +RUN mycroft-core/.venv/bin/python -m pip install -r mycroft-core/test-requirements.txt +RUN mycroft-core/.venv/bin/python -m pip install --no-deps /opt/mycroft/mycroft-core +RUN mkdir ~/.mycroft/allure-result + # Install Mark I default skills RUN msm -p mycroft_mark_1 default + # The behave feature files for a skill are defined within the skill's # repository. Copy those files into the local feature file directory # for test discovery. -RUN python -m test.integrationtests.voight_kampff.test_setup -c default.yml -RUN mkdir ~/.mycroft/allure-result +WORKDIR /opt/mycroft/mycroft-core +# Generate hash of required packages +RUN md5sum requirements.txt test-requirements.txt dev_setup.sh > .installed +RUN python -m test.integrationtests.voight_kampff.test_setup -c test/integrationtests/voight_kampff/default.yml + # Setup and run the integration tests +WORKDIR /opt/mycroft/mycroft-core/test/integrationtests/voight_kampff ENTRYPOINT "./run_test_suite.sh" From d0a6ff4a8036cfd6f53f398f8ead29e2e48fd438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 6 Mar 2020 13:49:51 +0100 Subject: [PATCH 17/96] Add mycroft-core to python-path --- test/Dockerfile.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Dockerfile.test b/test/Dockerfile.test index e067101c6e6f..2dc82c173f1b 100644 --- a/test/Dockerfile.test +++ b/test/Dockerfile.test @@ -91,5 +91,6 @@ RUN md5sum requirements.txt test-requirements.txt dev_setup.sh > .installed RUN python -m test.integrationtests.voight_kampff.test_setup -c test/integrationtests/voight_kampff/default.yml # Setup and run the integration tests +ENV PYTHONPATH /opt/mycroft/mycroft-core/ WORKDIR /opt/mycroft/mycroft-core/test/integrationtests/voight_kampff ENTRYPOINT "./run_test_suite.sh" From 0edccca65a0e3bbe68d07e4a1c84dc8336da28ae Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 13 Feb 2020 12:44:22 -0600 Subject: [PATCH 18/96] remove the -v from the chown command in prepare-msm This reduces the verbosity during the operation. This is not of much interest, mainly success or failure is what matters. --- scripts/prepare-msm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare-msm.sh b/scripts/prepare-msm.sh index a7f948edb90f..aae08b96f94c 100755 --- a/scripts/prepare-msm.sh +++ b/scripts/prepare-msm.sh @@ -43,7 +43,7 @@ fi # change ownership of ${mycroft_root_dir} to ${setup_user } recursively function change_ownership { echo "Changing ownership of" ${mycroft_root_dir} "to user:" ${setup_user} "with group:" ${setup_group} - $SUDO chown -Rvf ${setup_user}:${setup_group} ${mycroft_root_dir} + $SUDO chown -Rf ${setup_user}:${setup_group} ${mycroft_root_dir} } From 7c038f19d95b5c9b3f93644ec8dc659d542e114f Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 13 Feb 2020 13:06:00 -0600 Subject: [PATCH 19/96] fixed spelling of voight-kampff for consistency --- Jenkinsfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0d98a3dc28d0..79b3b6b4ff23 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -33,14 +33,14 @@ pipeline { timeout(time: 10, unit: 'MINUTES') { sh 'docker run \ - -v "$HOME/voigtmycroft:/root/.mycroft" \ + -v "$HOME/voight-kampff:/root/.mycroft" \ mycroft-core:${BRANCH_ALIAS}' } } post { always { echo 'Report Test Results' - sh 'mv $HOME/voigtmycroft/allure-result allure-result' + sh 'mv $HOME/voight-kampff/allure-result allure-result' script { allure([ includeProperties: false, @@ -55,8 +55,8 @@ pipeline { label: 'Publish Report to Web Server', script: '''scp allure-report.zip root@157.245.127.234:~; ssh root@157.245.127.234 "unzip -o ~/allure-report.zip"; - ssh root@157.245.127.234 "rm -rf /var/www/voigt-kampff/${BRANCH_ALIAS}"; - ssh root@157.245.127.234 "mv allure-report /var/www/voigt-kampff/${BRANCH_ALIAS}" + ssh root@157.245.127.234 "rm -rf /var/www/voight-kampff/${BRANCH_ALIAS}"; + ssh root@157.245.127.234 "mv allure-report /var/www/voight-kampff/${BRANCH_ALIAS}" ''' ) echo 'Report Published' From 38d43f2bb7b82f5c4b9b112cd36eddf28c4dded7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 17 Feb 2020 10:00:31 +0100 Subject: [PATCH 20/96] Move Jenkins specific commands into Jenkinsfile Allure commandline and chown command are now run through the Jenkinsfile instead of through the run_test_suite.sh --- Jenkinsfile | 13 ++++++++++++- test/Dockerfile.test | 2 +- .../voight_kampff/run_test_suite.sh | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 79b3b6b4ff23..be934c095beb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -34,12 +34,23 @@ pipeline { { sh 'docker run \ -v "$HOME/voight-kampff:/root/.mycroft" \ - mycroft-core:${BRANCH_ALIAS}' + mycroft-core:${BRANCH_ALIAS} -f \ + allure_behave.formatter:AllureFormatter \ + -o /root/.mycroft/allure-result' } } post { always { echo 'Report Test Results' + echo 'Changing ownership...' + sh 'docker run \ + -v "$HOME/voight-kampff:/root/.mycroft" \ + --entrypoint=/bin/bash \ + mycroft-core:${BRANCH_ALIAS} \ + -x -c "chown $(id -u $USER):$(id -g $USER) \ + -R /root/.mycroft/allure-result"' + + echo 'Transfering...' sh 'mv $HOME/voight-kampff/allure-result allure-result' script { allure([ diff --git a/test/Dockerfile.test b/test/Dockerfile.test index 2dc82c173f1b..8f532c5d29fa 100644 --- a/test/Dockerfile.test +++ b/test/Dockerfile.test @@ -93,4 +93,4 @@ RUN python -m test.integrationtests.voight_kampff.test_setup -c test/integration # Setup and run the integration tests ENV PYTHONPATH /opt/mycroft/mycroft-core/ WORKDIR /opt/mycroft/mycroft-core/test/integrationtests/voight_kampff -ENTRYPOINT "./run_test_suite.sh" +ENTRYPOINT ["./run_test_suite.sh"] diff --git a/test/integrationtests/voight_kampff/run_test_suite.sh b/test/integrationtests/voight_kampff/run_test_suite.sh index 542e14c9573c..bcfe189b85e8 100755 --- a/test/integrationtests/voight_kampff/run_test_suite.sh +++ b/test/integrationtests/voight_kampff/run_test_suite.sh @@ -9,13 +9,13 @@ /opt/mycroft/mycroft-core/start-mycroft.sh all # Run the integration test suite. Results will be formatted for input into # the Allure reporting tool. -behave -f allure_behave.formatter:AllureFormatter -o ~/.mycroft/allure-result +echo "Running behave with the arguments \"$@\"" +behave $@ RESULT=$? # Stop all mycroft core services. /opt/mycroft/mycroft-core/stop-mycroft.sh all # Make the jenkins user the owner of the allure results. This allows the # jenkins job to build a report from the results -chown --recursive 110:116 ~/.mycroft/allure-result # Remove temporary skill files rm -rf ~/.mycroft/skills # Remove intent cache From d9d5f57a4f3b388022ef8fdf83e45103b7c41882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 14 Feb 2020 14:46:15 +0100 Subject: [PATCH 21/96] Add meta information about the speech dialog Provides meta data such as dialog used and data that was inserted. --- mycroft/skills/mycroft_skill/mycroft_skill.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mycroft/skills/mycroft_skill/mycroft_skill.py b/mycroft/skills/mycroft_skill/mycroft_skill.py index 77c0683e3ea6..c783ee6d3fae 100644 --- a/mycroft/skills/mycroft_skill/mycroft_skill.py +++ b/mycroft/skills/mycroft_skill/mycroft_skill.py @@ -392,9 +392,9 @@ def validator_default(utterance): validator = validator or validator_default # Speak query and wait for user response - utterance = self.dialog_renderer.render(dialog, data) - if utterance: - self.speak(utterance, expect_response=True, wait=True) + dialog_exists = self.dialog_renderer.render(dialog, data) + if dialog_exists: + self.speak_dialog(dialog, data, expect_response=True, wait=True) else: self.bus.emit(Message('mycroft.mic.listen')) return self._wait_response(is_cancel, validator, on_fail_fn, @@ -1058,7 +1058,7 @@ def register_regex(self, regex_str): re.compile(regex) # validate regex self.intent_service.register_adapt_regex(regex) - def speak(self, utterance, expect_response=False, wait=False): + def speak(self, utterance, expect_response=False, wait=False, meta=None): """Speak a sentence. Arguments: @@ -1068,11 +1068,13 @@ def speak(self, utterance, expect_response=False, wait=False): speaking the utterance. wait (bool): set to True to block while the text is being spoken. + meta: Information of what built the sentence. """ # registers the skill as being active self.enclosure.register(self.name) data = {'utterance': utterance, - 'expect_response': expect_response} + 'expect_response': expect_response, + 'meta': meta or {}} message = dig_for_message() m = message.forward("speak", data) if message \ else Message("speak", data) @@ -1096,7 +1098,7 @@ def speak_dialog(self, key, data=None, expect_response=False, wait=False): """ data = data or {} self.speak(self.dialog_renderer.render(key, data), - expect_response, wait) + expect_response, wait, meta={'dialog': key, 'data': data}) def acknowledge(self): """Acknowledge a successful request. From 15148fadf5966627e55c7eefd8e807a5be6ceab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 14 Feb 2020 17:37:35 +0100 Subject: [PATCH 22/96] Rework bus hooks Override the bus clients on_message method and collect all messages in a list. This will make it harder to miss a message during a test --- .../voight_kampff/features/environment.py | 40 ++++++++++++++----- .../features/steps/utterance_responses.py | 40 +++++-------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/test/integrationtests/voight_kampff/features/environment.py b/test/integrationtests/voight_kampff/features/environment.py index 62e842e60d8f..f6695371e748 100644 --- a/test/integrationtests/voight_kampff/features/environment.py +++ b/test/integrationtests/voight_kampff/features/environment.py @@ -16,24 +16,40 @@ from time import sleep, monotonic from msm import MycroftSkillsManager +from mycroft.audio import wait_while_speaking from mycroft.messagebus.client import MessageBusClient from mycroft.messagebus import Message from mycroft.util import create_daemon +class InterceptAllBusClient(MessageBusClient): + def __init__(self): + super().__init__() + self.messages = [] + self.message_lock = Lock() + + def on_message(self, message): + with self.message_lock: + self.messages.append(Message.deserialize(message)) + super().on_message(message) + + def get_messages(self, msg_type): + with self.message_lock: + if msg_type is None: + return [m for m in self.messages] + else: + return [m for m in self.messages if m.msg_type == msg_type] + + def clear_messages(self): + with self.message_lock: + self.messages = [] + + def before_all(context): - bus = MessageBusClient() + bus = InterceptAllBusClient() bus_connected = Event() bus.once('open', bus_connected.set) - context.speak_messages = [] - context.speak_lock = Lock() - - def on_speak(message): - with context.speak_lock: - context.speak_messages.append(message) - - bus.on('speak', on_speak) create_daemon(bus.run_forever) context.msm = MycroftSkillsManager() @@ -65,7 +81,9 @@ def after_feature(context, feature): def after_scenario(context, scenario): - with context.speak_lock: - context.speak_messages = [] + # TODO wait for skill handler complete + sleep(0.5) + wait_while_speaking() + context.bus.clear_messages() context.matched_message = None sleep(0.5) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index 9252f2f2cdf3..a17086a3ed99 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -106,29 +106,13 @@ def _match_dialog_patterns(dialogs, sentence): return False, debug -def match_dialog_patterns(dialogs, sentence): - """Match sentence against a list of dialog patterns. - - Returns simple True/False and not index - """ - index, debug = _match_dialog_patterns(dialogs, sentence) - return index is not False, debug - - -def expected_dialog_check(utterance, skill_path, dialog): - """Check that expected dialog file is used. """ - dialogs, load_debug = load_dialog_list(skill_path, dialog) - match, match_debug = match_dialog_patterns(dialogs, utterance) - return match, load_debug + match_debug - - @given('an english speaking user') -def given_impl(context): +def given_english(context): context.lang = 'en-us' @when('the user says "{text}"') -def when_impl(context, text): +def when_user_says(context, text): context.bus.emit(Message('recognizer_loop:utterance', data={'utterances': [text], 'lang': 'en-us', @@ -137,11 +121,11 @@ def when_impl(context, text): context={'client_name': 'mycroft_listener'})) -def then_timeout(then_func, context, timeout=TIMEOUT): +def then_wait(msg_type, then_func, context, timeout=TIMEOUT): count = 0 debug = '' while count < TIMEOUT: - for message in context.speak_messages: + for message in context.bus.get_messages(msg_type): status, test_dbg = then_func(message) debug += test_dbg if status: @@ -155,13 +139,11 @@ def then_timeout(then_func, context, timeout=TIMEOUT): @then('"{skill}" should reply with dialog from "{dialog}"') def then_dialog(context, skill, dialog): - skill_path = context.msm.find_skill(skill).path - def check_dialog(message): - utt = message.data['utterance'].lower() - return expected_dialog_check(utt, skill_path, dialog) + utt_dialog = message.data.get('meta', {}).get('dialog') + return (utt_dialog == dialog.replace('.dialog', ''), '') - passed, debug = then_timeout(check_dialog, context) + passed, debug = then_wait('speak', check_dialog, context) if not passed: print(debug) assert passed @@ -183,7 +165,7 @@ def check_any_messages(message): result = message is not None return (result, debug) - passed = then_timeout(check_any_messages, context) + passed = then_wait('speak', check_any_messages, context) if not passed: print('No speech recieved at all.') assert passed @@ -197,7 +179,7 @@ def check_exact_match(message): result = utt == text.lower() return (result, debug) - passed, debug = then_timeout(check_exact_match, context) + passed, debug = then_wait('speak', check_exact_match, context) if not passed: print(debug) assert passed @@ -211,7 +193,7 @@ def check_contains(message): result = text.lower() in utt return (result, debug) - passed, debug = then_timeout(check_contains, context) + passed, debug = then_wait('speak', check_contains, context) if not passed: print('No speech contained the expected content') @@ -219,7 +201,7 @@ def check_contains(message): @then('the user says "{text}"') -def when_impl(context, text): +def then_user_follow_up(context, text): time.sleep(2) wait_while_speaking() context.bus.emit(Message('recognizer_loop:utterance', From 2592c9b1c28bad8894e9d836f3aba0ef60c3f8fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 14 Feb 2020 18:16:22 +0100 Subject: [PATCH 23/96] Add clearer feedback on failures Will now print all responses received from Mycroft --- .../features/steps/utterance_responses.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index a17086a3ed99..8a04d9dd59a0 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -137,6 +137,14 @@ def then_wait(msg_type, then_func, context, timeout=TIMEOUT): return False, debug +def print_mycroft_responses(context): + messages = context.bus.get_messages('speak') + if len(messages) > 0: + print('Mycroft responded with:') + for m in messages: + print('Mycroft: "{}"'.format(m.data['utterance'])) + + @then('"{skill}" should reply with dialog from "{dialog}"') def then_dialog(context, skill, dialog): def check_dialog(message): @@ -146,6 +154,8 @@ def check_dialog(message): passed, debug = then_wait('speak', check_dialog, context) if not passed: print(debug) + print_mycroft_responses(context) + assert passed @@ -182,6 +192,7 @@ def check_exact_match(message): passed, debug = then_wait('speak', check_exact_match, context) if not passed: print(debug) + print_mycroft_responses(context) assert passed @@ -197,6 +208,7 @@ def check_contains(message): if not passed: print('No speech contained the expected content') + print_mycroft_responses(context) assert passed From 5bcc5335830df512b0a225f38149c2f0ade80caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 17 Feb 2020 12:39:09 +0100 Subject: [PATCH 24/96] Don't run scenarios marked with @xfail --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index be934c095beb..6c663826a8cf 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -36,7 +36,7 @@ pipeline { -v "$HOME/voight-kampff:/root/.mycroft" \ mycroft-core:${BRANCH_ALIAS} -f \ allure_behave.formatter:AllureFormatter \ - -o /root/.mycroft/allure-result' + -o /root/.mycroft/allure-result --tags ~@xfail' } } post { From 12f2e63cc3e2ff41288fdb73fe0b39ca14c36cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 20 Feb 2020 15:40:23 +0100 Subject: [PATCH 25/96] Share only identity and allure in separate volume - Sharing only the identity file removes the need for clearing the skill sandbox dir and padatious cache - Make things a bit cleaner with separate Allure volume --- Jenkinsfile | 13 +++++++------ .../voight_kampff/run_test_suite.sh | 7 +------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6c663826a8cf..90a0d7e12e18 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -33,10 +33,11 @@ pipeline { timeout(time: 10, unit: 'MINUTES') { sh 'docker run \ - -v "$HOME/voight-kampff:/root/.mycroft" \ - mycroft-core:${BRANCH_ALIAS} -f \ - allure_behave.formatter:AllureFormatter \ - -o /root/.mycroft/allure-result --tags ~@xfail' + -v "$HOME/voight-kampff/identity:/root/.mycroft/identity" \ + -v "$HOME/voight-kampff/:/root/allure" \ + mycroft-core:${BRANCH_ALIAS} \ + -f allure_behave.formatter:AllureFormatter \ + -o /root/allure/allure-result --tags ~@xfail' } } post { @@ -44,11 +45,11 @@ pipeline { echo 'Report Test Results' echo 'Changing ownership...' sh 'docker run \ - -v "$HOME/voight-kampff:/root/.mycroft" \ + -v "$HOME/voight-kampff/:/root/allure" \ --entrypoint=/bin/bash \ mycroft-core:${BRANCH_ALIAS} \ -x -c "chown $(id -u $USER):$(id -g $USER) \ - -R /root/.mycroft/allure-result"' + -R /root/allure/"' echo 'Transfering...' sh 'mv $HOME/voight-kampff/allure-result allure-result' diff --git a/test/integrationtests/voight_kampff/run_test_suite.sh b/test/integrationtests/voight_kampff/run_test_suite.sh index bcfe189b85e8..82dfa1c64eaa 100755 --- a/test/integrationtests/voight_kampff/run_test_suite.sh +++ b/test/integrationtests/voight_kampff/run_test_suite.sh @@ -14,11 +14,6 @@ behave $@ RESULT=$? # Stop all mycroft core services. /opt/mycroft/mycroft-core/stop-mycroft.sh all -# Make the jenkins user the owner of the allure results. This allows the -# jenkins job to build a report from the results -# Remove temporary skill files -rm -rf ~/.mycroft/skills -# Remove intent cache -rm -rf ~/.mycroft/intent_cache +# Reort the result of the behave test as exit status exit $RESULT From 75f92a66dd37f53cf4c44039f21d2c3ec553a49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 26 Feb 2020 07:21:35 +0100 Subject: [PATCH 26/96] Add handler to capture messages of specific type --- .../features/steps/utterance_responses.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index 8a04d9dd59a0..12a7455d8286 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -222,3 +222,13 @@ def then_user_follow_up(context, text): 'session': '', 'ident': time.time()}, context={'client_name': 'mycroft_listener'})) + + +@then('mycroft should send the message "{message_type}"') +def then_messagebus_message(context, message_type): + cnt = 0 + while context.bus.get_messages(message_type) == []: + if cnt > 20: + assert False, "Message not found" + break + time.sleep(0.5) From 0c873b5065d838250218eec14416a429d65eb934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 27 Feb 2020 14:31:00 +0100 Subject: [PATCH 27/96] Update skill list for default skills test --- test/integrationtests/voight_kampff/default.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/integrationtests/voight_kampff/default.yml b/test/integrationtests/voight_kampff/default.yml index 354f09124dfc..5897eca4a922 100644 --- a/test/integrationtests/voight_kampff/default.yml +++ b/test/integrationtests/voight_kampff/default.yml @@ -2,8 +2,20 @@ extra_skills: - cocktails platform: mycroft_mark_1 tested_skills: +- mycroft-alarm +- mycroft-timer - mycroft-date-time +- mycroft-npr-news - mycroft-weather - mycroft-hello-world - mycroft-pairing - mycroft-wiki +- mycroft-personal +- mycroft-npr-news +- mycroft-installer +- mycroft-singing +- mycroft-stock +- mycroft-mark-1 +- fallback-unknown +- fallback-query +- mycroft-volume From 1470151c2b4103a6e2f0dd997fda053eaa999d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Sat, 29 Feb 2020 12:09:09 +0100 Subject: [PATCH 28/96] Add alternatives for user replies --- .../voight_kampff/features/steps/utterance_responses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index 12a7455d8286..f7bb4d4ea8e6 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -212,6 +212,8 @@ def check_contains(message): assert passed +@then('the user replies with "{text}"') +@then('the user replies "{text}"') @then('the user says "{text}"') def then_user_follow_up(context, text): time.sleep(2) From b43785a40304715601af60c84f432fadbaeed837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 20 Feb 2020 16:42:41 +0100 Subject: [PATCH 29/96] Recreate messed up assert messages --- .../features/steps/utterance_responses.py | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index f7bb4d4ea8e6..164db1bf0002 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -137,12 +137,18 @@ def then_wait(msg_type, then_func, context, timeout=TIMEOUT): return False, debug -def print_mycroft_responses(context): +def mycroft_responses(context): + responses = '' messages = context.bus.get_messages('speak') if len(messages) > 0: - print('Mycroft responded with:') + responses = 'Mycroft responded with:\n' for m in messages: - print('Mycroft: "{}"'.format(m.data['utterance'])) + responses += 'Mycroft: "{}"\n'.format(m.data['utterance']) + return responses + + +def print_mycroft_responses(context): + print(mycroft_responses(context)) @then('"{skill}" should reply with dialog from "{dialog}"') @@ -153,10 +159,10 @@ def check_dialog(message): passed, debug = then_wait('speak', check_dialog, context) if not passed: - print(debug) - print_mycroft_responses(context) + assert_msg = debug + assert_msg += mycroft_responses(context) - assert passed + assert passed, assert_msg @then('"{skill}" should reply with "{example}"') @@ -164,7 +170,7 @@ def then_example(context, skill, example): skill_path = context.msm.find_skill(skill).path dialog = dialog_from_sentence(example, skill_path) print('Matching with the dialog file: {}'.format(dialog)) - assert dialog is not None + assert dialog is not None, 'No matching dialog...' then_dialog(context, skill, dialog) @@ -176,9 +182,7 @@ def check_any_messages(message): return (result, debug) passed = then_wait('speak', check_any_messages, context) - if not passed: - print('No speech recieved at all.') - assert passed + assert passed, 'No speech received at all' @then('"{skill}" should reply with exactly "{text}"') @@ -191,9 +195,9 @@ def check_exact_match(message): passed, debug = then_wait('speak', check_exact_match, context) if not passed: - print(debug) - print_mycroft_responses(context) - assert passed + assert_msg = debug + assert_msg += mycroft_responses(context) + assert passed, assert_msg @then('mycroft reply should contain "{text}"') @@ -207,9 +211,9 @@ def check_contains(message): passed, debug = then_wait('speak', check_contains, context) if not passed: - print('No speech contained the expected content') - print_mycroft_responses(context) - assert passed + assert_msg = 'No speech contained the expected content' + assert_msg += mycroft_responses(context) + assert passed, assert_msg @then('the user replies with "{text}"') From c159377acb54d32906047017e7c1c3d4add8bdd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 2 Mar 2020 19:46:49 +0100 Subject: [PATCH 30/96] Consume matched messages. --- test/integrationtests/voight_kampff/features/environment.py | 6 +++++- .../voight_kampff/features/steps/utterance_responses.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/integrationtests/voight_kampff/features/environment.py b/test/integrationtests/voight_kampff/features/environment.py index f6695371e748..b8d4a42bfdf5 100644 --- a/test/integrationtests/voight_kampff/features/environment.py +++ b/test/integrationtests/voight_kampff/features/environment.py @@ -1,4 +1,4 @@ -# Copyright 2017 Mycroft AI Inc. +# Copyright 2020 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. @@ -40,6 +40,10 @@ def get_messages(self, msg_type): else: return [m for m in self.messages if m.msg_type == msg_type] + def remove_message(self, msg): + with self.message_lock: + self.messages.remove(msg) + def clear_messages(self): with self.message_lock: self.messages = [] diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index 164db1bf0002..00d425fe97b8 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -130,6 +130,7 @@ def then_wait(msg_type, then_func, context, timeout=TIMEOUT): debug += test_dbg if status: context.matched_message = message + context.bus.remove_message(message) return True, debug time.sleep(1) count += 1 @@ -213,6 +214,7 @@ def check_contains(message): if not passed: assert_msg = 'No speech contained the expected content' assert_msg += mycroft_responses(context) + assert passed, assert_msg From 7bc0e7a855b98d3faf5044066e7c024f36116d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 3 Mar 2020 09:58:28 +0100 Subject: [PATCH 31/96] Remove allure temporary files --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index 90a0d7e12e18..ebf54208111c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -52,6 +52,7 @@ pipeline { -R /root/allure/"' echo 'Transfering...' + sh 'rm -rf allure-result/*' sh 'mv $HOME/voight-kampff/allure-result allure-result' script { allure([ From 51f07a5d96d69f2318ecff186948a944feb1eac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 3 Mar 2020 10:35:36 +0100 Subject: [PATCH 32/96] Add assert message if no dialog is recorded --- .../voight_kampff/features/steps/utterance_responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index 00d425fe97b8..e6f5d4d3942e 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -163,7 +163,7 @@ def check_dialog(message): assert_msg = debug assert_msg += mycroft_responses(context) - assert passed, assert_msg + assert passed, assert_msg or 'Mycroft didn\'t respond' @then('"{skill}" should reply with "{example}"') From b79fe04eb87f7f14d3dc6666a555a511d163631e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 3 Mar 2020 10:37:09 +0100 Subject: [PATCH 33/96] Extend timeout after scenario --- test/integrationtests/voight_kampff/features/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integrationtests/voight_kampff/features/environment.py b/test/integrationtests/voight_kampff/features/environment.py index b8d4a42bfdf5..c3fcdb6746a9 100644 --- a/test/integrationtests/voight_kampff/features/environment.py +++ b/test/integrationtests/voight_kampff/features/environment.py @@ -86,7 +86,7 @@ def after_feature(context, feature): def after_scenario(context, scenario): # TODO wait for skill handler complete - sleep(0.5) + sleep(2) wait_while_speaking() context.bus.clear_messages() context.matched_message = None From ce1952fcfc30a60e647a41b17e0a8f5090ec70d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 3 Mar 2020 10:38:14 +0100 Subject: [PATCH 34/96] Increase Jenkins job Timeout to 60 minutes --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index ebf54208111c..1ac3770cb3b0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -30,7 +30,7 @@ pipeline { sh 'cp test/Dockerfile.test Dockerfile' sh 'docker build --target voight_kampff -t mycroft-core:${BRANCH_ALIAS} .' echo 'Running Tests' - timeout(time: 10, unit: 'MINUTES') + timeout(time: 60, unit: 'MINUTES') { sh 'docker run \ -v "$HOME/voight-kampff/identity:/root/.mycroft/identity" \ From d7cab332fe111e8c61b841e6f3177c817410a96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 4 Mar 2020 13:46:06 +0100 Subject: [PATCH 35/96] Add skill to speak meta information --- mycroft/skills/mycroft_skill/mycroft_skill.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mycroft/skills/mycroft_skill/mycroft_skill.py b/mycroft/skills/mycroft_skill/mycroft_skill.py index c783ee6d3fae..4f3b6d9da672 100644 --- a/mycroft/skills/mycroft_skill/mycroft_skill.py +++ b/mycroft/skills/mycroft_skill/mycroft_skill.py @@ -1071,10 +1071,12 @@ def speak(self, utterance, expect_response=False, wait=False, meta=None): meta: Information of what built the sentence. """ # registers the skill as being active + meta = meta or {} + meta['skill'] = self.name self.enclosure.register(self.name) data = {'utterance': utterance, 'expect_response': expect_response, - 'meta': meta or {}} + 'meta': meta} message = dig_for_message() m = message.forward("speak", data) if message \ else Message("speak", data) From d451e1c1e017abead2799dd4238a8e86287786e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 4 Mar 2020 13:47:54 +0100 Subject: [PATCH 36/96] Add more info on captured response (skill and dialog) --- .../voight_kampff/features/steps/utterance_responses.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index e6f5d4d3942e..b590e8cefdc6 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -144,7 +144,11 @@ def mycroft_responses(context): if len(messages) > 0: responses = 'Mycroft responded with:\n' for m in messages: - responses += 'Mycroft: "{}"\n'.format(m.data['utterance']) + responses += 'Mycroft: ' + if 'dialog' in m.data['meta']: + responses += '{}.dialog'.format(m.data['meta']['dialog']) + responses += '({})\n'.format(m.data['meta'].get('skill')) + responses += '"{}"\n'.format(m.data['utterance']) return responses From 0ea1af126d0ed382a595119730b5fe7561b5c50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 3 Mar 2020 15:40:26 +0100 Subject: [PATCH 37/96] Reduce safety timeouts --- test/integrationtests/voight_kampff/features/environment.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/integrationtests/voight_kampff/features/environment.py b/test/integrationtests/voight_kampff/features/environment.py index c3fcdb6746a9..9affbf23ed4f 100644 --- a/test/integrationtests/voight_kampff/features/environment.py +++ b/test/integrationtests/voight_kampff/features/environment.py @@ -81,13 +81,12 @@ def after_all(context): def after_feature(context, feature): - sleep(2) + sleep(1) def after_scenario(context, scenario): # TODO wait for skill handler complete - sleep(2) + sleep(0.5) wait_while_speaking() context.bus.clear_messages() context.matched_message = None - sleep(0.5) From 9a5f1f6f7e6a1ab13875c8c3bad96a22a2d38ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 5 Mar 2020 10:27:04 +0100 Subject: [PATCH 38/96] Add shared tools --- .../voight_kampff/__init__.py | 2 + test/integrationtests/voight_kampff/tools.py | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 test/integrationtests/voight_kampff/tools.py diff --git a/test/integrationtests/voight_kampff/__init__.py b/test/integrationtests/voight_kampff/__init__.py index 5ccd4d62e4e1..b698061e7033 100644 --- a/test/integrationtests/voight_kampff/__init__.py +++ b/test/integrationtests/voight_kampff/__init__.py @@ -12,3 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # + +from .tools import emit_utterance, wait_for_dialog diff --git a/test/integrationtests/voight_kampff/tools.py b/test/integrationtests/voight_kampff/tools.py new file mode 100644 index 000000000000..945735f9efec --- /dev/null +++ b/test/integrationtests/voight_kampff/tools.py @@ -0,0 +1,53 @@ +# Copyright 2020 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. +# + +"""Common tools to use when creating step files for behave tests.""" + +import time + +from mycroft.messagebus import Message + + +def emit_utterance(bus, utt): + """Emit an utterance on the bus. + + Arguments: + bus (InterceptAllBusClient): Bus instance to listen on + dialogs (list): list of acceptable dialogs + """ + bus.emit(Message('recognizer_loop:utterance', + data={'utterances': [utt], + 'lang': 'en-us', + 'session': '', + 'ident': time.time()}, + context={'client_name': 'mycroft_listener'})) + + +def wait_for_dialog(bus, dialogs, timeout=10): + """Wait for one of the dialogs given as argument. + + Arguments: + bus (InterceptAllBusClient): Bus instance to listen on + dialogs (list): list of acceptable dialogs + timeout (int): how long to wait for the messagem, defaults to 10 sec. + """ + for t in range(timeout): + for message in bus.get_messages('speak'): + dialog = message.data.get('meta', {}).get('dialog') + if dialog in dialogs: + bus.clear_messages() + return + time.sleep(1) + bus.clear_messages() From fa88562a8cae073ca984e79dd6748dcac6cb736f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 6 Mar 2020 14:38:09 +0100 Subject: [PATCH 39/96] Fix timeout of wait for message The counter didn't increment as intended --- .../voight_kampff/features/steps/utterance_responses.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index b590e8cefdc6..34812c0b5e68 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -243,4 +243,7 @@ def then_messagebus_message(context, message_type): if cnt > 20: assert False, "Message not found" break + else: + cnt += 1 + time.sleep(0.5) From 2055b502387770c16483a3de331f94f559d774b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 6 Mar 2020 14:39:01 +0100 Subject: [PATCH 40/96] Add before_feature logging current test --- .../voight_kampff/features/environment.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/integrationtests/voight_kampff/features/environment.py b/test/integrationtests/voight_kampff/features/environment.py index 9affbf23ed4f..6723d905c8d1 100644 --- a/test/integrationtests/voight_kampff/features/environment.py +++ b/test/integrationtests/voight_kampff/features/environment.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import logging from threading import Event, Lock from time import sleep, monotonic @@ -22,6 +23,18 @@ from mycroft.util import create_daemon +def create_voight_kampff_logger(): + fmt = logging.Formatter('{asctime} | {name} | {levelname} | {message}', + style='{') + handler = logging.StreamHandler() + handler.setFormatter(fmt) + log = logging.getLogger('Voight Kampff') + log.addHandler(handler) + log.setLevel(logging.INFO) + log.propagate = False + return log + + class InterceptAllBusClient(MessageBusClient): def __init__(self): super().__init__() @@ -50,6 +63,7 @@ def clear_messages(self): def before_all(context): + log = create_voight_kampff_logger() bus = InterceptAllBusClient() bus_connected = Event() bus.once('open', bus_connected.set) @@ -58,10 +72,10 @@ def before_all(context): context.msm = MycroftSkillsManager() # Wait for connection - print('Waiting for messagebus connection...') + log.info('Waiting for messagebus connection...') bus_connected.wait() - print('Waiting for skills to be loaded...') + log.info('Waiting for skills to be loaded...') start = monotonic() while True: response = bus.wait_for_response(Message('mycroft.skills.all_loaded')) @@ -74,6 +88,11 @@ def before_all(context): context.bus = bus context.matched_message = None + context.log = log + + +def before_feature(context, feature): + context.log.info('Starting tests for {}'.format(feature.name)) def after_all(context): @@ -81,6 +100,8 @@ def after_all(context): def after_feature(context, feature): + context.log.info('Result: {} ({:.2f}s)'.format(str(feature.status.name), + feature.duration)) sleep(1) From 0d5520be4aaa7aad22bd380042c8e4d0f1278f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 9 Mar 2020 14:02:37 +0100 Subject: [PATCH 41/96] Reduce sleep time while waiting for response --- .../features/steps/utterance_responses.py | 9 +++++---- test/integrationtests/voight_kampff/tools.py | 10 +++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index 34812c0b5e68..5443b740a5d9 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -28,6 +28,7 @@ TIMEOUT = 10 +SLEEP_LENGTH = 0.25 def find_dialog(skill_path, dialog): @@ -124,7 +125,7 @@ def when_user_says(context, text): def then_wait(msg_type, then_func, context, timeout=TIMEOUT): count = 0 debug = '' - while count < TIMEOUT: + while count < int(timeout * (1 / SLEEP_LENGTH)): for message in context.bus.get_messages(msg_type): status, test_dbg = then_func(message) debug += test_dbg @@ -132,7 +133,7 @@ def then_wait(msg_type, then_func, context, timeout=TIMEOUT): context.matched_message = message context.bus.remove_message(message) return True, debug - time.sleep(1) + time.sleep(SLEEP_LENGTH) count += 1 # Timed out return debug from test return False, debug @@ -240,10 +241,10 @@ def then_user_follow_up(context, text): def then_messagebus_message(context, message_type): cnt = 0 while context.bus.get_messages(message_type) == []: - if cnt > 20: + if cnt > int(TIMEOUT * (1.0 / SLEEP_LENGTH)): assert False, "Message not found" break else: cnt += 1 - time.sleep(0.5) + time.sleep(SLEEP_LENGTH) diff --git a/test/integrationtests/voight_kampff/tools.py b/test/integrationtests/voight_kampff/tools.py index 945735f9efec..3adab9714f7d 100644 --- a/test/integrationtests/voight_kampff/tools.py +++ b/test/integrationtests/voight_kampff/tools.py @@ -20,6 +20,10 @@ from mycroft.messagebus import Message +SLEEP_LENGTH = 0.25 +TIMEOUT = 10 + + def emit_utterance(bus, utt): """Emit an utterance on the bus. @@ -35,7 +39,7 @@ def emit_utterance(bus, utt): context={'client_name': 'mycroft_listener'})) -def wait_for_dialog(bus, dialogs, timeout=10): +def wait_for_dialog(bus, dialogs, timeout=TIMEOUT): """Wait for one of the dialogs given as argument. Arguments: @@ -43,11 +47,11 @@ def wait_for_dialog(bus, dialogs, timeout=10): dialogs (list): list of acceptable dialogs timeout (int): how long to wait for the messagem, defaults to 10 sec. """ - for t in range(timeout): + for t in range(int(timeout * (1 / SLEEP_LENGTH))): for message in bus.get_messages('speak'): dialog = message.data.get('meta', {}).get('dialog') if dialog in dialogs: bus.clear_messages() return - time.sleep(1) + time.sleep(SLEEP_LENGTH) bus.clear_messages() From 353239cbc2c5d03ed4cb9d8b8d44c29283a121fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 11 Mar 2020 07:47:36 +0100 Subject: [PATCH 42/96] Move more common functions to voight kampff tools --- .../voight_kampff/__init__.py | 3 +- .../features/steps/utterance_responses.py | 37 +------------ test/integrationtests/voight_kampff/tools.py | 54 +++++++++++++++++++ 3 files changed, 58 insertions(+), 36 deletions(-) diff --git a/test/integrationtests/voight_kampff/__init__.py b/test/integrationtests/voight_kampff/__init__.py index b698061e7033..c77a32ba3d82 100644 --- a/test/integrationtests/voight_kampff/__init__.py +++ b/test/integrationtests/voight_kampff/__init__.py @@ -13,4 +13,5 @@ # limitations under the License. # -from .tools import emit_utterance, wait_for_dialog +from .tools import (emit_utterance, wait_for_dialog, then_wait, + mycroft_responses, print_mycroft_responses) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index 5443b740a5d9..d284b67c5de5 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -26,6 +26,8 @@ from mycroft.messagebus import Message from mycroft.audio import wait_while_speaking +from test.integrationtests.voight_kampff import mycroft_responses, then_wait + TIMEOUT = 10 SLEEP_LENGTH = 0.25 @@ -122,41 +124,6 @@ def when_user_says(context, text): context={'client_name': 'mycroft_listener'})) -def then_wait(msg_type, then_func, context, timeout=TIMEOUT): - count = 0 - debug = '' - while count < int(timeout * (1 / SLEEP_LENGTH)): - for message in context.bus.get_messages(msg_type): - status, test_dbg = then_func(message) - debug += test_dbg - if status: - context.matched_message = message - context.bus.remove_message(message) - return True, debug - time.sleep(SLEEP_LENGTH) - count += 1 - # Timed out return debug from test - return False, debug - - -def mycroft_responses(context): - responses = '' - messages = context.bus.get_messages('speak') - if len(messages) > 0: - responses = 'Mycroft responded with:\n' - for m in messages: - responses += 'Mycroft: ' - if 'dialog' in m.data['meta']: - responses += '{}.dialog'.format(m.data['meta']['dialog']) - responses += '({})\n'.format(m.data['meta'].get('skill')) - responses += '"{}"\n'.format(m.data['utterance']) - return responses - - -def print_mycroft_responses(context): - print(mycroft_responses(context)) - - @then('"{skill}" should reply with dialog from "{dialog}"') def then_dialog(context, skill, dialog): def check_dialog(message): diff --git a/test/integrationtests/voight_kampff/tools.py b/test/integrationtests/voight_kampff/tools.py index 3adab9714f7d..ccad65c6666f 100644 --- a/test/integrationtests/voight_kampff/tools.py +++ b/test/integrationtests/voight_kampff/tools.py @@ -24,6 +24,60 @@ TIMEOUT = 10 +def then_wait(msg_type, criteria_func, context, timeout=TIMEOUT): + """Wait for a specified time for criteria to be fulfilled. + + Arguments: + msg_type: message type to watch + criteria_func: Function to determine if a message fulfilling the + test case has been found. + context: behave context + timeout: Time allowance for a message fulfilling the criteria + + Returns: + tuple (bool, str) test status and debug output + """ + count = 0 + debug = '' + while count < int(timeout * (1 / SLEEP_LENGTH)): + for message in context.bus.get_messages(msg_type): + status, test_dbg = criteria_func(message) + debug += test_dbg + if status: + context.matched_message = message + context.bus.remove_message(message) + return True, debug + time.sleep(SLEEP_LENGTH) + count += 1 + # Timed out return debug from test + return False, debug + + +def mycroft_responses(context): + """Collect and format mycroft responses from context. + + Arguments: + context: behave context to extract messages from. + + Returns: (str) Mycroft responses including skill and dialog file + """ + responses = '' + messages = context.bus.get_messages('speak') + if len(messages) > 0: + responses = 'Mycroft responded with:\n' + for m in messages: + responses += 'Mycroft: ' + if 'dialog' in m.data['meta']: + responses += '{}.dialog'.format(m.data['meta']['dialog']) + responses += '({})\n'.format(m.data['meta'].get('skill')) + responses += '"{}"\n'.format(m.data['utterance']) + return responses + + +def print_mycroft_responses(context): + print(mycroft_responses(context)) + + def emit_utterance(bus, utt): """Emit an utterance on the bus. From 0f860162e3ad54fa3404140a746cb82d4b127730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 11 Mar 2020 07:49:08 +0100 Subject: [PATCH 43/96] Update run_test_suite.sh - Pulseaudio is launched if on CI - remove hard-coding of start-mycroft.sh --- test/integrationtests/voight_kampff/run_test_suite.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/integrationtests/voight_kampff/run_test_suite.sh b/test/integrationtests/voight_kampff/run_test_suite.sh index 82dfa1c64eaa..7286fb9c5c25 100755 --- a/test/integrationtests/voight_kampff/run_test_suite.sh +++ b/test/integrationtests/voight_kampff/run_test_suite.sh @@ -5,15 +5,21 @@ # runtime. Assumes running within a Docker container where the PATH environment # variable has been set to include the virtual envionrment's bin directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +# Start pulseaudio if running in CI environment +if [[ -v CI ]]; then + pulseaudio -D +fi # Start all mycroft core services. -/opt/mycroft/mycroft-core/start-mycroft.sh all +${SCRIPT_DIR}/../../../start-mycroft.sh all # Run the integration test suite. Results will be formatted for input into # the Allure reporting tool. echo "Running behave with the arguments \"$@\"" behave $@ RESULT=$? # Stop all mycroft core services. -/opt/mycroft/mycroft-core/stop-mycroft.sh all +${SCRIPT_DIR}/../../../stop-mycroft.sh all # Reort the result of the behave test as exit status exit $RESULT From d86e8257a44713e60b1f52186ca364eb74c709e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 16 Mar 2020 08:41:59 +0100 Subject: [PATCH 44/96] Add a basic mycroft.conf --- test/Dockerfile.test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Dockerfile.test b/test/Dockerfile.test index 8f532c5d29fa..309348b45c19 100644 --- a/test/Dockerfile.test +++ b/test/Dockerfile.test @@ -72,6 +72,8 @@ EXPOSE 8181 # will run the "voight_kampff" integration test suite. # FROM builder as voight_kampff +RUN mkdir /etc/mycroft +RUN echo '{"tts": {"module": "dummy"}}' > /etc/mycroft/mycroft.conf # Add the mycroft core virtual environment to the system path. ENV PATH /opt/mycroft/mycroft-core/.venv/bin:$PATH # Install required packages for test environments From 27de6c72c4f8cd4af9def0a90db2b601b1927727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 11 Mar 2020 08:43:52 +0100 Subject: [PATCH 45/96] Add empty tts output on execute exception This will make sure that the isSpeaking flag is cleared correctly --- mycroft/tts/tts.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mycroft/tts/tts.py b/mycroft/tts/tts.py index 3ab244bb773e..9fd5163c57ec 100644 --- a/mycroft/tts/tts.py +++ b/mycroft/tts/tts.py @@ -51,6 +51,7 @@ def __init__(self, queue): self._terminated = False self._processing_queue = False self.enclosure = None + self.p = None # Check if the tts shall have a ducking role set if Configuration.get().get('tts', {}).get('pulse_duck'): self.pulse_env = _TTS_ENV @@ -102,8 +103,9 @@ def run(self): self.p = play_mp3(data, environment=self.pulse_env) if visemes: self.show_visemes(visemes) - self.p.communicate() - self.p.wait() + if self.p: + self.p.communicate() + self.p.wait() report_timing(ident, 'speech_playback', stopwatch) if self.queue.empty(): @@ -312,6 +314,13 @@ def execute(self, sentence, ident=None, listen=False): sentence = self.validate_ssml(sentence) create_signal("isSpeaking") + try: + self._execute(sentence, ident, listen) + except Exception: + # If an error occurs end the audio sequence + self.queue.put((None, None, None, None, None)) + + def _execute(self, sentence, ident, listen): if self.phonetic_spelling: for word in re.findall(r"[\w']+", sentence): if word.lower() in self.spellings: From 223274d2ad4d3ef1fdaedb2b2cb79206a4abe629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 12 Mar 2020 12:00:24 +0100 Subject: [PATCH 46/96] Update environment variables for job Set USER and CI --- test/Dockerfile.test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Dockerfile.test b/test/Dockerfile.test index 309348b45c19..7d1a0200d578 100644 --- a/test/Dockerfile.test +++ b/test/Dockerfile.test @@ -50,6 +50,8 @@ ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 +ENV USER root +ENV CI true # Setup the virtual environment # This may not be the most efficient way to do this in terms of number of # steps, but it is built to take advantage of Docker's caching mechanism From b52722faf3350e226152c0a88325ceac09532fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 16 Mar 2020 08:33:50 +0100 Subject: [PATCH 47/96] Add dummy TTS backend The backend doesn't generate any audio playback, only consumes the messages --- mycroft/tts/dummy_tts.py | 45 ++++++++++++++++++++++++++++++++++++++++ mycroft/tts/tts.py | 4 +++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 mycroft/tts/dummy_tts.py diff --git a/mycroft/tts/dummy_tts.py b/mycroft/tts/dummy_tts.py new file mode 100644 index 000000000000..86172d80c7a5 --- /dev/null +++ b/mycroft/tts/dummy_tts.py @@ -0,0 +1,45 @@ +# Copyright 2020 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. +# + +"""A Dummy TTS without any audio output.""" + +from mycroft.util.log import LOG + +from .tts import TTS, TTSValidator + + +class DummyTTS(TTS): + def __init__(self, lang, config): + super().__init__(lang, config, DummyValidator(self), 'wav') + + def execute(self, sentence, ident=None, listen=False): + """Don't do anything, return nothing.""" + LOG.info('Mycroft: {}'.format(sentence)) + return None + + +class DummyValidator(TTSValidator): + """Do no tests.""" + def __init__(self, tts): + super().__init__(tts) + + def validate_lang(self): + pass + + def validate_connection(self): + pass + + def get_tts_class(self): + return DummyTTS diff --git a/mycroft/tts/tts.py b/mycroft/tts/tts.py index 9fd5163c57ec..ccbc32e13203 100644 --- a/mycroft/tts/tts.py +++ b/mycroft/tts/tts.py @@ -471,6 +471,7 @@ class TTSFactory: from mycroft.tts.responsive_voice_tts import ResponsiveVoice from mycroft.tts.mimic2_tts import Mimic2 from mycroft.tts.yandex_tts import YandexTTS + from mycroft.tts.dummy_tts import DummyTTS CLASSES = { "mimic": Mimic, @@ -483,7 +484,8 @@ class TTSFactory: "watson": WatsonTTS, "bing": BingTTS, "responsive_voice": ResponsiveVoice, - "yandex": YandexTTS + "yandex": YandexTTS, + "dummy": DummyTTS } @staticmethod From 0ad91ce321ece28456a6e51e1ffacc7362674fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 17 Mar 2020 08:47:58 +0100 Subject: [PATCH 48/96] Add an event that can be waited on instead of sleep --- .../voight_kampff/features/environment.py | 2 ++ test/integrationtests/voight_kampff/tools.py | 13 ++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/integrationtests/voight_kampff/features/environment.py b/test/integrationtests/voight_kampff/features/environment.py index 6723d905c8d1..bf517477d461 100644 --- a/test/integrationtests/voight_kampff/features/environment.py +++ b/test/integrationtests/voight_kampff/features/environment.py @@ -40,10 +40,12 @@ def __init__(self): super().__init__() self.messages = [] self.message_lock = Lock() + self.new_message_available = Event() def on_message(self, message): with self.message_lock: self.messages.append(Message.deserialize(message)) + self.new_message_available.set() super().on_message(message) def get_messages(self, msg_type): diff --git a/test/integrationtests/voight_kampff/tools.py b/test/integrationtests/voight_kampff/tools.py index ccad65c6666f..7644e5a6ee5e 100644 --- a/test/integrationtests/voight_kampff/tools.py +++ b/test/integrationtests/voight_kampff/tools.py @@ -20,7 +20,6 @@ from mycroft.messagebus import Message -SLEEP_LENGTH = 0.25 TIMEOUT = 10 @@ -37,9 +36,9 @@ def then_wait(msg_type, criteria_func, context, timeout=TIMEOUT): Returns: tuple (bool, str) test status and debug output """ - count = 0 + start_time = time.monotonic() debug = '' - while count < int(timeout * (1 / SLEEP_LENGTH)): + while time.monotonic() < start_time + timeout: for message in context.bus.get_messages(msg_type): status, test_dbg = criteria_func(message) debug += test_dbg @@ -47,8 +46,7 @@ def then_wait(msg_type, criteria_func, context, timeout=TIMEOUT): context.matched_message = message context.bus.remove_message(message) return True, debug - time.sleep(SLEEP_LENGTH) - count += 1 + context.bus.new_message_available.wait(0.5) # Timed out return debug from test return False, debug @@ -101,11 +99,12 @@ def wait_for_dialog(bus, dialogs, timeout=TIMEOUT): dialogs (list): list of acceptable dialogs timeout (int): how long to wait for the messagem, defaults to 10 sec. """ - for t in range(int(timeout * (1 / SLEEP_LENGTH))): + start_time = time.monotonic() + while time.monotonic() < start_time + timeout: for message in bus.get_messages('speak'): dialog = message.data.get('meta', {}).get('dialog') if dialog in dialogs: bus.clear_messages() return - time.sleep(SLEEP_LENGTH) + bus.new_message_available.wait(0.5) bus.clear_messages() From 35d955899801d3b8882374d22442f63171ee45ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 17 Mar 2020 14:08:44 +0100 Subject: [PATCH 49/96] Handle communication failure with converse handler --- mycroft/skills/intent_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mycroft/skills/intent_service.py b/mycroft/skills/intent_service.py index 93f348109b61..9c0b5397ee63 100644 --- a/mycroft/skills/intent_service.py +++ b/mycroft/skills/intent_service.py @@ -230,8 +230,10 @@ def do_converse(self, utterances, skill_id, lang, message): if result and 'error' in result.data: self.handle_converse_error(result) return False - else: + elif result is not None: return result.data.get('result', False) + else: + return False def handle_converse_error(self, message): skill_id = message.data["skill_id"] From a6d1e91942943d0aebc4f2c638bd7e2be075ba45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 17 Mar 2020 15:02:13 +0100 Subject: [PATCH 50/96] Add auto-retry This switches behave to the current dev release including the autoretry system. --- test-requirements.txt | 2 +- test/integrationtests/voight_kampff/features/environment.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index dca9a4b830ba..874a0f130d47 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,5 +5,5 @@ pytest-cov==2.8.1 cov-core==1.15.0 sphinx==2.2.1 sphinx-rtd-theme==0.4.3 -behave==1.2.6 +git+https://github.com/behave/behave@v1.2.7.dev1 allure-behave==2.8.10 diff --git a/test/integrationtests/voight_kampff/features/environment.py b/test/integrationtests/voight_kampff/features/environment.py index bf517477d461..fa1d87011063 100644 --- a/test/integrationtests/voight_kampff/features/environment.py +++ b/test/integrationtests/voight_kampff/features/environment.py @@ -15,6 +15,7 @@ import logging from threading import Event, Lock from time import sleep, monotonic +from behave.contrib.scenario_autoretry import patch_scenario_with_autoretry from msm import MycroftSkillsManager from mycroft.audio import wait_while_speaking @@ -95,6 +96,8 @@ def before_all(context): def before_feature(context, feature): context.log.info('Starting tests for {}'.format(feature.name)) + for scenario in feature.scenarios: + patch_scenario_with_autoretry(scenario, max_attempts=2) def after_all(context): From 1a643b0d4d2ef7811d2292bc0fa0f2070eff64d8 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Tue, 17 Mar 2020 10:40:14 -0500 Subject: [PATCH 51/96] re-order build steps for performance. least likely to change first. --- test/Dockerfile.test | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/test/Dockerfile.test b/test/Dockerfile.test index 7d1a0200d578..87e3c85287eb 100644 --- a/test/Dockerfile.test +++ b/test/Dockerfile.test @@ -5,7 +5,8 @@ # like Jenkins jobs don't spend a lot of time re-building things that did not # change from one build to the next. # -FROM ubuntu:18.04 as builder +FROM ubuntu:18.04 as core_builder +ARG platform ENV TERM linux ENV DEBIAN_FRONTEND noninteractive # Un-comment any package sources that include a multiverse @@ -56,15 +57,27 @@ ENV CI true # This may not be the most efficient way to do this in terms of number of # steps, but it is built to take advantage of Docker's caching mechanism # to only rebuild things that have changed since the last build. -RUN mkdir /opt/mycroft -RUN mkdir /var/log/mycroft -WORKDIR /opt/mycroft -RUN mkdir mycroft-core skills ~/.mycroft +RUN mkdir -p /opt/mycroft/mycroft-core /opt/mycroft/skills /root/.mycroft /var/log/mycroft RUN python3 -m venv "/opt/mycroft/mycroft-core/.venv" -RUN mycroft-core/.venv/bin/python -m pip install pip==20.0.2 -COPY requirements.txt mycroft-core -RUN mycroft-core/.venv/bin/python -m pip install -r mycroft-core/requirements.txt -COPY . mycroft-core + +# Install required Python packages. Generate hash, which mycroft core uses to +# determine if any changes have been made since it last started +WORKDIR /opt/mycroft/mycroft-core +RUN .venv/bin/python -m pip install pip==20.0.2 +COPY requirements.txt . +RUN .venv/bin/python -m pip install -r requirements.txt +COPY test-requirements.txt . +RUN .venv/bin/python -m pip install -r test-requirements.txt +COPY dev_setup.sh . +RUN md5sum requirements.txt test-requirements.txt dev_setup.sh > .installed + +# Add the mycroft core virtual environment to the system path. +ENV PATH /opt/mycroft/mycroft-core/.venv/bin:$PATH + +# Install Mark I default skills +RUN msm -p mycroft_mark_1 default +COPY . /opt/mycroft/mycroft-core +RUN .venv/bin/python -m pip install --no-deps /opt/mycroft/mycroft-core EXPOSE 8181 @@ -73,25 +86,18 @@ EXPOSE 8181 # Build against this target to set the container up as an executable that # will run the "voight_kampff" integration test suite. # -FROM builder as voight_kampff +FROM core_builder as voight_kampff_builder +ARG platform +# Setup a dummy TTS backend for the audio process RUN mkdir /etc/mycroft RUN echo '{"tts": {"module": "dummy"}}' > /etc/mycroft/mycroft.conf -# Add the mycroft core virtual environment to the system path. -ENV PATH /opt/mycroft/mycroft-core/.venv/bin:$PATH -# Install required packages for test environments -RUN mycroft-core/.venv/bin/python -m pip install -r mycroft-core/test-requirements.txt -RUN mycroft-core/.venv/bin/python -m pip install --no-deps /opt/mycroft/mycroft-core RUN mkdir ~/.mycroft/allure-result -# Install Mark I default skills -RUN msm -p mycroft_mark_1 default - # The behave feature files for a skill are defined within the skill's # repository. Copy those files into the local feature file directory # for test discovery. WORKDIR /opt/mycroft/mycroft-core # Generate hash of required packages -RUN md5sum requirements.txt test-requirements.txt dev_setup.sh > .installed RUN python -m test.integrationtests.voight_kampff.test_setup -c test/integrationtests/voight_kampff/default.yml # Setup and run the integration tests From 71b2c3549e3c1fe8f0c94e26bc38f74a21aec423 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Tue, 17 Mar 2020 10:46:53 -0500 Subject: [PATCH 52/96] changed naming of docker image and added platform variable --- Jenkinsfile | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1ac3770cb3b0..6870f6cfbd3d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,16 +4,7 @@ pipeline { // Running builds concurrently could cause a race condition with // building the Docker image. disableConcurrentBuilds() - } - environment { - // Some branches have a "/" in their name (e.g. feature/new-and-cool) - // Some commands, such as those tha deal with directories, don't - // play nice with this naming convention. Define an alias for the - // branch name that can be used in these scenarios. - BRANCH_ALIAS = sh( - script: 'echo $BRANCH_NAME | sed -e "s#/#_#g"', - returnStdout: true - ).trim() + buildDiscarder(logRotator(numToKeepStr: '5')) } stages { // Run the build in the against the dev branch to check for compile errors @@ -25,17 +16,30 @@ pipeline { changeRequest target: 'dev' } } + environment { + // Some branches have a "/" in their name (e.g. feature/new-and-cool) + // Some commands, such as those tha deal with directories, don't + // play nice with this naming convention. Define an alias for the + // branch name that can be used in these scenarios. + BRANCH_ALIAS = sh( + script: 'echo $BRANCH_NAME | sed -e "s#/#_#g"', + returnStdout: true + ).trim() + } steps { - echo 'Building Test Docker Image' + echo 'Building Mark I Voight-Kampff Docker Image' sh 'cp test/Dockerfile.test Dockerfile' - sh 'docker build --target voight_kampff -t mycroft-core:${BRANCH_ALIAS} .' - echo 'Running Tests' + sh 'docker build \ + --target voight_kampff_builder \ + --build-arg platform=mycroft_mark_1 \ + -t voight-kampff-mark-1:${BRANCH_ALIAS} .' + echo 'Running Mark I Voight-Kampff Test Suite' timeout(time: 60, unit: 'MINUTES') { sh 'docker run \ -v "$HOME/voight-kampff/identity:/root/.mycroft/identity" \ -v "$HOME/voight-kampff/:/root/allure" \ - mycroft-core:${BRANCH_ALIAS} \ + voight-kampff-mark-1:${BRANCH_ALIAS} \ -f allure_behave.formatter:AllureFormatter \ -o /root/allure/allure-result --tags ~@xfail' } @@ -47,11 +51,11 @@ pipeline { sh 'docker run \ -v "$HOME/voight-kampff/:/root/allure" \ --entrypoint=/bin/bash \ - mycroft-core:${BRANCH_ALIAS} \ + voight-kampff-mark-1:${BRANCH_ALIAS} \ -x -c "chown $(id -u $USER):$(id -g $USER) \ -R /root/allure/"' - echo 'Transfering...' + echo 'Transferring...' sh 'rm -rf allure-result/*' sh 'mv $HOME/voight-kampff/allure-result allure-result' script { From 5801a6af09600c83666b12d361613df51de499dd Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Tue, 17 Mar 2020 14:45:23 -0500 Subject: [PATCH 53/96] added stage to build major release docker image for skill testing --- Jenkinsfile | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 6870f6cfbd3d..62bad4686664 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,6 +80,33 @@ pipeline { } } } + // Build a voight_kampff image for major releases. This will be used + // by the mycroft-skills repository to test skill changes. Skills are + // tested against major releases to determine if they play nicely with + // the breaking changes included in said release. + stage('Build Major Release Image') { + when { + tag "release/v*.*.0" + } + environment { + // Tag name is usually formatted like "20.2.0" whereas skill + // branch names are usually "20.02". Reformat the tag name + // to the skill branch format so this image will be easy to find + // in the mycroft-skill repository. + SKILL_BRANCH = sh( + script: 'echo $TAG_NAME | sed -e "s/v//g" -e "s/[.]0//g" -e "s/[.]/.0/g"', + returnStdout: true + ).trim() + } + steps { + echo 'Building ${TAG_NAME} Docker Image for Skill Testing' + sh 'cp test/Dockerfile.test Dockerfile' + sh 'docker build \ + --target voight_kampff_builder \ + --build-arg platform=mycroft_mark_1 \ + -t voight-kampff-mark-1:${SKILL_BRANCH} .' + } + } } post { cleanup { From 2a7b0ccef9038fb94e621007e17552da48938e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 18 Mar 2020 14:08:15 +0100 Subject: [PATCH 54/96] Update voight kampff Readme with some extra info --- test/integrationtests/voight_kampff/README.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/integrationtests/voight_kampff/README.md b/test/integrationtests/voight_kampff/README.md index 3efe9d589b64..0639b1ae9bf6 100644 --- a/test/integrationtests/voight_kampff/README.md +++ b/test/integrationtests/voight_kampff/README.md @@ -56,3 +56,26 @@ Example phrase: `Then "" should reply with "" This will try to map the example phrase to a dialog file and will allow any response from that dialog file. + +User reply: +`Then the user says ""` + +This allows setting up scenarios with conversational aspects, e.g. when using `get_response()` in the skill. + +Example: +```feature +Scenario: Bridge of death + Given an english speaking user + When the user says "let's go to the bridge of death" + Then "death-bridge" should reply with dialog from "questions_one.dialog" + Then the user says "My name is Sir Lancelot of Camelot" + Then "death-bridge" should reply with dialog from "questions_two.dialog" + Then the user says "To seek the holy grail" + Then "death-bridge" should reply with dialog from "questions_three.dialog" + Then the user says "blue" +``` + +Mycroft messagebus message: +`mycroft should send the message ""` + +This verifies that a specific message is emitted on the messagebus. This can be used to check that a playback request is sent or other action is triggered. From e0dec90d1794141aeb71bffdcdcde7e5848a78a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 19 Mar 2020 08:33:45 +0100 Subject: [PATCH 55/96] Allow test_setup to update skills --- test/integrationtests/voight_kampff/test_setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integrationtests/voight_kampff/test_setup.py b/test/integrationtests/voight_kampff/test_setup.py index 3d0d618d13b3..8ce3c7f7a8cd 100644 --- a/test/integrationtests/voight_kampff/test_setup.py +++ b/test/integrationtests/voight_kampff/test_setup.py @@ -23,6 +23,7 @@ import yaml from msm import MycroftSkillsManager +from msm.exceptions import MsmException from .generate_feature import generate_feature @@ -101,6 +102,11 @@ def run_setup(msm, test_skills, extra_skills, num_random_skills): if not s.is_local: print('Installing {}'.format(s)) msm.install(s) + else: + try: + msm.update(s) + except MsmException: + pass # collect feature files for skill_name in test_skills: From 5d6cb83899659d43b0f15aeaaee949ae3161442e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 20 Mar 2020 08:22:56 +0100 Subject: [PATCH 56/96] Make all fields in config optional --- test/integrationtests/voight_kampff/test_setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integrationtests/voight_kampff/test_setup.py b/test/integrationtests/voight_kampff/test_setup.py index 8ce3c7f7a8cd..fd08b80240c2 100644 --- a/test/integrationtests/voight_kampff/test_setup.py +++ b/test/integrationtests/voight_kampff/test_setup.py @@ -58,11 +58,11 @@ def load_config(config, args): with open(expanduser(config)) as f: conf_dict = yaml.safe_load(f) - if not args.tested_skills: + if not args.tested_skills and 'tested_skills' in conf_dict: args.tested_skills = conf_dict['tested_skills'] - if not args.extra_skills: + if not args.extra_skills and 'extra_skills' in conf_dict: args.extra_skills = conf_dict['extra_skills'] - if not args.platform: + if not args.platform and 'platform' in conf_dict: args.platform = conf_dict['platform'] return From daeac964e0506bd3976799c3f2261af78f6bc28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 20 Mar 2020 09:37:53 +0100 Subject: [PATCH 57/96] Rename tested-skills argument to test-skills --- .../integrationtests/voight_kampff/default.yml | 4 +--- .../voight_kampff/test_setup.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/test/integrationtests/voight_kampff/default.yml b/test/integrationtests/voight_kampff/default.yml index 5897eca4a922..7ffe2f5ee176 100644 --- a/test/integrationtests/voight_kampff/default.yml +++ b/test/integrationtests/voight_kampff/default.yml @@ -1,7 +1,5 @@ -extra_skills: -- cocktails platform: mycroft_mark_1 -tested_skills: +test_skills: - mycroft-alarm - mycroft-timer - mycroft-date-time diff --git a/test/integrationtests/voight_kampff/test_setup.py b/test/integrationtests/voight_kampff/test_setup.py index fd08b80240c2..a199ce7bf3c0 100644 --- a/test/integrationtests/voight_kampff/test_setup.py +++ b/test/integrationtests/voight_kampff/test_setup.py @@ -32,8 +32,8 @@ The script sets up the selected tests in the feature directory so they can be found and executed by the behave framework. -The script also ensures that the tested skills are installed and that any -specified extra skills also gets installed into the environment. +The script also ensures that the skills marked for testing are installed and +that anyi specified extra skills also gets installed into the environment. """ FEATURE_DIR = join(dirname(__file__), 'features') + '/' @@ -58,8 +58,8 @@ def load_config(config, args): with open(expanduser(config)) as f: conf_dict = yaml.safe_load(f) - if not args.tested_skills and 'tested_skills' in conf_dict: - args.tested_skills = conf_dict['tested_skills'] + if not args.test_skills and 'test_skills' in conf_dict: + args.test_skills = conf_dict['test_skills'] if not args.extra_skills and 'extra_skills' in conf_dict: args.extra_skills = conf_dict['extra_skills'] if not args.platform and 'platform' in conf_dict: @@ -72,15 +72,15 @@ def main(cmdline_args): platforms = list(MycroftSkillsManager.SKILL_GROUPS) parser = argparse.ArgumentParser() parser.add_argument('-p', '--platform', choices=platforms) - parser.add_argument('-t', '--tested-skills', default=[]) + parser.add_argument('-t', '--test-skills', default=[]) parser.add_argument('-s', '--extra-skills', default=[]) parser.add_argument('-r', '--random-skills', default=0) parser.add_argument('-d', '--skills-dir') parser.add_argument('-c', '--config') args = parser.parse_args(cmdline_args) - if args.tested_skills: - args.tested_skills = args.tested_skills.replace(',', ' ').split() + if args.test_skills: + args.test_skills = args.test_skills.replace(',', ' ').split() if args.extra_skills: args.extra_skills = args.extra_skills.replace(',', ' ').split() @@ -91,7 +91,7 @@ def main(cmdline_args): args.platform = "mycroft_mark_1" msm = MycroftSkillsManager(args.platform, args.skills_dir) - run_setup(msm, args.tested_skills, args.extra_skills, args.random_skills) + run_setup(msm, args.test_skills, args.extra_skills, args.random_skills) def run_setup(msm, test_skills, extra_skills, num_random_skills): @@ -144,7 +144,7 @@ def print_install_report(platform, test_skills, extra_skills): print('-------- TEST SETUP --------') yml = yaml.dump({ 'platform': platform, - 'tested_skills': test_skills, + 'test_skills': test_skills, 'extra_skills': extra_skills }) print(yml) From 8e7e19ecc23cafb1ca9077ca949fb3e8d07acb44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 20 Mar 2020 09:56:00 +0100 Subject: [PATCH 58/96] Add help text to test_setup.py parameters --- .../voight_kampff/test_setup.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/integrationtests/voight_kampff/test_setup.py b/test/integrationtests/voight_kampff/test_setup.py index a199ce7bf3c0..172604be247f 100644 --- a/test/integrationtests/voight_kampff/test_setup.py +++ b/test/integrationtests/voight_kampff/test_setup.py @@ -13,6 +13,7 @@ # limitations under the License. # import argparse +from argparse import RawTextHelpFormatter from glob import glob from os.path import join, dirname, expanduser, basename from pathlib import Path @@ -70,13 +71,20 @@ def load_config(config, args): def main(cmdline_args): """Parse arguments and run environment setup.""" platforms = list(MycroftSkillsManager.SKILL_GROUPS) - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter) parser.add_argument('-p', '--platform', choices=platforms) - parser.add_argument('-t', '--test-skills', default=[]) - parser.add_argument('-s', '--extra-skills', default=[]) - parser.add_argument('-r', '--random-skills', default=0) + parser.add_argument('-t', '--test-skills', default=[], + help=('Comma-separated list of skills to test.\n' + 'Ex: "mycroft-weather, mycroft-stock"')) + parser.add_argument('-s', '--extra-skills', default=[], + help=('Comma-separated list of extra skills to ' + 'install.\n' + 'Ex: "cocktails, laugh"')) + parser.add_argument('-r', '--random-skills', default=0, + help='Number of random skills to install.') parser.add_argument('-d', '--skills-dir') - parser.add_argument('-c', '--config') + parser.add_argument('-c', '--config', + help='Path to test configuration file.') args = parser.parse_args(cmdline_args) if args.test_skills: From f8c6107ea5d299a65436096340b0a7e9b96d3bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 20 Mar 2020 11:22:39 +0100 Subject: [PATCH 59/96] Update readme with some new steps --- test/integrationtests/voight_kampff/README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/integrationtests/voight_kampff/README.md b/test/integrationtests/voight_kampff/README.md index 0639b1ae9bf6..5b0909921224 100644 --- a/test/integrationtests/voight_kampff/README.md +++ b/test/integrationtests/voight_kampff/README.md @@ -47,17 +47,22 @@ The When is the start of the test and will inject a message on the running mycro where utterance is the sentence to test. ### Then ... -The "Then" step will verify the response of the user, currently there is two ways of specifying the response. +The "Then" step will verify Mycroft's response, handle a followup action or check for messages on the messagebus. -Expected dialog: +#### Expected dialog: `"" should reply with dialog from ""` Example phrase: `Then "" should reply with "" -This will try to map the example phrase to a dialog file and will allow any response from that dialog file. +This will try to map the example phrase to a dialog file and will allow any response from that dialog file. This one is somewhat experimental et the moment. -User reply: +#### Should contain: +`mycroft reply should contain ""` + +This will match any sentence containing the specified text. + +#### User reply: `Then the user says ""` This allows setting up scenarios with conversational aspects, e.g. when using `get_response()` in the skill. @@ -75,7 +80,7 @@ Scenario: Bridge of death Then the user says "blue" ``` -Mycroft messagebus message: +#### Mycroft messagebus message: `mycroft should send the message ""` This verifies that a specific message is emitted on the messagebus. This can be used to check that a playback request is sent or other action is triggered. From c33f9ee875ac32fc9a141bf64b49101a01b03eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 23 Mar 2020 07:47:01 +0100 Subject: [PATCH 60/96] Restructure test_setup.py according to review --- .../voight_kampff/test_setup.py | 122 ++++++++++++------ 1 file changed, 82 insertions(+), 40 deletions(-) diff --git a/test/integrationtests/voight_kampff/test_setup.py b/test/integrationtests/voight_kampff/test_setup.py index 172604be247f..bfd8b44f5960 100644 --- a/test/integrationtests/voight_kampff/test_setup.py +++ b/test/integrationtests/voight_kampff/test_setup.py @@ -15,14 +15,12 @@ import argparse from argparse import RawTextHelpFormatter from glob import glob -from os.path import join, dirname, expanduser, basename -from pathlib import Path +from os.path import join, dirname, expanduser, basename, exists from random import shuffle import shutil import sys import yaml - from msm import MycroftSkillsManager from msm.exceptions import MsmException @@ -54,7 +52,7 @@ def copy_step_files(source, destination): shutil.copyfile(f, join(destination, basename(f))) -def load_config(config, args): +def apply_config(config, args): """Load config and add to unset arguments.""" with open(expanduser(config)) as f: conf_dict = yaml.safe_load(f) @@ -65,47 +63,57 @@ def load_config(config, args): args.extra_skills = conf_dict['extra_skills'] if not args.platform and 'platform' in conf_dict: args.platform = conf_dict['platform'] - return -def main(cmdline_args): - """Parse arguments and run environment setup.""" +def create_argument_parser(): + """Create the argument parser for the command line options. + + Returns: ArgumentParser + """ + class TestSkillAction(argparse.Action): + def __call__(self, parser, args, values, option_string=None): + args.test_skills = values.replace(',', ' ').split() + + class ExtraSkillAction(argparse.Action): + def __call__(self, parser, args, values, option_string=None): + args.extra_skills = values.replace(',', ' ').split() + platforms = list(MycroftSkillsManager.SKILL_GROUPS) parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter) - parser.add_argument('-p', '--platform', choices=platforms) + parser.add_argument('-p', '--platform', choices=platforms, + default='mycroft_mark_1') parser.add_argument('-t', '--test-skills', default=[], + action=TestSkillAction, help=('Comma-separated list of skills to test.\n' 'Ex: "mycroft-weather, mycroft-stock"')) parser.add_argument('-s', '--extra-skills', default=[], + action=ExtraSkillAction, help=('Comma-separated list of extra skills to ' 'install.\n' 'Ex: "cocktails, laugh"')) - parser.add_argument('-r', '--random-skills', default=0, + parser.add_argument('-r', '--random-skills', default=0, type=int, help='Number of random skills to install.') parser.add_argument('-d', '--skills-dir') parser.add_argument('-c', '--config', help='Path to test configuration file.') + return parser - args = parser.parse_args(cmdline_args) - if args.test_skills: - args.test_skills = args.test_skills.replace(',', ' ').split() - if args.extra_skills: - args.extra_skills = args.extra_skills.replace(',', ' ').split() - - if args.config: - load_config(args.config, args) - if args.platform is None: - args.platform = "mycroft_mark_1" +def get_random_skills(msm, num_random_skills): + """Install random skills from uninstalled skill list.""" + random_skills = [s for s in msm.all_skills if not s.is_local] + shuffle(random_skills) # Make them random + return [s.name for s in random_skills[:num_random_skills]] - msm = MycroftSkillsManager(args.platform, args.skills_dir) - run_setup(msm, args.test_skills, args.extra_skills, args.random_skills) +def install_or_upgrade_skills(msm, skills): + """Install needed skills if uninstalled, otherwise try to update. -def run_setup(msm, test_skills, extra_skills, num_random_skills): - """Install needed skills and collect feature files for the test.""" - skills = [msm.find_skill(s) for s in test_skills + extra_skills] - # Install test skills + Arguments: + msm: msm instance to use for the operations + skills: list of skills + """ + skills = [msm.find_skill(s) for s in skills] for s in skills: if not s.is_local: print('Installing {}'.format(s)) @@ -116,15 +124,22 @@ def run_setup(msm, test_skills, extra_skills, num_random_skills): except MsmException: pass - # collect feature files - for skill_name in test_skills: + +def collect_test_cases(msm, skills): + """Collect feature files and step files for each skill. + + Arguments: + msm: msm instance to use for the operations + skills: list of skills + """ + for skill_name in skills: skill = msm.find_skill(skill_name) behave_dir = join(skill.path, 'test', 'behave') - if Path(behave_dir).exists(): + if exists(behave_dir): copy_feature_files(behave_dir, FEATURE_DIR) step_dir = join(behave_dir, 'steps') - if Path().exists(): + if exists(step_dir): copy_step_files(step_dir, join(FEATURE_DIR, 'steps')) else: # Generate feature file if no data exists yet @@ -135,17 +150,6 @@ def run_setup(msm, test_skills, extra_skills, num_random_skills): with open(join(FEATURE_DIR, skill_name + '.feature'), 'w') as f: f.write(feature) - # Install random skills from uninstalled skill list - random_skills = [s for s in msm.all_skills if s not in msm.local_skills] - shuffle(random_skills) # Make them random - random_skills = random_skills[:num_random_skills] - for s in random_skills: - msm.install(s) - - print_install_report(msm.platform, test_skills, - extra_skills + [s.name for s in random_skills]) - return - def print_install_report(platform, test_skills, extra_skills): """Print in nice format.""" @@ -159,5 +163,43 @@ def print_install_report(platform, test_skills, extra_skills): print('----------------------------') +def get_arguments(cmdline_args): + """Get arguments for test setup. + + Parses the commandline and if specified applies configuration file. + + Arguments: + cmdline_args (list): argv like list of arguments + + Returns: + Argument parser NameSpace + """ + parser = create_argument_parser() + args = parser.parse_args(cmdline_args) + return args + + +def main(cmdline_args): + """Parse arguments and run test environment setup. + + This installs and/or upgrades any skills needed for the tests and + collects the feature and step files for the skills. + """ + args = get_arguments(cmdline_args) + if args.config: + apply_config(args.config, args) + + msm = MycroftSkillsManager(args.platform, args.skills_dir) + + random_skills = get_random_skills(msm, args.random_skills) + all_skills = args.test_skills + args.extra_skills + random_skills + + install_or_upgrade_skills(msm, all_skills) + collect_test_cases(msm, args.test_skills) + + print_install_report(msm.platform, args.test_skills, + args.extra_skills + random_skills) + + if __name__ == '__main__': main(sys.argv[1:]) From 00c83c5139e7a276dcb72a7eb3bc433b167961e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 23 Mar 2020 08:10:19 +0100 Subject: [PATCH 61/96] Add repo and branch selection for test_setup.py --url can be added to specify the repo url --branch can be added to specify a specific branch --- .../voight_kampff/test_setup.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/integrationtests/voight_kampff/test_setup.py b/test/integrationtests/voight_kampff/test_setup.py index bfd8b44f5960..27bc727b64ea 100644 --- a/test/integrationtests/voight_kampff/test_setup.py +++ b/test/integrationtests/voight_kampff/test_setup.py @@ -21,7 +21,7 @@ import sys import yaml -from msm import MycroftSkillsManager +from msm import MycroftSkillsManager, SkillRepo from msm.exceptions import MsmException from .generate_feature import generate_feature @@ -94,6 +94,10 @@ def __call__(self, parser, args, values, option_string=None): parser.add_argument('-r', '--random-skills', default=0, type=int, help='Number of random skills to install.') parser.add_argument('-d', '--skills-dir') + parser.add_argument('-u', '--repo-url', + help='URL for skills repo to install / update from') + parser.add_argument('-b', '--branch', + help='repo branch to use') parser.add_argument('-c', '--config', help='Path to test configuration file.') return parser @@ -179,6 +183,22 @@ def get_arguments(cmdline_args): return args +def create_skills_manager(platform, skills_dir, url, branch): + """Create mycroft skills manager for the given url / branch. + + Arguments: + platform (str): platform to use + skills_dir (str): skill directory to use + url (str): skills repo url + branch (str): skills repo branch + + Returns: + MycroftSkillsManager + """ + repo = SkillRepo(url=url, branch=branch) + return MycroftSkillsManager(platform, skills_dir, repo) + + def main(cmdline_args): """Parse arguments and run test environment setup. @@ -189,7 +209,8 @@ def main(cmdline_args): if args.config: apply_config(args.config, args) - msm = MycroftSkillsManager(args.platform, args.skills_dir) + msm = create_skills_manager(args.platform, args.skills_dir, + args.repo_url, args.branch) random_skills = get_random_skills(msm, args.random_skills) all_skills = args.test_skills + args.extra_skills + random_skills From 9fe158d8419a1966bd52855b0ddeb109e5ad98c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 23 Mar 2020 09:15:04 +0100 Subject: [PATCH 62/96] Prepare for multi-lang support --- .../features/steps/utterance_responses.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py index d284b67c5de5..4bd36056bcb0 100644 --- a/test/integrationtests/voight_kampff/features/steps/utterance_responses.py +++ b/test/integrationtests/voight_kampff/features/steps/utterance_responses.py @@ -33,15 +33,15 @@ SLEEP_LENGTH = 0.25 -def find_dialog(skill_path, dialog): +def find_dialog(skill_path, dialog, lang): """Check the usual location for dialogs. TODO: subfolders """ if exists(join(skill_path, 'dialog')): - return join(skill_path, 'dialog', 'en-us', dialog) + return join(skill_path, 'dialog', lang, dialog) else: - return join(skill_path, 'locale', 'en-us', dialog) + return join(skill_path, 'locale', lang, dialog) def load_dialog_file(dialog_path): @@ -68,9 +68,17 @@ def load_dialog_list(skill_path, dialog): return load_dialog_file(dialog_path), debug -def dialog_from_sentence(sentence, skill_path): - """Find dialog file from example sentence.""" - dialog_paths = join(skill_path, 'dialog', 'en-us', '*.dialog') +def dialog_from_sentence(sentence, skill_path, lang): + """Find dialog file from example sentence. + + Arguments: + sentence (str): Text to match + skill_path (str): path to skill directory + lang (str): language code to use + + Returns (str): Dialog file best matching the sentence. + """ + dialog_paths = join(skill_path, 'dialog', lang, '*.dialog') best = (None, 0) for path in glob(dialog_paths): patterns = load_dialog_file(path) @@ -118,7 +126,7 @@ def given_english(context): def when_user_says(context, text): context.bus.emit(Message('recognizer_loop:utterance', data={'utterances': [text], - 'lang': 'en-us', + 'lang': context.lang, 'session': '', 'ident': time.time()}, context={'client_name': 'mycroft_listener'})) @@ -141,7 +149,7 @@ def check_dialog(message): @then('"{skill}" should reply with "{example}"') def then_example(context, skill, example): skill_path = context.msm.find_skill(skill).path - dialog = dialog_from_sentence(example, skill_path) + dialog = dialog_from_sentence(example, skill_path, context.lang) print('Matching with the dialog file: {}'.format(dialog)) assert dialog is not None, 'No matching dialog...' then_dialog(context, skill, dialog) @@ -198,7 +206,7 @@ def then_user_follow_up(context, text): wait_while_speaking() context.bus.emit(Message('recognizer_loop:utterance', data={'utterances': [text], - 'lang': 'en-us', + 'lang': context.lang, 'session': '', 'ident': time.time()}, context={'client_name': 'mycroft_listener'})) From 3fb37e7793ba54aac0c40fbcead94d060e97e93e Mon Sep 17 00:00:00 2001 From: ChanceNCounter Date: Tue, 24 Mar 2020 17:01:10 -0700 Subject: [PATCH 63/96] upgrade Lingua Franca to 0.2.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 26ad94823c15..6ef47dcd11ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ google-api-python-client==1.6.4 fasteners==0.14.1 PyYAML==5.1.2 -lingua-franca==0.2.0 +lingua-franca==0.2.1 msm==0.8.7 msk==0.3.14 adapt-parser==0.3.4 From 1a96f0402021d60af36b630465d460ce78195d50 Mon Sep 17 00:00:00 2001 From: Kris Gesling Date: Tue, 24 Mar 2020 20:09:22 +0930 Subject: [PATCH 64/96] Add single Voight Kampff module interface Takes in arguments for both test_setup.py and behave test runner. Parses any args for test_setup and passes any remaining arguments to behave. This moves argparsing out of the test_setup main() allowing the helper commands to pass in pre-parsed arguments rather than adding logic inside main to differentiate between a list and a preparsed arument object --- bin/mycroft-skill-testrunner | 5 ++- start-mycroft.sh | 5 +++ .../voight_kampff/__main__.py | 37 +++++++++++++++++++ .../voight_kampff/test_setup.py | 5 +-- 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 test/integrationtests/voight_kampff/__main__.py diff --git a/bin/mycroft-skill-testrunner b/bin/mycroft-skill-testrunner index 9699a4d138f2..53ebda644a13 100755 --- a/bin/mycroft-skill-testrunner +++ b/bin/mycroft-skill-testrunner @@ -23,6 +23,9 @@ source "$DIR/../venv-activate.sh" -q # Invoke the individual skill tester if [ "$#" -eq 0 ] ; then python -m test.integrationtests.skills.runner . +elif [ "$1" = "vktest" ] ; then + shift + python -m test.integrationtests.voight_kampff "$@" else python -m test.integrationtests.skills.runner $@ -fi \ No newline at end of file +fi diff --git a/start-mycroft.sh b/start-mycroft.sh index 1397756768d6..3bc4f7bf3a18 100755 --- a/start-mycroft.sh +++ b/start-mycroft.sh @@ -40,6 +40,7 @@ function help() { echo " cli the Command Line Interface" echo " unittest run mycroft-core unit tests (requires pytest)" echo " skillstest run the skill autotests for all skills (requires pytest)" + echo " vktest run the Voight Kampff integration test suite" echo echo "Util COMMANDs:" echo " audiotest attempt simple audio validation" @@ -236,6 +237,10 @@ case ${_opt} in source-venv pytest test/integrationtests/skills/discover_tests.py "$@" ;; + "vktest") + source-venv + python -m test.integrationtests.voight_kampff "$@" + ;; "audiotest") launch-process ${_opt} ;; diff --git a/test/integrationtests/voight_kampff/__main__.py b/test/integrationtests/voight_kampff/__main__.py new file mode 100644 index 000000000000..f10f7399f381 --- /dev/null +++ b/test/integrationtests/voight_kampff/__main__.py @@ -0,0 +1,37 @@ +# Copyright 2020 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 os +import subprocess +import sys +from .test_setup import main as test_setup +from .test_setup import create_argument_parser +"""Voigt Kampff Test Module + +A single interface for the Voice Kampff integration test module. + +Full documentation can be found at https://mycroft.ai/docs +""" + + +def main(cmdline_args): + parser = create_argument_parser() + setup_args, behave_args = parser.parse_known_args(cmdline_args) + test_setup(setup_args) + os.chdir(os.path.dirname(__file__)) + subprocess.call(['./run_test_suite.sh', *behave_args]) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/test/integrationtests/voight_kampff/test_setup.py b/test/integrationtests/voight_kampff/test_setup.py index 27bc727b64ea..a3754169c517 100644 --- a/test/integrationtests/voight_kampff/test_setup.py +++ b/test/integrationtests/voight_kampff/test_setup.py @@ -199,13 +199,12 @@ def create_skills_manager(platform, skills_dir, url, branch): return MycroftSkillsManager(platform, skills_dir, repo) -def main(cmdline_args): +def main(args): """Parse arguments and run test environment setup. This installs and/or upgrades any skills needed for the tests and collects the feature and step files for the skills. """ - args = get_arguments(cmdline_args) if args.config: apply_config(args.config, args) @@ -223,4 +222,4 @@ def main(cmdline_args): if __name__ == '__main__': - main(sys.argv[1:]) + main(get_arguments(sys.argv[1:])) From 15ad6c724924a465c62c6d9f6a3add990941fbfe Mon Sep 17 00:00:00 2001 From: Kris Gesling Date: Thu, 26 Mar 2020 13:39:00 +0930 Subject: [PATCH 65/96] Add configurable API url to Watson TTS The new IBM Watson TTS service now provides a unique url for your TTS API resource. This adds a configuration option to override the default url. As the url provided by IBM contains the api_path, we check for it and if found remove it, as the RemoteTTS.__request method appends it when making the call. --- mycroft/tts/ibm_tts.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mycroft/tts/ibm_tts.py b/mycroft/tts/ibm_tts.py index 7263b5f962eb..6ea54dfabe69 100644 --- a/mycroft/tts/ibm_tts.py +++ b/mycroft/tts/ibm_tts.py @@ -16,6 +16,7 @@ from .tts import TTSValidator from .remote_tts import RemoteTTS from mycroft.configuration import Configuration +from mycroft.util import remove_last_slash from requests.auth import HTTPBasicAuth @@ -23,13 +24,16 @@ class WatsonTTS(RemoteTTS): PARAMS = {'accept': 'audio/wav'} def __init__(self, lang, config, - url="https://stream.watsonplatform.net/text-to-speech/api"): - super(WatsonTTS, self).__init__(lang, config, url, '/v1/synthesize', + url='https://stream.watsonplatform.net/text-to-speech/api', + api_path='/v1/synthesize'): + super(WatsonTTS, self).__init__(lang, config, url, api_path, WatsonTTSValidator(self)) self.type = "wav" user = self.config.get("user") or self.config.get("username") password = self.config.get("password") api_key = self.config.get("apikey") + self.url = remove_api_path(self.config.get("url", self.url), api_path) + if api_key is None: self.auth = HTTPBasicAuth(user, password) else: @@ -63,3 +67,11 @@ def validate_connection(self): def get_tts_class(self): return WatsonTTS + + +def remove_api_path(url, api_path): + """Remove api_path as RemoteTTS.__request method appends it""" + url = remove_last_slash(url) + if url.endswith(api_path): + url = url[:-len(api_path)] + return url From bcd45dd0fdbffb9bccf1be94325716dfefb84fb6 Mon Sep 17 00:00:00 2001 From: Kris Gesling Date: Thu, 26 Mar 2020 15:10:39 +0930 Subject: [PATCH 66/96] add learning-opt-out to request params fix pep-8 --- mycroft/tts/ibm_tts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mycroft/tts/ibm_tts.py b/mycroft/tts/ibm_tts.py index 6ea54dfabe69..330756a15759 100644 --- a/mycroft/tts/ibm_tts.py +++ b/mycroft/tts/ibm_tts.py @@ -43,6 +43,8 @@ def build_request_params(self, sentence): params = self.PARAMS.copy() params['LOCALE'] = self.lang params['voice'] = self.voice + params['X-Watson-Learning-Opt-Out'] = self.config.get( + 'X-Watson-Learning-Opt-Out', 'true') params['text'] = sentence.encode('utf-8') return params From 1b4977409125aafe044e708ba504ce3a98cd5623 Mon Sep 17 00:00:00 2001 From: Kris Gesling Date: Thu, 26 Mar 2020 20:27:39 +0930 Subject: [PATCH 67/96] Generalize custom remote TTS url config --- mycroft/tts/ibm_tts.py | 12 ++---------- mycroft/tts/remote_tts.py | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/mycroft/tts/ibm_tts.py b/mycroft/tts/ibm_tts.py index 330756a15759..d07e23ba973a 100644 --- a/mycroft/tts/ibm_tts.py +++ b/mycroft/tts/ibm_tts.py @@ -16,7 +16,6 @@ from .tts import TTSValidator from .remote_tts import RemoteTTS from mycroft.configuration import Configuration -from mycroft.util import remove_last_slash from requests.auth import HTTPBasicAuth @@ -32,7 +31,8 @@ def __init__(self, lang, config, user = self.config.get("user") or self.config.get("username") password = self.config.get("password") api_key = self.config.get("apikey") - self.url = remove_api_path(self.config.get("url", self.url), api_path) + if self.url.endswith(api_path): + self.url = self.url[:-len(api_path)] if api_key is None: self.auth = HTTPBasicAuth(user, password) @@ -69,11 +69,3 @@ def validate_connection(self): def get_tts_class(self): return WatsonTTS - - -def remove_api_path(url, api_path): - """Remove api_path as RemoteTTS.__request method appends it""" - url = remove_last_slash(url) - if url.endswith(api_path): - url = url[:-len(api_path)] - return url diff --git a/mycroft/tts/remote_tts.py b/mycroft/tts/remote_tts.py index 0c64520e82a7..8c70eba48a42 100644 --- a/mycroft/tts/remote_tts.py +++ b/mycroft/tts/remote_tts.py @@ -17,7 +17,7 @@ from requests_futures.sessions import FuturesSession from .tts import TTS -from mycroft.util import remove_last_slash, play_wav +from mycroft.util import play_wav from mycroft.util.log import LOG @@ -41,7 +41,7 @@ def __init__(self, lang, config, url, api_path, validator): super(RemoteTTS, self).__init__(lang, config, validator) self.api_path = api_path self.auth = None - self.url = remove_last_slash(url) + self.url = config.get('url', url).rstrip('/') self.session = FuturesSession() def execute(self, sentence, ident=None, listen=False): From c8be703e492d0602c0779ab78dc22f8d0361f159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 26 Mar 2020 18:43:19 +0100 Subject: [PATCH 68/96] Additional zero was prepended. The formatting of small numbers was fixed in lingua franca --- mycroft/util/format.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mycroft/util/format.py b/mycroft/util/format.py index 358be0eb138a..d8b1faa907a0 100644 --- a/mycroft/util/format.py +++ b/mycroft/util/format.py @@ -307,8 +307,6 @@ def _duration_handler(time1, lang=None, speech=True, *, time2=None, if len(out.split()) > 3 or seconds < 1: out += _translate_word("and", lang) + " " # speaking "zero point five seconds" is better than "point five" - if seconds < 1: - out += pronounce_number(0, lang) out += pronounce_number(seconds, lang) + " " out += _translate_word("second" if seconds == 1 else "seconds", lang) From efc9af558aa8f0eb3ae7dbe9235bdaa3a9f9cc93 Mon Sep 17 00:00:00 2001 From: jarbasal Date: Tue, 11 Feb 2020 18:24:13 +0000 Subject: [PATCH 69/96] Read mark1 eye color --- mycroft/client/enclosure/mark1/eyes.py | 11 +++++++++++ mycroft/enclosure/api.py | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/mycroft/client/enclosure/mark1/eyes.py b/mycroft/client/enclosure/mark1/eyes.py index 10d0eac16d39..b1956f7dee02 100644 --- a/mycroft/client/enclosure/mark1/eyes.py +++ b/mycroft/client/enclosure/mark1/eyes.py @@ -23,6 +23,9 @@ class EnclosureEyes: def __init__(self, bus, writer): self.bus = bus self.writer = writer + + self._num_pixels = 12 * 2 + self._current_rgb = [(255, 255, 255) for i in range(self._num_pixels)] self.__init_events() def __init_events(self): @@ -40,6 +43,12 @@ def __init_events(self): self.bus.on('enclosure.eyes.setpixel', self.set_pixel) self.bus.on('enclosure.eyes.fill', self.fill) + self.bus.on('enclosure.eyes.rgb.get', self.handle_get_color) + + def handle_get_color(self, message): + self.bus.emit(message.reply("enclosure.eyes.rgb", + {"pixels": self._current_rgb})) + def on(self, event=None): self.writer.write("eyes.on") @@ -67,6 +76,7 @@ def color(self, event=None): g = int(event.data.get("g", g)) b = int(event.data.get("b", b)) color = (r * 65536) + (g * 256) + b + self._current_rgb = [(r, g, b) for i in range(self._num_pixels)] self.writer.write("eyes.color=" + str(color)) def set_pixel(self, event=None): @@ -77,6 +87,7 @@ def set_pixel(self, event=None): r = int(event.data.get("r", r)) g = int(event.data.get("g", g)) b = int(event.data.get("b", b)) + self._current_rgb[idx] = (r, g, b) color = (r * 65536) + (g * 256) + b self.writer.write("eyes.set=" + str(idx) + "," + str(color)) diff --git a/mycroft/enclosure/api.py b/mycroft/enclosure/api.py index 1c40b0434484..0328e55fd1c7 100644 --- a/mycroft/enclosure/api.py +++ b/mycroft/enclosure/api.py @@ -324,3 +324,29 @@ def deactivate_mouth_events(self): """Disable movement of the mouth with speech""" self.bus.emit(Message('enclosure.mouth.events.deactivate', context={"destination": ["enclosure"]})) + + def get_eyes_color(self): + """ + Get the eye RGB color for all pixels + + :returns pixels (list) - list of (r,g,b) tuples for each eye pixel + + """ + message = Message("enclosure.eyes.rgb.get", + context={"source": "enclosure_api", + "destination": "enclosure"}) + response = self.bus.wait_for_response(message, "enclosure.eyes.rgb") + if response: + return response.data["pixels"] + raise TimeoutError("Enclosure took too long to respond") + + def get_eyes_pixel_color(self, idx): + """ + Get the RGB color for a specific eye pixel + + :returns (r,g,b) tuples for selected pixel + + """ + if idx < 0 or idx > 23: + raise ValueError('idx ({}) must be between 0-23'.format(str(idx))) + return self.get_eyes_color()[idx] From 9d712a898fcaaf95a2f9224b0eb723eb9ac5f789 Mon Sep 17 00:00:00 2001 From: jarbasal Date: Sun, 29 Mar 2020 21:36:03 +0100 Subject: [PATCH 70/96] Update docstrings --- mycroft/client/enclosure/mark1/eyes.py | 4 ++++ mycroft/enclosure/api.py | 16 ++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mycroft/client/enclosure/mark1/eyes.py b/mycroft/client/enclosure/mark1/eyes.py index b1956f7dee02..668b5416f38a 100644 --- a/mycroft/client/enclosure/mark1/eyes.py +++ b/mycroft/client/enclosure/mark1/eyes.py @@ -46,6 +46,10 @@ def __init_events(self): self.bus.on('enclosure.eyes.rgb.get', self.handle_get_color) def handle_get_color(self, message): + """Get the eye RGB color for all pixels + Returns: + (list) list of (r,g,b) tuples for each eye pixel + """ self.bus.emit(message.reply("enclosure.eyes.rgb", {"pixels": self._current_rgb})) diff --git a/mycroft/enclosure/api.py b/mycroft/enclosure/api.py index 0328e55fd1c7..9b20bb658bf4 100644 --- a/mycroft/enclosure/api.py +++ b/mycroft/enclosure/api.py @@ -326,11 +326,9 @@ def deactivate_mouth_events(self): context={"destination": ["enclosure"]})) def get_eyes_color(self): - """ - Get the eye RGB color for all pixels - - :returns pixels (list) - list of (r,g,b) tuples for each eye pixel - + """Get the eye RGB color for all pixels + Returns: + (list) pixels - list of (r,g,b) tuples for each eye pixel """ message = Message("enclosure.eyes.rgb.get", context={"source": "enclosure_api", @@ -341,11 +339,9 @@ def get_eyes_color(self): raise TimeoutError("Enclosure took too long to respond") def get_eyes_pixel_color(self, idx): - """ - Get the RGB color for a specific eye pixel - - :returns (r,g,b) tuples for selected pixel - + """Get the RGB color for a specific eye pixel + Returns: + (r,g,b) tuples for selected pixel """ if idx < 0 or idx > 23: raise ValueError('idx ({}) must be between 0-23'.format(str(idx))) From 5f71d32885cebde0c3cacc7689932993cd9603c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 30 Mar 2020 14:05:32 +0200 Subject: [PATCH 71/96] Add lock to avoid multiple concurrent builds --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index 62bad4686664..c47999578dca 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,6 +5,7 @@ pipeline { // building the Docker image. disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: '5')) + lock resource: 'VoightKampff' } stages { // Run the build in the against the dev branch to check for compile errors From 0a916de238ac8549a27c4eda4e2f603a5c1b9d81 Mon Sep 17 00:00:00 2001 From: andlo Date: Sun, 29 Mar 2020 15:07:36 +0100 Subject: [PATCH 72/96] fix danish typos and better translations --- mycroft/res/text/da-dk/and.word | 2 +- mycroft/res/text/da-dk/backend.down.dialog | 8 ++++---- mycroft/res/text/da-dk/cancel.voc | 7 ++++--- mycroft/res/text/da-dk/checking for updates.dialog | 4 ++-- mycroft/res/text/da-dk/day.word | 2 +- mycroft/res/text/da-dk/days.word | 2 +- mycroft/res/text/da-dk/hour.word | 2 +- mycroft/res/text/da-dk/hours.word | 2 +- mycroft/res/text/da-dk/i didn't catch that.dialog | 8 ++++---- mycroft/res/text/da-dk/last.voc | 3 +++ mycroft/res/text/da-dk/learning disabled.dialog | 2 +- mycroft/res/text/da-dk/learning enabled.dialog | 2 +- mycroft/res/text/da-dk/message_loading.skills.dialog | 1 + mycroft/res/text/da-dk/message_rebooting.dialog | 2 +- mycroft/res/text/da-dk/message_synching.clock.dialog | 2 +- mycroft/res/text/da-dk/message_updating.dialog | 2 +- mycroft/res/text/da-dk/minute.word | 2 +- mycroft/res/text/da-dk/minutes.word | 2 +- mycroft/res/text/da-dk/mycroft.intro.dialog | 2 +- mycroft/res/text/da-dk/no.voc | 8 ++++---- .../text/da-dk/not connected to the internet.dialog | 9 +++++---- mycroft/res/text/da-dk/not.loaded.dialog | 6 +----- mycroft/res/text/da-dk/or.word | 2 +- mycroft/res/text/da-dk/phonetic_spellings.txt | 4 ++++ .../res/text/da-dk/reset to factory defaults.dialog | 2 +- mycroft/res/text/da-dk/second.word | 2 +- mycroft/res/text/da-dk/seconds.word | 2 +- mycroft/res/text/da-dk/skill.error.dialog | 7 +------ mycroft/res/text/da-dk/skills updated.dialog | 4 ++-- .../sorry I couldn't install default skills.dialog | 2 +- mycroft/res/text/da-dk/ssh disabled.dialog | 2 +- mycroft/res/text/da-dk/ssh enabled.dialog | 2 +- mycroft/res/text/da-dk/time.changed.reboot.dialog | 2 +- mycroft/res/text/da-dk/yes.voc | 11 +++++------ 34 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 mycroft/res/text/da-dk/last.voc create mode 100644 mycroft/res/text/da-dk/message_loading.skills.dialog diff --git a/mycroft/res/text/da-dk/and.word b/mycroft/res/text/da-dk/and.word index ae9a4e643575..3db2cff883a1 100644 --- a/mycroft/res/text/da-dk/and.word +++ b/mycroft/res/text/da-dk/and.word @@ -1 +1 @@ -og \ No newline at end of file +og diff --git a/mycroft/res/text/da-dk/backend.down.dialog b/mycroft/res/text/da-dk/backend.down.dialog index 74122c86570c..00638b919ad9 100644 --- a/mycroft/res/text/da-dk/backend.down.dialog +++ b/mycroft/res/text/da-dk/backend.down.dialog @@ -1,4 +1,4 @@ -Jeg har problemer med at kommunikere med Mycroft serverne. Giv mig et par minutter, før du prver at tale med mig. -Jeg har problemer med at kommunikere med Mycroft serverne. Vent et par minutter, før du prver at tale med mig. -Det ser ud til, at jeg ikke kan oprette forbindelse til Mycroft-serverne. Giv mig et par minutter, før du prver at tale med mig. -Det ser ud til, at jeg ikke kan oprette forbindelse til Mycroft-serverne. Vent et par minutter, før du prver at tale med mig. +Jeg har problemer med at kommunikere med Mycroft-serverne. Giv mig et par minutter, før du prøver at tale med mig. +Jeg har problemer med at kommunikere med Mycroft-serverne. Vent venligst et par minutter, før du prøver at tale med mig. +Det ser ud til, at jeg ikke kan oprette forbindelse til Mycroft-serverne. Giv mig et par minutter, før du prøver at tale med mig. +Det ser ud til, at jeg ikke kan oprette forbindelse til Mycroft-serverne. Vent venligst et par minutter, før du prøver at tale med mig. diff --git a/mycroft/res/text/da-dk/cancel.voc b/mycroft/res/text/da-dk/cancel.voc index 4046aa003e63..8bbcf0b913d7 100644 --- a/mycroft/res/text/da-dk/cancel.voc +++ b/mycroft/res/text/da-dk/cancel.voc @@ -1,3 +1,4 @@ -afbryd det -ignorer det -glem det \ No newline at end of file +afbryd +Glem det +abort +afslut \ No newline at end of file diff --git a/mycroft/res/text/da-dk/checking for updates.dialog b/mycroft/res/text/da-dk/checking for updates.dialog index 42ef958d5d99..f82994596708 100644 --- a/mycroft/res/text/da-dk/checking for updates.dialog +++ b/mycroft/res/text/da-dk/checking for updates.dialog @@ -1,2 +1,2 @@ -Leder efter opdateringer -Et øjeblik, mens jeg opdaterer mig selv \ No newline at end of file +Kontrollerer for opdateringer +vent et øjeblik, mens jeg opdaterer mig selv diff --git a/mycroft/res/text/da-dk/day.word b/mycroft/res/text/da-dk/day.word index 73e686aad032..9e4015a98474 100644 --- a/mycroft/res/text/da-dk/day.word +++ b/mycroft/res/text/da-dk/day.word @@ -1 +1 @@ -dag \ No newline at end of file +dag diff --git a/mycroft/res/text/da-dk/days.word b/mycroft/res/text/da-dk/days.word index 1b5c2d69c5e1..9d526d03a5ee 100644 --- a/mycroft/res/text/da-dk/days.word +++ b/mycroft/res/text/da-dk/days.word @@ -1 +1 @@ -dage \ No newline at end of file +dage diff --git a/mycroft/res/text/da-dk/hour.word b/mycroft/res/text/da-dk/hour.word index 0082886fdac7..3f3e7c6703ff 100644 --- a/mycroft/res/text/da-dk/hour.word +++ b/mycroft/res/text/da-dk/hour.word @@ -1 +1 @@ -time \ No newline at end of file +time diff --git a/mycroft/res/text/da-dk/hours.word b/mycroft/res/text/da-dk/hours.word index 036d1ab325fd..a0bf4afd1f7e 100644 --- a/mycroft/res/text/da-dk/hours.word +++ b/mycroft/res/text/da-dk/hours.word @@ -1 +1 @@ -timer \ No newline at end of file +timer diff --git a/mycroft/res/text/da-dk/i didn't catch that.dialog b/mycroft/res/text/da-dk/i didn't catch that.dialog index 31feeef08fe8..8cc475e38df4 100644 --- a/mycroft/res/text/da-dk/i didn't catch that.dialog +++ b/mycroft/res/text/da-dk/i didn't catch that.dialog @@ -1,4 +1,4 @@ -Desværre, det forstod jeg ikke -Jeg er bange for, at jeg ikke kunne forstå det -Kan du sige det igen? -Kan du gentage det? \ No newline at end of file +Undskyld, Det fangede jeg ikke +Jeg er bange for, at jeg ikke kunne forstå det +Kan du sige det igen? +Kan du venligst gentage det? diff --git a/mycroft/res/text/da-dk/last.voc b/mycroft/res/text/da-dk/last.voc new file mode 100644 index 000000000000..bcd713e75292 --- /dev/null +++ b/mycroft/res/text/da-dk/last.voc @@ -0,0 +1,3 @@ +sidste valg +sidste mulighed +sidste diff --git a/mycroft/res/text/da-dk/learning disabled.dialog b/mycroft/res/text/da-dk/learning disabled.dialog index 5bddc74cc43b..2c53e154a6f0 100644 --- a/mycroft/res/text/da-dk/learning disabled.dialog +++ b/mycroft/res/text/da-dk/learning disabled.dialog @@ -1 +1 @@ -Interaktionsdata vil ikke længere blive sendt til Mycroft AI. \ No newline at end of file +Interaktionsdata vil ikke længere blive sendt til Mycroft AI. diff --git a/mycroft/res/text/da-dk/learning enabled.dialog b/mycroft/res/text/da-dk/learning enabled.dialog index ed63881f9c7e..eba2ab033451 100644 --- a/mycroft/res/text/da-dk/learning enabled.dialog +++ b/mycroft/res/text/da-dk/learning enabled.dialog @@ -1 +1 @@ -Jeg vil nu uploade interaktionsdata til Mycroft AI, så jeg kan blive klogere. I øjeblikket omfatter dette optagelser af wake-up ord. \ No newline at end of file +Jeg vil nu uploade interaktionsdata til Mycroft AI for at give mig mulighed for at blive smartere.  I øjeblikket inkluderer dette optagelser af wake-word-aktiveringer. diff --git a/mycroft/res/text/da-dk/message_loading.skills.dialog b/mycroft/res/text/da-dk/message_loading.skills.dialog new file mode 100644 index 000000000000..8827db56fdf4 --- /dev/null +++ b/mycroft/res/text/da-dk/message_loading.skills.dialog @@ -0,0 +1 @@ +< < < INDLÆSER < < <> diff --git a/mycroft/res/text/da-dk/message_rebooting.dialog b/mycroft/res/text/da-dk/message_rebooting.dialog index fe2a1660cba3..f21d8c4d7cb0 100644 --- a/mycroft/res/text/da-dk/message_rebooting.dialog +++ b/mycroft/res/text/da-dk/message_rebooting.dialog @@ -1 +1 @@ -STARTAR IGEN... +GENSTARTER... diff --git a/mycroft/res/text/da-dk/message_synching.clock.dialog b/mycroft/res/text/da-dk/message_synching.clock.dialog index da7303a6fd97..81d09f90deb9 100644 --- a/mycroft/res/text/da-dk/message_synching.clock.dialog +++ b/mycroft/res/text/da-dk/message_synching.clock.dialog @@ -1 +1 @@ -< < < SYNKRONISERE < < < +< < < SYNC < < < \ No newline at end of file diff --git a/mycroft/res/text/da-dk/message_updating.dialog b/mycroft/res/text/da-dk/message_updating.dialog index 8b67540bcada..22ff0d847d99 100644 --- a/mycroft/res/text/da-dk/message_updating.dialog +++ b/mycroft/res/text/da-dk/message_updating.dialog @@ -1 +1 @@ -< < < OPDATERER < < < +< < < OPDATERER < < < diff --git a/mycroft/res/text/da-dk/minute.word b/mycroft/res/text/da-dk/minute.word index 4b98366b131c..155a2f0b856d 100644 --- a/mycroft/res/text/da-dk/minute.word +++ b/mycroft/res/text/da-dk/minute.word @@ -1 +1 @@ -minut \ No newline at end of file +minut diff --git a/mycroft/res/text/da-dk/minutes.word b/mycroft/res/text/da-dk/minutes.word index caf1b024a4e6..476acc7b3912 100644 --- a/mycroft/res/text/da-dk/minutes.word +++ b/mycroft/res/text/da-dk/minutes.word @@ -1 +1 @@ -minuter \ No newline at end of file +minutter diff --git a/mycroft/res/text/da-dk/mycroft.intro.dialog b/mycroft/res/text/da-dk/mycroft.intro.dialog index 70d255642dbc..beb065ceef6d 100644 --- a/mycroft/res/text/da-dk/mycroft.intro.dialog +++ b/mycroft/res/text/da-dk/mycroft.intro.dialog @@ -1 +1 @@ -Hej Jeg er Mycroft, din nye assistent. For at hjælpe dig skal jeg være forbundet til internettet. Du kan enten forbinde mig med et netværkskabel eller bruge wifi. Følg disse instruktioner for at konfigurere Wi-Fi: \ No newline at end of file +Hej jeg er Mycroft, din nye assistent. For at hjælpe dig skal jeg være tilsluttet internettet. Du kan enten tilslutte mig et netværkskabel, eller brug wifi. Følg disse instruktioner for at konfigurere wifi: diff --git a/mycroft/res/text/da-dk/no.voc b/mycroft/res/text/da-dk/no.voc index 30b4969de720..ec9532ea922d 100644 --- a/mycroft/res/text/da-dk/no.voc +++ b/mycroft/res/text/da-dk/no.voc @@ -1,5 +1,5 @@ -no nope -nah -negative -nej +nix +nah +negativ +nej \ No newline at end of file diff --git a/mycroft/res/text/da-dk/not connected to the internet.dialog b/mycroft/res/text/da-dk/not connected to the internet.dialog index d7b137657c91..54a31dfac742 100644 --- a/mycroft/res/text/da-dk/not connected to the internet.dialog +++ b/mycroft/res/text/da-dk/not connected to the internet.dialog @@ -1,4 +1,5 @@ -Det ser ud til, at jeg ikke har forbindelse til internettet -Jeg synes ikke at være forbundet til internettet -Jeg kan ikke nå internettet lige nu -Jeg kan ikke nå internettet \ No newline at end of file +Det ser ud til, at jeg ikke har forbindelse til Internettet, Kontroller din netværksforbindelse. +Jeg ser ikke ud til at være tilsluttet internettet, Kontroller din netværksforbindelse. +Jeg kan ikke forbinde til internettet lige nu, Kontroller din netværksforbindelse. +Jeg kan ikke forbinde til internettet, Kontroller din netværksforbindelse. +Jeg har problemer med at forbinde til internettet lige nu, Kontroller din netværksforbindelse. diff --git a/mycroft/res/text/da-dk/not.loaded.dialog b/mycroft/res/text/da-dk/not.loaded.dialog index 421bd4301189..e82e8c78ae1d 100644 --- a/mycroft/res/text/da-dk/not.loaded.dialog +++ b/mycroft/res/text/da-dk/not.loaded.dialog @@ -1,5 +1 @@ -Jeg har problemer med at kommunikere med Mycroft serverne. Giv mig et par minutter, før du prver at tale til mig. -Jeg har problemer med at kommunikere med Mycroft serverne. Vent et par minutter, før du prver at tale til mig. -Det ser ud til, at jeg ikke kan oprette forbindelse til Mycroft-serverne. Giv mig et par minutter, før du prøver at tale til mig. -Det ser ud til, at jeg ikke kan oprette forbindelse til Mycroft-serverne. Vent et par minutter, før du prøver at tale til mig. -Vent et øjeblik, til jeg er færdig med at starte op. +Vent et øjeblik, til jeg er færdig med at starte op. diff --git a/mycroft/res/text/da-dk/or.word b/mycroft/res/text/da-dk/or.word index d99648a07ac1..3a97698c63bd 100644 --- a/mycroft/res/text/da-dk/or.word +++ b/mycroft/res/text/da-dk/or.word @@ -1 +1 @@ -eller \ No newline at end of file +eller diff --git a/mycroft/res/text/da-dk/phonetic_spellings.txt b/mycroft/res/text/da-dk/phonetic_spellings.txt index 7d0ecbe6f02a..06eb41a79b88 100644 --- a/mycroft/res/text/da-dk/phonetic_spellings.txt +++ b/mycroft/res/text/da-dk/phonetic_spellings.txt @@ -7,3 +7,7 @@ seksten: sejsten spotify: spåtifej spot-ify: spåtifej chat: tjat +wifi: vejfej +uploade: oplåte +wake-word-aktiveringer: wæik word aktiveringer +SSH-login: SSH-log-in \ No newline at end of file diff --git a/mycroft/res/text/da-dk/reset to factory defaults.dialog b/mycroft/res/text/da-dk/reset to factory defaults.dialog index fc1c30b7a1f8..ec343723a861 100644 --- a/mycroft/res/text/da-dk/reset to factory defaults.dialog +++ b/mycroft/res/text/da-dk/reset to factory defaults.dialog @@ -1 +1 @@ -Jeg er blevet nulstillet til fabriksindstillingerne. \ No newline at end of file +Jeg er nulstillet til fabriksindstillinger. diff --git a/mycroft/res/text/da-dk/second.word b/mycroft/res/text/da-dk/second.word index 300f8e50c43a..1c4aa7d3d59f 100644 --- a/mycroft/res/text/da-dk/second.word +++ b/mycroft/res/text/da-dk/second.word @@ -1 +1 @@ -sekund \ No newline at end of file +anden diff --git a/mycroft/res/text/da-dk/seconds.word b/mycroft/res/text/da-dk/seconds.word index aa5fc1206453..d27a05a918ba 100644 --- a/mycroft/res/text/da-dk/seconds.word +++ b/mycroft/res/text/da-dk/seconds.word @@ -1 +1 @@ -sekunder \ No newline at end of file +sekunder diff --git a/mycroft/res/text/da-dk/skill.error.dialog b/mycroft/res/text/da-dk/skill.error.dialog index 34e98a9e8f5e..8ea1edecd312 100644 --- a/mycroft/res/text/da-dk/skill.error.dialog +++ b/mycroft/res/text/da-dk/skill.error.dialog @@ -1,6 +1 @@ -Jeg har problemer med at kommunikere med Mycroft serverne. Giv mig et par minutter, før du prver at tale med mig. -Jeg har problemer med at kommunikere med Mycroft serverne. Vent et par minutter, før du prver at tale med mig. -Det ser ud til, at jeg ikke kan oprette forbindelse til Mycroft-serverne. Giv mig et par minutter, før du prver at tale med mig. -Det ser ud til, at jeg ikke kan oprette forbindelse til Mycroft-serverne. Vent et par minutter, før du prøver at tale med mig. -Vent et øjeblik, til jeg er førdig med at starte op. -Der opstod en fejl under behandling af en anmodning i {{skill}} +Der opstod en fejl under behandling af en anmodning i {{skill}} diff --git a/mycroft/res/text/da-dk/skills updated.dialog b/mycroft/res/text/da-dk/skills updated.dialog index ec4f312653a7..cb74fe479876 100644 --- a/mycroft/res/text/da-dk/skills updated.dialog +++ b/mycroft/res/text/da-dk/skills updated.dialog @@ -1,2 +1,2 @@ -Jeg har nu opdateret mine færdigheder. Jeg kan derfor godt hjælpe dig nu -Mine færdigheder er nu opdateret. Jeg er klar til at hjælpe dig. \ No newline at end of file +Jeg er opdateret nu +Færdigheder opdateret.  Jeg er klar til at hjælpe dig. diff --git a/mycroft/res/text/da-dk/sorry I couldn't install default skills.dialog b/mycroft/res/text/da-dk/sorry I couldn't install default skills.dialog index 68f98e675590..d2a863365bc5 100644 --- a/mycroft/res/text/da-dk/sorry I couldn't install default skills.dialog +++ b/mycroft/res/text/da-dk/sorry I couldn't install default skills.dialog @@ -1 +1 @@ -Der opstod en fejl under opdatering af færdigheder \ No newline at end of file +der opstod en fejl under opdatering af færdigheder diff --git a/mycroft/res/text/da-dk/ssh disabled.dialog b/mycroft/res/text/da-dk/ssh disabled.dialog index a6c43c09a4bc..70547678d4f8 100644 --- a/mycroft/res/text/da-dk/ssh disabled.dialog +++ b/mycroft/res/text/da-dk/ssh disabled.dialog @@ -1 +1 @@ -SSH login er blevet deaktiveret \ No newline at end of file +SSH-login er deaktiveret diff --git a/mycroft/res/text/da-dk/ssh enabled.dialog b/mycroft/res/text/da-dk/ssh enabled.dialog index de6ef14aa48e..e19adcca99bc 100644 --- a/mycroft/res/text/da-dk/ssh enabled.dialog +++ b/mycroft/res/text/da-dk/ssh enabled.dialog @@ -1 +1 @@ -SSH logins er nu tilladt \ No newline at end of file +SSH-login er nu tilladt diff --git a/mycroft/res/text/da-dk/time.changed.reboot.dialog b/mycroft/res/text/da-dk/time.changed.reboot.dialog index 51f986bb82fa..603f5f749062 100644 --- a/mycroft/res/text/da-dk/time.changed.reboot.dialog +++ b/mycroft/res/text/da-dk/time.changed.reboot.dialog @@ -1 +1 @@ -Jeg skal genstarte efter synkronisering af mit ur med internettet, er snart tilbage. \ No newline at end of file +Jeg bliver nødt til at genstarte efter at have synkroniseret mit ur med internettet. Jeg er straks tilbage. diff --git a/mycroft/res/text/da-dk/yes.voc b/mycroft/res/text/da-dk/yes.voc index 13817b120f11..8ff628abf811 100644 --- a/mycroft/res/text/da-dk/yes.voc +++ b/mycroft/res/text/da-dk/yes.voc @@ -1,6 +1,5 @@ -yes -yeah -yep -ja -ja tak -tak \ No newline at end of file +Ja +yeah +jep +jo da +jepper \ No newline at end of file From bdb98c338f2cf58838b82a2f23ab82b2ba56c30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Tue, 31 Mar 2020 09:33:04 +0200 Subject: [PATCH 73/96] Add .theia to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8f636ba2b58d..3155be6df2a5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ doc/_build/ .installed .mypy_cache .vscode +.theia .venv/ # Created by unit tests From 0d51a0416695f0e67d3da09e01ace60eb13e6eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 2 Apr 2020 16:02:18 +0200 Subject: [PATCH 74/96] Fallback to internal version if no version file exists This will allow non-packaged versions of mycroft core to report version to the backend. --- mycroft/version/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycroft/version/__init__.py b/mycroft/version/__init__.py index c76f61f950e1..9d4a0b4d1ce0 100644 --- a/mycroft/version/__init__.py +++ b/mycroft/version/__init__.py @@ -45,7 +45,7 @@ def get(): return json.load(f) except Exception: LOG.error("Failed to load version from '%s'" % version_file) - return {"coreVersion": None, "enclosureVersion": None} + return {"coreVersion": CORE_VERSION_STR, "enclosureVersion": None} def check_version(version_string): From 2a50fb1578bcd2052447a9bdc48f2a821ec1194b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 2 Apr 2020 19:13:16 +0200 Subject: [PATCH 75/96] Warn about skills that take a long time to shutdown --- mycroft/skills/skill_manager.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/mycroft/skills/skill_manager.py b/mycroft/skills/skill_manager.py index 63ef27e75c12..780373c87760 100644 --- a/mycroft/skills/skill_manager.py +++ b/mycroft/skills/skill_manager.py @@ -16,7 +16,7 @@ import os from glob import glob from threading import Thread, Event, Lock -from time import sleep, time +from time import sleep, time, monotonic from mycroft.api import is_paired from mycroft.enclosure.api import EnclosureAPI @@ -77,6 +77,29 @@ def put(self, loader): self._queue.append(loader) +def _shutdown_skill(instance): + """Shutdown a skill. + + Call the default_shutdown method of the skill, will produce a warning if + the shutdown process takes longer than 1 second. + + Arguments: + instance (MycroftSkill): Skill instance to shutdown + """ + try: + ref_time = monotonic() + # Perform the shutdown + instance.default_shutdown() + + shutdown_time = monotonic() - ref_time + if shutdown_time > 1: + LOG.warning('{} shutdown took {} seconds'.format(instance.skill_id, + shutdown_time)) + except Exception: + LOG.exception('Failed to shut down skill: ' + '{}'.format(instance.skill_id)) + + class SkillManager(Thread): _msm = None @@ -382,12 +405,7 @@ def stop(self): # Do a clean shutdown of all skills for skill_loader in self.skill_loaders.values(): if skill_loader.instance is not None: - try: - skill_loader.instance.default_shutdown() - except Exception: - LOG.exception( - 'Failed to shut down skill: ' + skill_loader.skill_id - ) + _shutdown_skill(skill_loader.instance) def handle_converse_request(self, message): """Check if the targeted skill id can handle conversation From 6b27ceca1c6edf08ac607e5c0a98b2081a6ecbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 2 Apr 2020 19:13:51 +0200 Subject: [PATCH 76/96] Allow stopping settings upload mid-stride The upload queue will now check if it's stopped while consuming the queue, allowing quicker shutdown if triggered while in the middle of uploading settingsmeta. --- mycroft/skills/skill_manager.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mycroft/skills/skill_manager.py b/mycroft/skills/skill_manager.py index 780373c87760..1b128b950056 100644 --- a/mycroft/skills/skill_manager.py +++ b/mycroft/skills/skill_manager.py @@ -51,6 +51,10 @@ def start(self): self.send() self.started = True + def stop(self): + """Stop the queue, and hinder any further transmissions.""" + self.started = False + def send(self): """Loop through all stored loaders triggering settingsmeta upload.""" with self.lock: @@ -59,7 +63,10 @@ def send(self): if queue: LOG.info('New Settings meta to upload.') for loader in queue: - loader.instance.settings_meta.upload() + if self.started: + loader.instance.settings_meta.upload() + else: + break def __len__(self): return len(self._queue) @@ -401,6 +408,7 @@ def stop(self): """Tell the manager to shutdown.""" self._stop_event.set() self.settings_downloader.stop_downloading() + self.upload_queue.stop() # Do a clean shutdown of all skills for skill_loader in self.skill_loaders.values(): From a487d32cc17086fcf265f53383e9fb6993d047dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 2 Apr 2020 19:16:40 +0200 Subject: [PATCH 77/96] Shutdown bus after shutting down services for "all" "stop-mycroft.sh all" stopped the messagebus before the services were shutdown, causing disconnected errors for the processes and making some of the shutdown code take longer. --- stop-mycroft.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stop-mycroft.sh b/stop-mycroft.sh index 51582cdfacff..da74fbecd886 100755 --- a/stop-mycroft.sh +++ b/stop-mycroft.sh @@ -94,11 +94,11 @@ case ${OPT} in ;& "") echo "Stopping all mycroft-core services" - end-process messagebus.service end-process skills end-process audio end-process speech end-process enclosure + end-process messagebus.service ;; "bus") end-process messagebus.service From 9246190c34688869c245f2d49c5c8320370f6fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 2 Apr 2020 19:18:17 +0200 Subject: [PATCH 78/96] Reduce timeout for track info response The 5 second timeout made the playback control take quite some time to shutdown if the audio process isn't running. --- mycroft/skills/audioservice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycroft/skills/audioservice.py b/mycroft/skills/audioservice.py index 0ec301dc8a1c..e138dc4df62b 100644 --- a/mycroft/skills/audioservice.py +++ b/mycroft/skills/audioservice.py @@ -149,7 +149,7 @@ def track_info(self): info = self.bus.wait_for_response( Message('mycroft.audio.service.track_info'), reply_type='mycroft.audio.service.track_info_reply', - timeout=5) + timeout=1) return info.data if info else {} def available_backends(self): From b3ce33ab702e5a1b2ad1ed43c93238272a0dfe72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 2 Apr 2020 19:25:35 +0200 Subject: [PATCH 79/96] Update test to match change Also minor cleaning up of test code --- test/unittests/version/test_version.py | 35 ++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/test/unittests/version/test_version.py b/test/unittests/version/test_version.py index fde48e51457c..9068728e432c 100644 --- a/test/unittests/version/test_version.py +++ b/test/unittests/version/test_version.py @@ -13,10 +13,9 @@ # limitations under the License. # import unittest - from unittest.mock import mock_open, patch -import mycroft.version +from mycroft.version import check_version, CORE_VERSION_STR, VersionManager VERSION_INFO = """ @@ -34,32 +33,36 @@ def test_get_version(self): Assures that only lower versions return True """ - self.assertTrue(mycroft.version.check_version('0.0.1')) - self.assertTrue(mycroft.version.check_version('0.8.1')) - self.assertTrue(mycroft.version.check_version('0.8.20')) - self.assertFalse(mycroft.version.check_version('0.8.22')) - self.assertFalse(mycroft.version.check_version('0.9.12')) - self.assertFalse(mycroft.version.check_version('1.0.2')) + self.assertTrue(check_version('0.0.1')) + self.assertTrue(check_version('0.8.1')) + self.assertTrue(check_version('0.8.20')) + self.assertFalse(check_version('0.8.22')) + self.assertFalse(check_version('0.9.12')) + self.assertFalse(check_version('1.0.2')) @patch('mycroft.version.isfile') @patch('mycroft.version.exists') @patch('mycroft.version.open', mock_open(read_data=VERSION_INFO), create=True) - def test_version_manager(self, mock_exists, mock_isfile): - """ - Test mycroft.version.VersionManager.get() + def test_version_manager_get(self, mock_exists, mock_isfile): + """Test mycroft.version.VersionManager.get() - asserts that the method returns expected data + Asserts that the method returns data from version file """ mock_isfile.return_value = True mock_exists.return_value = True - version = mycroft.version.VersionManager.get() + version = VersionManager.get() self.assertEqual(version['coreVersion'], "1505203453") self.assertEqual(version['enclosureVersion'], "1.0.0") - # Check file not existing case + @patch('mycroft.version.exists') + def test_version_manager_get_no_file(self, mock_exists): + """Test mycroft.version.VersionManager.get() + + Asserts that the method returns current version if no file exists. + """ mock_exists.return_value = False - version = mycroft.version.VersionManager.get() - self.assertEqual(version['coreVersion'], None) + version = VersionManager.get() + self.assertEqual(version['coreVersion'], CORE_VERSION_STR) self.assertEqual(version['enclosureVersion'], None) From 4f65591c21b0e9f0e7600534fad73868cf57a039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 2 Apr 2020 11:49:18 +0200 Subject: [PATCH 80/96] Simplify the GUI websocket code Use a single websocket but don't send data from clients to other clients. --- mycroft/client/enclosure/base.py | 204 ++++++++++--------------------- 1 file changed, 63 insertions(+), 141 deletions(-) diff --git a/mycroft/client/enclosure/base.py b/mycroft/client/enclosure/base.py index 58000715399b..a12c71590bb5 100644 --- a/mycroft/client/enclosure/base.py +++ b/mycroft/client/enclosure/base.py @@ -22,9 +22,9 @@ from mycroft.util import create_daemon from mycroft.util.log import LOG -import tornado.web import json -from tornado import autoreload, ioloop +import tornado.web as web +from tornado import ioloop from tornado.websocket import WebSocketHandler from mycroft.messagebus.message import Message @@ -63,7 +63,6 @@ class Enclosure: def __init__(self): # Establish Enclosure's websocket connection to the messagebus self.bus = MessageBusClient() - # Load full config Configuration.set_config_update_handlers(self.bus) config = Configuration.get() @@ -72,6 +71,7 @@ def __init__(self): self.config = config.get("enclosure") self.global_config = config + self.gui = create_gui_service(self, config['gui_websocket']) # This datastore holds the data associated with the GUI provider. Data # is stored in Namespaces, so you can have: # self.datastore["namespace"]["name"] = value @@ -94,7 +94,6 @@ def __init__(self): self.explicit_move = True # Set to true to send reorder commands # Listen for new GUI clients to announce themselves on the main bus - self.GUIs = {} # GUIs, either local or remote self.active_namespaces = [] self.bus.on("mycroft.gui.connected", self.on_gui_client_connected) self.register_gui_handlers() @@ -116,13 +115,14 @@ def run(self): ###################################################################### # GUI client API - def send(self, *args, **kwargs): + def send(self, msg_dict): """ Send to all registered GUIs. """ - for gui in self.GUIs.values(): - if gui.socket: - gui.socket.send(*args, **kwargs) - else: - LOG.error('GUI connection {} has no socket!'.format(gui)) + LOG.info('SENDING...') + for connection in GUIWebsocketHandler.clients: + try: + connection.send(msg_dict) + except Exception as e: + LOG.exception(repr(e)) def on_gui_send_event(self, message): """ Send an event to the GUIs. """ @@ -398,37 +398,18 @@ def remove_pages(self, namespace, pages): # If the connection is lost, it must be renegotiated and restarted. def on_gui_client_connected(self, message): # GUI has announced presence + LOG.info('GUI HAS ANNOUNCED!') + port = self.global_config["gui_websocket"]["base_port"] LOG.debug("on_gui_client_connected") gui_id = message.data.get("gui_id") - # Spin up a new communication socket for this GUI - if gui_id in self.GUIs: - # TODO: Close it? - pass - try: - asyncio.get_event_loop() - except RuntimeError: - asyncio.set_event_loop(asyncio.new_event_loop()) - - self.GUIs[gui_id] = GUIConnection(gui_id, self.global_config, - self.callback_disconnect, self) LOG.debug("Heard announcement from gui_id: {}".format(gui_id)) # Announce connection, the GUI should connect on it soon self.bus.emit(Message("mycroft.gui.port", - {"port": self.GUIs[gui_id].port, + {"port": port, "gui_id": gui_id})) - def callback_disconnect(self, gui_id): - LOG.info("Disconnecting!") - # TODO: Whatever is needed to kill the websocket instance - LOG.info(self.GUIs.keys()) - LOG.info('deleting: {}'.format(gui_id)) - if gui_id in self.GUIs: - del self.GUIs[gui_id] - else: - LOG.warning('ID doesn\'t exist') - def register_gui_handlers(self): # TODO: Register handlers for standard (Mark 1) events # self.bus.on('enclosure.eyes.on', self.on) @@ -472,125 +453,73 @@ def register_gui_handlers(self): } -class GUIConnection: - """ A single GUIConnection exists per graphic interface. This object - maintains the socket used for communication and keeps the state of the - Mycroft data in sync with the GUIs data. - - Serves as a communication interface between Qt/QML frontend and Mycroft - Core. This is bidirectional, e.g. "show me this visual" to the frontend as - well as "the user just tapped this button" from the frontend. - - For the rough protocol, see: - https://cgit.kde.org/scratch/mart/mycroft-gui.git/tree/transportProtocol.txt?h=newapi # nopep8 - - TODO: Implement variable deletion - TODO: Implement 'models' support - TODO: Implement events - TODO: Implement data coming back from Qt to Mycroft - """ +def create_gui_service(enclosure, config): + import tornado.options + LOG.info('Starting message bus for GUI...') + # Disable all tornado logging so mycroft loglevel isn't overridden + tornado.options.parse_command_line(['--logging=None']) - _last_idx = 0 # this is incremented by 1 for each open GUIConnection - server_thread = None + routes = [(config['route'], GUIWebsocketHandler)] + application = web.Application(routes, debug=True) + application.enclosure = enclosure + application.listen(config['base_port'], config['host']) - def __init__(self, id, config, callback_disconnect, enclosure): - LOG.debug("Creating GUIConnection") - self.id = id - self.socket = None - self.callback_disconnect = callback_disconnect - self.enclosure = enclosure + create_daemon(ioloop.IOLoop.instance().start) + LOG.info('GUI Message bus started!') + return application - # Each connection will run its own Tornado server. If the - # connection drops, the server is killed. - websocket_config = config.get("gui_websocket") - host = websocket_config.get("host") - route = websocket_config.get("route") - base_port = websocket_config.get("base_port") - while True: - self.port = base_port + GUIConnection._last_idx - GUIConnection._last_idx += 1 +class GUIWebsocketHandler(WebSocketHandler): + """The socket pipeline between the GUI and Mycroft.""" + clients = [] - try: - self.webapp = tornado.web.Application( - [(route, GUIWebsocketHandler)], **gui_app_settings - ) - # Hacky way to associate socket with this object: - self.webapp.gui = self - self.webapp.listen(self.port, host) - except Exception as e: - LOG.debug('Error: {}'.format(repr(e))) - continue - break - # Can't run two IOLoop's in the same process - if not GUIConnection.server_thread: - GUIConnection.server_thread = create_daemon( - ioloop.IOLoop.instance().start) - LOG.debug('IOLoop started @ ' - 'ws://{}:{}{}'.format(host, self.port, route)) - - def on_connection_opened(self, socket_handler): - LOG.debug("on_connection_opened") - self.socket = socket_handler + def open(self): + GUIWebsocketHandler.clients.append(self) + LOG.info('New Connection opened!') self.synchronize() + def on_close(self): + LOG.info('Closing {}'.format(id(self))) + GUIWebsocketHandler.clients.remove(self) + def synchronize(self): - """ Upload namespaces, pages and data. """ + """ Upload namespaces, pages and data to the last connected. """ namespace_pos = 0 - for namespace, pages in self.enclosure.loaded: + enclosure = self.application.enclosure + + for namespace, pages in enclosure.loaded: + LOG.info('Sync {}'.format(namespace)) # Insert namespace - self.socket.send({"type": "mycroft.session.list.insert", - "namespace": "mycroft.system.active_skills", - "position": namespace_pos, - "data": [{"skill_id": namespace}] - }) + self.send({"type": "mycroft.session.list.insert", + "namespace": "mycroft.system.active_skills", + "position": namespace_pos, + "data": [{"skill_id": namespace}] + }) # Insert pages - self.socket.send({"type": "mycroft.gui.list.insert", - "namespace": namespace, - "position": 0, - "data": [{"url": p} for p in pages] - }) + self.send({"type": "mycroft.gui.list.insert", + "namespace": namespace, + "position": 0, + "data": [{"url": p} for p in pages] + }) # Insert data - data = self.enclosure.datastore.get(namespace, {}) + data = enclosure.datastore.get(namespace, {}) for key in data: - self.socket.send({"type": "mycroft.session.set", - "namespace": namespace, - "data": {key: data[key]} - }) - + self.send({"type": "mycroft.session.set", + "namespace": namespace, + "data": {key: data[key]} + }) namespace_pos += 1 - def on_connection_closed(self, socket): - # Self-destruct (can't reconnect on the same port) - LOG.debug("on_connection_closed") - if self.socket: - LOG.debug("Server stopped: {}".format(self.socket)) - # TODO: How to stop the webapp for this socket? - # self.socket.stop() - self.socket = None - self.callback_disconnect(self.id) - - -class GUIWebsocketHandler(WebSocketHandler): - """ - The socket pipeline between Qt and Mycroft - """ - - def open(self): - self.application.gui.on_connection_opened(self) - def on_message(self, message): - LOG.debug("Received: {}".format(message)) + LOG.info("Received: {}".format(message)) msg = json.loads(message) if (msg.get('type') == "mycroft.events.triggered" and (msg.get('event_name') == 'page_gained_focus' or msg.get('event_name') == 'system.gui.user.interaction')): # System event, a page was changed msg_type = 'gui.page_interaction' - msg_data = { - 'namespace': msg['namespace'], - 'page_number': msg['parameters'].get('number') - } + msg_data = {'namespace': msg['namespace'], + 'page_number': msg['parameters'].get('number')} elif msg.get('type') == "mycroft.events.triggered": # A normal event was triggered msg_type = '{}.{}'.format(msg['namespace'], msg['event_name']) @@ -602,10 +531,12 @@ def on_message(self, message): msg_data = msg['data'] message = Message(msg_type, msg_data) - self.application.gui.enclosure.bus.emit(message) + LOG.info('Forwarding to bus...') + self.application.enclosure.bus.emit(message) + LOG.info('Done!') def write_message(self, *arg, **kwarg): - """ Wraps WebSocketHandler.write_message() with a lock. """ + """Wraps WebSocketHandler.write_message() with a lock. """ try: asyncio.get_event_loop() except RuntimeError: @@ -614,13 +545,6 @@ def write_message(self, *arg, **kwarg): with write_lock: super().write_message(*arg, **kwarg) - def send_message(self, message): - if isinstance(message, Message): - self.write_message(message.serialize()) - else: - LOG.info('message: {}'.format(message)) - self.write_message(str(message)) - def send(self, data): """Send the given data across the socket as JSON @@ -628,7 +552,5 @@ def send(self, data): data (dict): Data to transmit """ s = json.dumps(data) + LOG.info('Sending {}'.format(s)) self.write_message(s) - - def on_close(self): - self.application.gui.on_connection_closed(self) From b7cf8e755e63e7f0d45625530be51ccb2143b6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Sat, 4 Apr 2020 09:15:47 +0200 Subject: [PATCH 81/96] Remove origin check from gui websocket This allows simple websocket clients to connect as well. --- mycroft/client/enclosure/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mycroft/client/enclosure/base.py b/mycroft/client/enclosure/base.py index a12c71590bb5..f97138f7e898 100644 --- a/mycroft/client/enclosure/base.py +++ b/mycroft/client/enclosure/base.py @@ -554,3 +554,7 @@ def send(self, data): s = json.dumps(data) LOG.info('Sending {}'.format(s)) self.write_message(s) + + def check_origin(self, origin): + """Disable origin check to make js connections work.""" + return True From 1aea90b40d2b0e0fe1ac6adc0184228f0918e1ea Mon Sep 17 00:00:00 2001 From: "Brian J. Tarricone" Date: Mon, 6 Apr 2020 21:22:23 -0700 Subject: [PATCH 82/96] Fix standalone docs in README The README contains a brief note about running Mycroft standalone (without home.mycroft.ai), but the docs were very incomplete and somewhat outdated. --- README.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index aeddef71434c..1665b0a68dd5 100644 --- a/README.md +++ b/README.md @@ -84,14 +84,32 @@ When the configuration loader starts, it looks in these locations in this order, ## Using Mycroft Without Home -If you do not wish to use the Mycroft Home service, you may insert your own API keys into the configuration files listed below in configuration. +If you do not wish to use the Mycroft Home service, before starting Mycroft for the first time, create `$HOME/.mycroft/mycroft.conf` with the following contents: -The place to insert the API key looks like the following: +``` +{ + "skills": { + "blacklisted_skills": [ + "mycroft-configuration.mycroftai", + "mycroft-pairing.mycroftai" + ] + } +} +``` + +Mycroft will then be unable to perform speech-to-text conversion, so you'll need to set that up as well, using one of the [STT engines Mycroft supports](https://mycroft-ai.gitbook.io/docs/using-mycroft-ai/customizations/stt-engine). -`[WeatherSkill]` -`api_key = ""` +You may insert your own API keys into the configuration files listed above in Configuration. For example, to insert the API key for the Weather skill, create a new JSON key in the configuration file like so: -Put a relevant key inside the quotes and mycroft-core should begin to use the key immediately. +``` +{ + // other configuration settings... + // + "WeatherSkill": { + "api_key": "" + } +} +``` ## API Key Services From 000c3533168ba7887fdae42bff056a8bfd358690 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Fri, 3 Apr 2020 15:59:50 -0500 Subject: [PATCH 83/96] minor refactor --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index c47999578dca..b4c1ad8f28b1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -23,7 +23,7 @@ pipeline { // play nice with this naming convention. Define an alias for the // branch name that can be used in these scenarios. BRANCH_ALIAS = sh( - script: 'echo $BRANCH_NAME | sed -e "s#/#_#g"', + script: 'echo $BRANCH_NAME | sed -e "s#/#-#g"', returnStdout: true ).trim() } From b848a1c7412ea1f534bd1c3b141d80187042fc79 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Fri, 3 Apr 2020 16:01:53 -0500 Subject: [PATCH 84/96] move allure reports from tests running in mycroft-core repo to a subdirectory to differentiate from tests run on the skill repository. --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index b4c1ad8f28b1..31dd4c9f4228 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -73,8 +73,8 @@ pipeline { label: 'Publish Report to Web Server', script: '''scp allure-report.zip root@157.245.127.234:~; ssh root@157.245.127.234 "unzip -o ~/allure-report.zip"; - ssh root@157.245.127.234 "rm -rf /var/www/voight-kampff/${BRANCH_ALIAS}"; - ssh root@157.245.127.234 "mv allure-report /var/www/voight-kampff/${BRANCH_ALIAS}" + ssh root@157.245.127.234 "rm -rf /var/www/voight-kampff/core/${BRANCH_ALIAS}"; + ssh root@157.245.127.234 "mv allure-report /var/www/voight-kampff/core/${BRANCH_ALIAS}" ''' ) echo 'Report Published' From 53f2195023bd0484882b20ccd0bdd5f90c61be85 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Fri, 3 Apr 2020 16:02:18 -0500 Subject: [PATCH 85/96] Add email on failure or success. --- Jenkinsfile | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 31dd4c9f4228..6f09ec7d037b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -79,6 +79,79 @@ pipeline { ) echo 'Report Published' } + failure { + // Send failure email containing a link to the Jenkins build + // the results report and the console log messages to Mycroft + // developers, the developers of the pull request and the + // developers that caused the build to fail. + echo 'Sending Failure Email' + emailext ( + attachLog: true, + subject: "FAILED - Core Integration Tests - Build ${BRANCH_NAME} #${BUILD_NUMBER}", + body: """ +

+ One or more integration tests failed. Use the + resources below to identify the issue and fix + the failing tests. +

+
+

+ + Jenkins Build Details + +  (Requires account on Mycroft's Jenkins instance) +

+
+

+ + Report of Test Results + +

+
+

Console log is attached.

""", + replyTo: 'devops@mycroft.ai', + to: 'dev@mycroft.ai', + recipientProviders: [ + [$class: 'RequesterRecipientProvider'], + [$class:'CulpritsRecipientProvider'], + [$class:'DevelopersRecipientProvider'] + ] + ) + } + success { + // Send success email containing a link to the Jenkins build + // and the results report to Mycroft developers, the developers + // of the pull request and the developers that caused the + // last failed build. + echo 'Sending Success Email' + emailext ( + subject: "SUCCESS - Core Integration Tests - Build ${BRANCH_NAME} #${BUILD_NUMBER}", + body: """ +

+ All integration tests passed. No further action required. +

+
+

+ + Jenkins Build Details + +  (Requires account on Mycroft's Jenkins instance) +

+
+

+ + Report of Test Results + +

""", + replyTo: 'devops@mycroft.ai', + to: 'dev@mycroft.ai', + recipientProviders: [ + [$class: 'RequesterRecipientProvider'], + [$class:'CulpritsRecipientProvider'], + [$class:'DevelopersRecipientProvider'] + ] + ) + } } } // Build a voight_kampff image for major releases. This will be used From a24e1ea70e95ff50ef6ec77bf0fecdc68f60e417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 8 Apr 2020 08:41:57 +0200 Subject: [PATCH 86/96] Send report to pull request through PR comment --- Jenkinsfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 6f09ec7d037b..3a9ebb01aa96 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,6 +80,13 @@ pipeline { echo 'Report Published' } failure { + script { + // Create comment for Pull Requests + if (env.CHANGE_ID) { + echo 'Sending PR comment' + pullRequest.comment('Voight Kampff Integration Test Failed ([Results](https://reports.mycroft.ai/core/' + env.BRANCH_ALIAS + '))') + } + } // Send failure email containing a link to the Jenkins build // the results report and the console log messages to Mycroft // developers, the developers of the pull request and the @@ -119,6 +126,12 @@ pipeline { ) } success { + script { + if (env.CHANGE_ID) { + echo 'Sending PR comment' + pullRequest.comment('Voight Kampff Integration Test Succeeded ([Results](https://reports.mycroft.ai/core/' + env.BRANCH_ALIAS + '))') + } + } // Send success email containing a link to the Jenkins build // and the results report to Mycroft developers, the developers // of the pull request and the developers that caused the From 0718e397c5f9e0e3f1bba4b26ab8bf40eaf4ae0d Mon Sep 17 00:00:00 2001 From: luca-vercelli Date: Mon, 13 Apr 2020 16:24:50 +0200 Subject: [PATCH 87/96] Issues-2534 - language fallback fix update the lang value if currently set language doesn't exist. --- mycroft/tts/google_tts.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mycroft/tts/google_tts.py b/mycroft/tts/google_tts.py index 0bb3c0441ec6..8c0e421a9c15 100755 --- a/mycroft/tts/google_tts.py +++ b/mycroft/tts/google_tts.py @@ -13,6 +13,7 @@ # limitations under the License. # from gtts import gTTS +from gtts.lang import tts_langs from .tts import TTS, TTSValidator @@ -32,6 +33,13 @@ def get_tts(self, sentence, wav_file): Returns: Tuple ((str) written file, None) """ + langs = tts_langs() + if self.lang.lower() not in langs: + if self.lang[:2].lower() in langs: + self.lang = self.lang[:2] + else: + raise ValueError("Language not supported by gTTS: {}" + .format(self.lang)) tts = gTTS(text=sentence, lang=self.lang) tts.save(wav_file) return (wav_file, None) # No phonemes From a9cfa513a32d4e783b20e5d65cea22cdf87cc7f6 Mon Sep 17 00:00:00 2001 From: Luca Vercelli Date: Tue, 14 Apr 2020 16:20:57 +0200 Subject: [PATCH 88/96] Use lang validator to raise error --- mycroft/tts/google_tts.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mycroft/tts/google_tts.py b/mycroft/tts/google_tts.py index 8c0e421a9c15..7f8aef028fea 100755 --- a/mycroft/tts/google_tts.py +++ b/mycroft/tts/google_tts.py @@ -17,10 +17,15 @@ from .tts import TTS, TTSValidator +supported_langs = tts_langs() + class GoogleTTS(TTS): """Interface to google TTS.""" def __init__(self, lang, config): + if lang.lower() not in supported_langs and \ + lang[:2].lower() in supported_langs: + lang = lang[:2] super(GoogleTTS, self).__init__(lang, config, GoogleTTSValidator( self), 'mp3') @@ -33,13 +38,6 @@ def get_tts(self, sentence, wav_file): Returns: Tuple ((str) written file, None) """ - langs = tts_langs() - if self.lang.lower() not in langs: - if self.lang[:2].lower() in langs: - self.lang = self.lang[:2] - else: - raise ValueError("Language not supported by gTTS: {}" - .format(self.lang)) tts = gTTS(text=sentence, lang=self.lang) tts.save(wav_file) return (wav_file, None) # No phonemes @@ -50,8 +48,10 @@ def __init__(self, tts): super(GoogleTTSValidator, self).__init__(tts) def validate_lang(self): - # TODO - pass + lang = self.tts.lang + if lang.lower() not in supported_langs: + raise ValueError("Language not supported by gTTS: {}" + .format(lang)) def validate_connection(self): try: From 290d950424b5437ce8a97163593951a66200fdb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 16 Apr 2020 09:47:53 +0200 Subject: [PATCH 89/96] Fix shutdown of enclosure process Previously it would always be killed, now it exits smoothly. --- mycroft/client/enclosure/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mycroft/client/enclosure/__main__.py b/mycroft/client/enclosure/__main__.py index 7d75ee0a7877..116b0d295508 100644 --- a/mycroft/client/enclosure/__main__.py +++ b/mycroft/client/enclosure/__main__.py @@ -17,6 +17,8 @@ from mycroft.util.log import LOG from mycroft.messagebus.client import MessageBusClient from mycroft.configuration import Configuration, LocalConf, SYSTEM_CONFIG +from mycroft.util import (create_daemon, wait_for_exit_signal, + reset_sigint_handler) def main(): @@ -43,7 +45,9 @@ def main(): if enclosure: try: LOG.debug("Enclosure started!") - enclosure.run() + reset_sigint_handler() + create_daemon(enclosure.run) + wait_for_exit_signal() except Exception as e: print(e) finally: From 6d138726388bdce389c884dae7bf845cf2fb0b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 16 Apr 2020 10:58:07 +0200 Subject: [PATCH 90/96] Cleanup of the enclosure entrypoint - split out enclosure determination code - Add docstrings - Increase logging --- mycroft/client/enclosure/__main__.py | 45 ++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/mycroft/client/enclosure/__main__.py b/mycroft/client/enclosure/__main__.py index 116b0d295508..20cb41aaac21 100644 --- a/mycroft/client/enclosure/__main__.py +++ b/mycroft/client/enclosure/__main__.py @@ -12,36 +12,57 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import sys +"""Entrypoint for enclosure service. +This provides any "enclosure" specific functionality, for example GUI or +control over the Mark-1 Faceplate. +""" +from mycroft.configuration import LocalConf, SYSTEM_CONFIG from mycroft.util.log import LOG -from mycroft.messagebus.client import MessageBusClient -from mycroft.configuration import Configuration, LocalConf, SYSTEM_CONFIG from mycroft.util import (create_daemon, wait_for_exit_signal, reset_sigint_handler) -def main(): - # Read the system configuration - system_config = LocalConf(SYSTEM_CONFIG) - platform = system_config.get("enclosure", {}).get("platform") +def create_enclosure(platform): + """Create an enclosure based on the provided platform string. + Arguments: + platform (str): platform name string + + Returns: + Enclosure object + """ if platform == "mycroft_mark_1": - LOG.debug("Creating Mark I Enclosure") + LOG.info("Creating Mark I Enclosure") from mycroft.client.enclosure.mark1 import EnclosureMark1 enclosure = EnclosureMark1() elif platform == "mycroft_mark_2": - LOG.debug("Creating Mark II Enclosure") + LOG.info("Creating Mark II Enclosure") from mycroft.client.enclosure.mark2 import EnclosureMark2 enclosure = EnclosureMark2() else: - LOG.debug("Creating generic enclosure, platform='{}'".format(platform)) + LOG.info("Creating generic enclosure, platform='{}'".format(platform)) # TODO: Mechanism to load from elsewhere. E.g. read a script path from # the mycroft.conf, then load/launch that script. from mycroft.client.enclosure.generic import EnclosureGeneric enclosure = EnclosureGeneric() + return enclosure + + +def main(): + """Launch one of the available enclosure implementations. + + This depends on the configured platform and can currently either be + mycroft_mark_1 or mycroft_mark_2, if unconfigured a generic enclosure with + only the GUI bus will be started. + """ + # Read the system configuration + system_config = LocalConf(SYSTEM_CONFIG) + platform = system_config.get("enclosure", {}).get("platform") + + enclosure = create_enclosure(platform) if enclosure: try: LOG.debug("Enclosure started!") @@ -50,10 +71,8 @@ def main(): wait_for_exit_signal() except Exception as e: print(e) - finally: - sys.exit() else: - LOG.debug("No enclosure available for this hardware, running headless") + LOG.info("No enclosure available for this hardware, running headless") if __name__ == "__main__": From c5c0881d94f51cb8571cef11b7bfddd29a992ec1 Mon Sep 17 00:00:00 2001 From: Aditya Mehra Date: Tue, 14 Apr 2020 13:23:19 +0530 Subject: [PATCH 91/96] Add configuration option to set microphone timeout listener->recording_timeout: Maximum length of recording listener->recording_timeout: Maximum length of silence before stopping recording --- mycroft/client/speech/mic.py | 19 ++++++++++++++++--- mycroft/configuration/mycroft.conf | 6 +++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/mycroft/client/speech/mic.py b/mycroft/client/speech/mic.py index d72c21684102..0f45d4844a79 100644 --- a/mycroft/client/speech/mic.py +++ b/mycroft/client/speech/mic.py @@ -197,10 +197,12 @@ class ResponsiveRecognizer(speech_recognition.Recognizer): # before a phrase will be considered complete MIN_SILENCE_AT_END = 0.25 + # TODO: Remove in 20.08 # The maximum seconds a phrase can be recorded, # provided there is noise the entire time RECORDING_TIMEOUT = 10.0 + # TODO: Remove in 20.08 # The maximum time it will continue to record silence # when not enough noise has been detected RECORDING_TIMEOUT_WITH_SILENCE = 3.0 @@ -252,6 +254,17 @@ def __init__(self, wake_word_recognizer): self._account_id = None + # The maximum seconds a phrase can be recorded, + # provided there is noise the entire time + self.recording_timeout = listener_config.get('recording_timeout', + self.RECORDING_TIMEOUT) + + # The maximum time it will continue to record silence + # when not enough noise has been detected + self.recording_timeout_with_silence = listener_config.get( + 'recording_timeout_with_silence', + self.RECORDING_TIMEOUT_WITH_SILENCE) + @property def account_id(self): """Fetch account from backend when needed. @@ -288,7 +301,7 @@ def _record_phrase( Essentially, this code waits for a period of silence and then returns the audio. If silence isn't detected, it will terminate and return - a buffer of RECORDING_TIMEOUT duration. + a buffer of self.recording_timeout duration. Args: source (AudioSource): Source producing the audio chunks @@ -326,11 +339,11 @@ def decrease_noise(level): min_loud_chunks = int(self.MIN_LOUD_SEC_PER_PHRASE / sec_per_buffer) # Maximum number of chunks to record before timing out - max_chunks = int(self.RECORDING_TIMEOUT / sec_per_buffer) + max_chunks = int(self.recording_timeout / sec_per_buffer) num_chunks = 0 # Will return if exceeded this even if there's not enough loud chunks - max_chunks_of_silence = int(self.RECORDING_TIMEOUT_WITH_SILENCE / + max_chunks_of_silence = int(self.recording_timeout_with_silence / sec_per_buffer) # bytearray to store audio in diff --git a/mycroft/configuration/mycroft.conf b/mycroft/configuration/mycroft.conf index fb781f4eb093..b3e324eb5531 100644 --- a/mycroft/configuration/mycroft.conf +++ b/mycroft/configuration/mycroft.conf @@ -189,7 +189,11 @@ "multiplier": 1.0, "energy_ratio": 1.5, "wake_word": "hey mycroft", - "stand_up_word": "wake up" + "stand_up_word": "wake up", + + // Settings used by microphone to set recording timeout + "recording_timeout": 10.0, + "recording_timeout_with_silence": 3.0 }, // Settings used for any precise wake words From d51fb126b8026cc106a5b5090fdfb678c0864d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Fri, 17 Apr 2020 20:33:31 +0200 Subject: [PATCH 92/96] Fix fallback to mimic After catching exception to clear the isSpeaking flag the exception shall be re-raised to allow the fallback TTS to trigger. --- mycroft/tts/tts.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mycroft/tts/tts.py b/mycroft/tts/tts.py index ccbc32e13203..4cd036d763ec 100644 --- a/mycroft/tts/tts.py +++ b/mycroft/tts/tts.py @@ -40,6 +40,9 @@ _TTS_ENV['PULSE_PROP'] = 'media.role=phone' +EMPTY_PLAYBACK_QUEUE_TUPLE = (None, None, None, None, None) + + class PlaybackThread(Thread): """Thread class for playing back tts audio and sending viseme data to enclosure. @@ -317,8 +320,10 @@ def execute(self, sentence, ident=None, listen=False): try: self._execute(sentence, ident, listen) except Exception: - # If an error occurs end the audio sequence - self.queue.put((None, None, None, None, None)) + # If an error occurs end the audio sequence through an empty entry + self.queue.put(EMPTY_PLAYBACK_QUEUE_TUPLE) + # Re-raise to allow the Exception to be handled externally as well. + raise def _execute(self, sentence, ident, listen): if self.phonetic_spelling: From 7586de86d9c26116d8d0f4713b4b04d3af3889de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 20 Apr 2020 09:17:46 +0200 Subject: [PATCH 93/96] Fix MutableStream __init__ with mute=True If the stream is restarted while muted this would cause an Exception leading to an unresponsive voice client due to the read_lock being called before creation. --- mycroft/client/speech/mic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mycroft/client/speech/mic.py b/mycroft/client/speech/mic.py index 0f45d4844a79..33ee8bdc8d5d 100644 --- a/mycroft/client/speech/mic.py +++ b/mycroft/client/speech/mic.py @@ -50,14 +50,14 @@ def __init__(self, wrapped_stream, format, muted=False): assert wrapped_stream is not None self.wrapped_stream = wrapped_stream - self.muted = muted - if muted: - self.mute() - self.SAMPLE_WIDTH = pyaudio.get_sample_size(format) self.muted_buffer = b''.join([b'\x00' * self.SAMPLE_WIDTH]) self.read_lock = Lock() + self.muted = muted + if muted: + self.mute() + def mute(self): """Stop the stream and set the muted flag.""" with self.read_lock: From 202cc94262c68eae79a617ca4696b9af8ea83f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 20 Apr 2020 09:57:03 +0200 Subject: [PATCH 94/96] Fix uploading settings meta This changes the order so the started flag is set before the settingsmeta the send() method is called. The send() method now exits if the started flag isn't set causing the skill settings not to be uploaded --- mycroft/skills/skill_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycroft/skills/skill_manager.py b/mycroft/skills/skill_manager.py index 1b128b950056..cc166b003707 100644 --- a/mycroft/skills/skill_manager.py +++ b/mycroft/skills/skill_manager.py @@ -48,8 +48,8 @@ def __init__(self): def start(self): """Start processing of the queue.""" - self.send() self.started = True + self.send() def stop(self): """Stop the queue, and hinder any further transmissions.""" From d1b6db306fc50438d5d31d4a4a4253ab75edbcaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 20 Apr 2020 10:44:19 +0200 Subject: [PATCH 95/96] Add test case for sending items added before start --- test/unittests/skills/test_skill_manager.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unittests/skills/test_skill_manager.py b/test/unittests/skills/test_skill_manager.py index 7d84f001e0a8..f2bdde3fa466 100644 --- a/test/unittests/skills/test_skill_manager.py +++ b/test/unittests/skills/test_skill_manager.py @@ -41,6 +41,18 @@ def test_upload_queue_use(self): queue.send() self.assertEqual(len(queue), 0) + def test_upload_queue_preloaded(self): + queue = UploadQueue() + loaders = [Mock(), Mock(), Mock(), Mock()] + for i, l in enumerate(loaders): + queue.put(l) + self.assertEqual(len(queue), i + 1) + # Check that starting the queue will send all the items in the queue + queue.start() + self.assertEqual(len(queue), 0) + for l in loaders: + l.instance.settings_meta.upload.assert_called_once_with() + class TestSkillManager(MycroftUnitTestBase): mock_package = 'mycroft.skills.skill_manager.' From 4d45933c80d5f304b56048cf0d4a7c4985b3daa2 Mon Sep 17 00:00:00 2001 From: devs-mycroft Date: Thu, 23 Apr 2020 13:22:41 +0000 Subject: [PATCH 96/96] Version bump from 20.2.1 to 20.2.2 --- mycroft/version/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycroft/version/__init__.py b/mycroft/version/__init__.py index 9d4a0b4d1ce0..279f03fb56b5 100644 --- a/mycroft/version/__init__.py +++ b/mycroft/version/__init__.py @@ -25,7 +25,7 @@ # START_VERSION_BLOCK CORE_VERSION_MAJOR = 20 CORE_VERSION_MINOR = 2 -CORE_VERSION_BUILD = 1 +CORE_VERSION_BUILD = 2 # END_VERSION_BLOCK CORE_VERSION_TUPLE = (CORE_VERSION_MAJOR,