diff --git a/mycroft/skills/mycroft_skill/mycroft_skill.py b/mycroft/skills/mycroft_skill/mycroft_skill.py index 95e2f2f80e40..d205d25cbd22 100644 --- a/mycroft/skills/mycroft_skill/mycroft_skill.py +++ b/mycroft/skills/mycroft_skill/mycroft_skill.py @@ -600,7 +600,7 @@ def ask_selection(self, options, dialog='', else: num = extract_number(resp, self.lang, ordinals=True) resp = None - if num and num < len(options): + if num and num <= len(options): resp = options[num - 1] else: resp = match diff --git a/test/unittests/mocks.py b/test/unittests/mocks.py index 776acf455dad..1e8d9e594eec 100644 --- a/test/unittests/mocks.py +++ b/test/unittests/mocks.py @@ -23,6 +23,15 @@ __CONFIG = LocalConf(DEFAULT_CONFIG) +class AnyCallable: + """Class matching any callable. + + Useful for assert_called_with arguments. + """ + def __eq__(self, other): + return callable(other) + + def base_config(): """Base config used when mocking. diff --git a/test/unittests/skills/test_common_play_skill.py b/test/unittests/skills/test_common_play_skill.py index d7e92ff52106..352f0e1bb54f 100644 --- a/test/unittests/skills/test_common_play_skill.py +++ b/test/unittests/skills/test_common_play_skill.py @@ -3,15 +3,7 @@ from mycroft.messagebus import Message from mycroft.skills.common_play_skill import CommonPlaySkill, CPSMatchLevel from mycroft.skills.audioservice import AudioService - - -class AnyCallable: - """Class matching any callable. - - Useful for assert_called_with arguments. - """ - def __eq__(self, other): - return callable(other) +from test.unittests.mocks import AnyCallable class TestCommonPlay(TestCase): diff --git a/test/unittests/skills/test_common_query_skill.py b/test/unittests/skills/test_common_query_skill.py index 0900f859cad6..8c138ef33345 100644 --- a/test/unittests/skills/test_common_query_skill.py +++ b/test/unittests/skills/test_common_query_skill.py @@ -4,14 +4,7 @@ from mycroft.skills.common_query_skill import (CommonQuerySkill, CQSMatchLevel, CQSVisualMatchLevel, handles_visuals) - - -class AnyCallable: - """Class matching any callable. - Useful for assert_called_with arguments. - """ - def __eq__(self, other): - return callable(other) +from test.unittests.mocks import AnyCallable class TestCommonQuerySkill(TestCase): diff --git a/test/unittests/skills/test_mycroft_skill_get_response.py b/test/unittests/skills/test_mycroft_skill_get_response.py new file mode 100644 index 000000000000..c13bc50718ce --- /dev/null +++ b/test/unittests/skills/test_mycroft_skill_get_response.py @@ -0,0 +1,241 @@ +"""Tests for the mycroft skill's get_response variations.""" + +from os.path import dirname, join +from threading import Thread +import time +from unittest import TestCase, mock + +from mycroft import MycroftSkill +from mycroft.messagebus import Message + +from test.unittests.mocks import base_config, AnyCallable + + +def create_converse_responder(response, skill): + """Create a function to inject a response into the converse method. + + The function waits for the converse method to be replaced by the + _wait_response logic and afterwards injects the provided response. + + Arguments: + response (str): Sentence to inject. + skill (MycroftSkill): skill to monitor. + """ + default_converse = skill.converse + converse_return = None + + def wait_for_new_converse(): + """Wait until there is a new converse handler then send sentence. + """ + nonlocal converse_return + start_time = time.monotonic() + while time.monotonic() < start_time + 5: + if skill.converse != default_converse: + skill.converse([response]) + break + + time.sleep(0.1) + + return wait_for_new_converse + + +@mock.patch('mycroft.skills.mycroft_skill.mycroft_skill.Configuration') +def create_skill(mock_conf, lang='en-us'): + mock_conf.get.return_value = base_config() + skill = MycroftSkill(name='test_skill') + bus = mock.Mock() + skill.bind(bus) + skill.config_core['lang'] = lang + skill.load_data_files(join(dirname(__file__), 'test_skill')) + return skill + + +class TestMycroftSkillWaitResponse(TestCase): + def test_wait(self): + """Ensure that _wait_response() returns the response from converse.""" + skill = create_skill() + + expected_response = 'Yes I do, very much' + + converser = Thread(target=create_converse_responder(expected_response, + skill)) + converser.start() + validator = mock.Mock() + validator.return_value = True + is_cancel = mock.Mock() + is_cancel.return_value = False + on_fail = mock.Mock() + + response = skill._wait_response(is_cancel, validator, on_fail, 1) + self.assertEqual(response, expected_response) + converser.join() + + def test_wait_cancel(self): + """Test that a matching cancel function cancels the wait.""" + skill = create_skill() + + converser = Thread(target=create_converse_responder('cancel', skill)) + converser.start() + validator = mock.Mock() + validator.return_value = False + on_fail = mock.Mock() + + def is_cancel(utterance): + return utterance == 'cancel' + + response = skill._wait_response(is_cancel, validator, on_fail, 1) + self.assertEqual(response, None) + converser.join() + + +class TestMycroftSkillGetResponse(TestCase): + def test_get_response(self): + """Test response using a dialog file.""" + skill = create_skill() + skill._wait_response = mock.Mock() + skill.speak_dialog = mock.Mock() + + expected_response = 'ice creamr please' + skill._wait_response.return_value = expected_response + response = skill.get_response('what do you want') + self.assertEqual(response, expected_response) + self.assertTrue(skill.speak_dialog.called) + + def test_get_response_text(self): + """Assert that text is used if no dialog exists.""" + skill = create_skill() + skill._wait_response = mock.Mock() + skill.speak_dialog = mock.Mock() + + expected_response = 'green' + skill._wait_response.return_value = expected_response + response = skill.get_response('tell me a color') + self.assertEqual(response, expected_response) + self.assertTrue(skill.speak_dialog.called) + skill.speak_dialog.assert_called_with('tell me a color', + {}, + expect_response=True, + wait=True) + + def test_get_response_no_dialog(self): + """Check that when no dialog/text is provided listening is triggered. + """ + skill = create_skill() + skill._wait_response = mock.Mock() + skill.speak_dialog = mock.Mock() + + expected_response = 'ice creamr please' + skill._wait_response.return_value = expected_response + response = skill.get_response() + self.assertEqual(response, expected_response) + self.assertFalse(skill.speak_dialog.called) + self.assertTrue(skill.bus.emit.called) + sent_message = skill.bus.emit.call_args[0][0] + self.assertEqual(sent_message.msg_type, 'mycroft.mic.listen') + + def test_get_response_validator(self): + """Ensure validator is passed on.""" + skill = create_skill() + skill._wait_response = mock.Mock() + skill.speak_dialog = mock.Mock() + + def validator(*args, **kwargs): + return True + + expected_response = 'ice creamr please' + skill._wait_response.return_value = expected_response + response = skill.get_response('what do you want', + validator=validator) + skill._wait_response.assert_called_with(AnyCallable(), validator, + AnyCallable(), -1) + + +class TestMycroftSkillAskYesNo(TestCase): + def test_ask_yesno_no(self): + """Check that a negative response is interpreted as a no.""" + skill = create_skill() + skill.get_response = mock.Mock() + skill.get_response.return_value = 'nope' + + response = skill.ask_yesno('Do you like breakfast') + self.assertEqual(response, 'no') + + def test_ask_yesno_yes(self): + """Check that an affirmative response is interpreted as a yes.""" + skill = create_skill() + skill.get_response = mock.Mock() + skill.get_response.return_value = 'yes' + + response = skill.ask_yesno('Do you like breakfast') + self.assertEqual(response, 'yes') + + def test_ask_yesno_other(self): + """Check that non yes no response gets returned.""" + skill = create_skill() + skill.get_response = mock.Mock() + skill.get_response.return_value = 'I am a fish' + + response = skill.ask_yesno('Do you like breakfast') + self.assertEqual(response, 'I am a fish') + + def test_ask_yesno_german(self): + """Check that when the skill is set to german it responds to "ja".""" + skill = create_skill(lang='de-de') + skill.get_response = mock.Mock() + skill.get_response.return_value = 'ja' + + response = skill.ask_yesno('Do you like breakfast') + self.assertEqual(response, 'yes') + + +class TestMycroftAskSelection(TestCase): + def test_selection_number(self): + """Test selection by number.""" + skill = create_skill() + skill.speak = mock.Mock() + skill.get_response = mock.Mock() + + skill.get_response.return_value = 'the third' + + options = ['a balloon', 'an octopus', 'a piano'] + response = skill.ask_selection(options, 'which is better') + self.assertEqual(options[2], response) + + # Assert that the spoken sentence contains all options. + spoken_sentence = skill.speak.call_args[0][0] + for opt in options: + self.assertTrue(opt in spoken_sentence) + + def test_selection_last(self): + """Test selection by "last".""" + skill = create_skill() + skill.speak = mock.Mock() + skill.get_response = mock.Mock() + + skill.get_response.return_value = 'last one' + + options = ['a balloon', 'an octopus', 'a piano'] + response = skill.ask_selection(options, 'which is better') + self.assertEqual(options[2], response) + + # Assert that the spoken sentence contains all options. + spoken_sentence = skill.speak.call_args[0][0] + for opt in options: + self.assertTrue(opt in spoken_sentence) + + def test_selection_name(self): + """Test selection by name.""" + skill = create_skill() + skill.speak = mock.Mock() + skill.get_response = mock.Mock() + + skill.get_response.return_value = 'octopus' + + options = ['a balloon', 'an octopus', 'a piano'] + response = skill.ask_selection(options, 'which is better') + self.assertEqual(options[1], response) + + # Assert that the spoken sentence contains all options. + spoken_sentence = skill.speak.call_args[0][0] + for opt in options: + self.assertTrue(opt in spoken_sentence) diff --git a/test/unittests/skills/test_skill/dialog/en-us/what do you want.dialog b/test/unittests/skills/test_skill/dialog/en-us/what do you want.dialog new file mode 100644 index 000000000000..c44a2c2cc236 --- /dev/null +++ b/test/unittests/skills/test_skill/dialog/en-us/what do you want.dialog @@ -0,0 +1 @@ +What do you want