Beispiel #1
0
class TestMycroftSkillsManager(object):
    def setup(self):
        self.root = root = dirname(abspath(__file__))
        self.msm = MycroftSkillsManager(
            platform='default',
            skills_dir=join(root, 'test-skills'),
            repo=SkillRepo(
                join(root, 'repo-instance'),
                branch='test-repo',
                url='https://github.com/mycroftai/mycroft-skills-manager'),
            versioned=True)

    def teardown(self):
        if exists(self.msm.skills_dir):
            rmtree(self.msm.skills_dir)
        if exists(self.msm.repo.path):
            rmtree(self.msm.repo.path)

    def test_install(self):
        """Install by url or name"""
        self.msm.install('skill-a')
        with pytest.raises(AlreadyInstalled):
            self.msm.install('skill-a')
        self.msm.install('skill-b')

    def test_remove(self):
        """Remove by url or name"""
        with pytest.raises(AlreadyRemoved):
            self.msm.remove('skill-a')
        self.msm.install('skill-a')
        self.msm.remove('skill-a')

    def test_update(self):
        """Update all downloaded skills"""
        self.msm.install('skill-a')
        self.msm.update()

    def test_install_defaults(self):
        """Installs the default skills, updates all others"""
        assert not self.msm.find_skill('skill-a').is_local
        self.msm.install_defaults()
        assert self.msm.find_skill('skill-a').is_local
        assert not self.msm.find_skill('skill-b').is_local
        self.msm.platform = 'platform-1'
        self.msm.install_defaults()
        assert self.msm.find_skill('skill-b').is_local

    def test_list(self):
        all_skills = {'skill-a', 'skill-b', 'skill-cd', 'skill-ce'}
        assert {i.name for i in self.msm.list()} == all_skills

    def test_find_skill(self):
        with pytest.raises(MultipleSkillMatches):
            self.msm.find_skill('skill-c')
        with pytest.raises(SkillNotFound):
            self.msm.find_skill('jsafpcq')
Beispiel #2
0
class TestMycroftSkillsManager(TestCase):
    def setUp(self):
        temp_dir = tempfile.mkdtemp()
        self.temp_dir = Path(temp_dir)
        self.skills_json_path = self.temp_dir.joinpath('skills.json')
        self.skills_dir = self.temp_dir.joinpath('skills')
        self._build_fake_skills()
        self._mock_skills_json_path()
        self._mock_skill_entry()
        self._mock_skill_repo()
        copyfile('skills_test.json', str(self.skills_json_path))
        self.msm = MycroftSkillsManager(platform='default',
                                        skills_dir=str(
                                            self.temp_dir.joinpath('skills')),
                                        repo=self.skill_repo_mock,
                                        versioned=True)

    def _build_fake_skills(self):
        foo_skill_dir = self.skills_dir.joinpath('skill-foo')
        foo_skill_dir.mkdir(parents=True)
        foo_skill_dir.joinpath('__init__.py').touch()
        bar_skill_dir = self.skills_dir.joinpath('skill-bar')
        bar_skill_dir.mkdir(parents=True)
        bar_skill_dir.joinpath('__init__.py').touch()

    def _mock_log(self):
        log_patch = patch('msm.mycroft_skills_manager.LOG')
        self.addCleanup(log_patch.stop)
        self.log_mock = log_patch.start()

    def _mock_skills_json_path(self):
        expanduser_patch = patch('msm.skill_state.expanduser')
        self.addCleanup(expanduser_patch.stop)
        self.skills_json_path_mock = expanduser_patch.start()
        self.skills_json_path_mock.return_value = str(
            self.temp_dir.joinpath('skills.json'))

    def _mock_skill_entry(self):
        skill_entry_patch = patch(
            'msm.mycroft_skills_manager.SkillEntry.install', spec=True)
        self.addCleanup(skill_entry_patch.stop)
        self.skill_entry_mock = skill_entry_patch.start()

    def _mock_skill_repo(self):
        skill_repo_patch = patch('msm.mycroft_skills_manager.SkillRepo',
                                 spec=True)
        self.addCleanup(skill_repo_patch.stop)
        self.skill_repo_mock = skill_repo_patch.start()
        self.skill_repo_mock.skills_meta_info = {'https://skill_foo_url': None}

    def teardown(self):
        rmtree(str(self.temp_dir))

    def test_device_skill_state(self):
        """Contents of skills.json are loaded into memory"""
        state = self.msm.device_skill_state
        initial_state = [
            dict(name='skill-foo',
                 origin='default',
                 beta=False,
                 status='active',
                 installed=12345,
                 updated=0,
                 installation='installed',
                 skill_gid='@|skill-foo'),
            dict(name='skill-bar',
                 origin='default',
                 beta=False,
                 status='active',
                 installed=23456,
                 updated=0,
                 installation='installed',
                 skill_gid='@|skill-bar')
        ]

        self.assertListEqual(initial_state, state['skills'])
        self.assertListEqual([], state['blacklist'])
        self.assertEqual(2, state['version'])

        new_hash = device_skill_state_hash(self.msm.device_skill_state)
        self.assertEqual(new_hash, self.msm.device_skill_state_hash)

    def test_build_device_skill_state(self):
        """No skill.json file so build one."""
        os.remove(str(self.skills_json_path))
        self.msm._device_skill_state = None
        self.msm._init_skills_data()
        state = self.msm.device_skill_state

        initial_state = [
            dict(name='skill-bar',
                 origin='non-msm',
                 beta=False,
                 status='active',
                 installed=0,
                 updated=0,
                 installation='installed',
                 skill_gid='@|skill-bar'),
            dict(name='skill-foo',
                 origin='non-msm',
                 beta=False,
                 status='active',
                 installed=0,
                 updated=0,
                 installation='installed',
                 skill_gid='@|skill-foo')
        ]

        self.assertTrue(self.skills_json_path.exists())
        with open(self.skills_json_path) as skills_json:
            device_skill_state = json.load(skills_json)
        self.assertListEqual(initial_state, state['skills'])
        self.assertListEqual(initial_state, device_skill_state['skills'])
        self.assertListEqual([], state['blacklist'])
        self.assertListEqual([], device_skill_state['blacklist'])
        self.assertEqual(2, state['version'])
        self.assertEqual(2, device_skill_state['version'])
        new_hash = device_skill_state_hash(self.msm.device_skill_state)
        self.assertEqual(new_hash, self.msm.device_skill_state_hash)

    def test_remove_from_device_skill_state(self):
        """Remove a file no longer installed from the device's skill state.

        Delete skill-bar from the local skills.  This should trigger it being
        removed from the device skill state.
        """

        del (self.msm.local_skills['skill-bar'])
        self.msm._device_skill_state = None
        state = self.msm.device_skill_state

        initial_state = [
            dict(name='skill-foo',
                 origin='default',
                 beta=False,
                 status='active',
                 installed=12345,
                 updated=0,
                 installation='installed',
                 skill_gid='@|skill-foo')
        ]

        self.assertListEqual(initial_state, state['skills'])
        self.assertListEqual([], state['blacklist'])
        self.assertEqual(2, state['version'])

    def test_skill_list(self):
        """The skill.list() method is called."""
        all_skills = self.msm.list()

        skill_names = [skill.name for skill in all_skills]
        self.assertIn('skill-foo', skill_names)
        self.assertIn('skill-bar', skill_names)
        self.assertEqual(2, len(all_skills))
        self.assertIsNone(self.msm._local_skills)
        self.assertIsNone(self.msm._default_skills)
        self.assertEqual(all_skills, self.msm._all_skills)

    def test_install(self):
        """Install a skill

        Test that the install method was called on the skill being installed
        and that the new skill was added to the device's skill state.
        """
        skill_to_install = self.skill_entry_mock()
        skill_to_install.name = 'skill-test'
        skill_to_install.skill_gid = 'test-skill|99.99'
        skill_to_install.is_beta = False
        with patch('msm.mycroft_skills_manager.isinstance') as isinstance_mock:
            isinstance_mock.return_value = True
            with patch('msm.mycroft_skills_manager.time') as time_mock:
                time_mock.time.return_value = 100
                self.msm.install(skill_to_install, origin='voice')

        with open(self.skills_json_path) as skills_json:
            device_skill_state = json.load(skills_json)

        skill_test_state = dict(name='skill-test',
                                origin='voice',
                                beta=False,
                                status='active',
                                installed=100,
                                updated=0,
                                installation='installed',
                                skill_gid='test-skill|99.99')
        self.assertIn(skill_test_state, device_skill_state['skills'])
        self.assertListEqual([call.install(None)],
                             skill_to_install.method_calls)

    def test_already_installed(self):
        """Attempt install of skill already on the device.

        When this happens, an AlreadyInstalled exception is raised and the
        device skill state is not modified.
        """
        skill_to_install = self.skill_entry_mock()
        skill_to_install.name = 'skill-foo'
        skill_to_install.skill_gid = 'skill-foo|99.99'
        skill_to_install.is_beta = False
        skill_to_install.install = Mock(side_effect=AlreadyInstalled())
        pre_install_hash = device_skill_state_hash(self.msm.device_skill_state)
        with patch('msm.mycroft_skills_manager.isinstance') as isinstance_mock:
            isinstance_mock.return_value = True
            with self.assertRaises(AlreadyInstalled):
                self.msm.install(skill_to_install)

        self.assertIsNotNone(self.msm._local_skills)
        self.assertIn('all_skills', self.msm._cache)
        post_install_hash = device_skill_state_hash(
            self.msm.device_skill_state)
        self.assertEqual(pre_install_hash, post_install_hash)

    def test_install_failure(self):
        """Install attempt fails for whatever reason

        When an install fails, the installation will raise a MsmException.  The
        skill install will be saved to the device skill state as failed and
        the error that caused the exception will be included in the state.
        """
        skill_to_install = self.skill_entry_mock()
        skill_to_install.name = 'skill-test'
        skill_to_install.skill_gid = 'skill-test|99.99'
        skill_to_install.is_beta = False
        skill_to_install.install = Mock(side_effect=MsmException('RED ALERT!'))
        with patch('msm.mycroft_skills_manager.isinstance') as isinstance_mock:
            isinstance_mock.return_value = True
            self.msm.install(skill_to_install, origin='cli')

        with open(self.skills_json_path) as skills_json:
            device_skill_state = json.load(skills_json)

        skill_test_state = dict(name='skill-test',
                                origin='cli',
                                beta=False,
                                status='error',
                                installed=0,
                                updated=0,
                                installation='failed',
                                skill_gid='skill-test|99.99',
                                failure_message='RED ALERT!')
        self.assertIn(skill_test_state, self.msm.device_skill_state['skills'])
        self.assertIn(skill_test_state, device_skill_state['skills'])
        self.assertListEqual([call.install(None)],
                             skill_to_install.method_calls)

    def test_remove(self):
        """Remove a skill

        Test that the remove method was called on the skill being installed
        and that the new skill was removed from the device's skill state.
        """
        skill_to_remove = self.skill_entry_mock()
        skill_to_remove.name = 'skill-foo'
        pre_install_hash = device_skill_state_hash(self.msm.device_skill_state)
        with patch('msm.mycroft_skills_manager.isinstance') as isinstance_mock:
            isinstance_mock.return_value = True
            self.msm.remove(skill_to_remove)

        with open(self.skills_json_path) as skills_json:
            device_skill_state = json.load(skills_json)

        skill_names = [skill['name'] for skill in device_skill_state['skills']]
        self.assertNotIn('skill_foo', skill_names)
        skill_names = [
            skill['name'] for skill in self.msm.device_skill_state['skills']
        ]
        self.assertNotIn('skill_foo', skill_names)
        self.assertListEqual([call.remove()], skill_to_remove.method_calls)
        self.assertNotIn('all_skills', self.msm._cache)
        self.assertIsNone(self.msm._local_skills)
        post_install_hash = device_skill_state_hash(
            self.msm.device_skill_state)
        self.assertNotEqual(pre_install_hash, post_install_hash)

    def test_already_removed(self):
        """Attempt removal of skill already removed from the device.

        When this happens, an AlreadyRemoved exception is raised and the
        device skill state is not modified.
        """
        skill_to_remove = self.skill_entry_mock()
        skill_to_remove.name = 'skill-foo'
        skill_to_remove.remove = Mock(side_effect=AlreadyRemoved())
        pre_install_hash = device_skill_state_hash(self.msm.device_skill_state)
        with patch('msm.mycroft_skills_manager.isinstance') as isinstance_mock:
            isinstance_mock.return_value = True
            self.msm.remove(skill_to_remove)

        self.assertListEqual([call.remove()], skill_to_remove.method_calls)
        self.assertIsNotNone(self.msm._local_skills)
        self.assertIn('all_skills', self.msm._cache)
        post_install_hash = device_skill_state_hash(
            self.msm.device_skill_state)
        self.assertEqual(pre_install_hash, post_install_hash)

    def test_remove_failure(self):
        """Skill removal attempt fails for whatever reason

        When n removal fails, a MsmException is raised.  The removal will not
        be saved to the device skill state.
        """
        skill_to_remove = self.skill_entry_mock()
        skill_to_remove.name = 'skill-test'
        skill_to_remove.remove = Mock(side_effect=MsmException('RED ALERT!'))
        pre_install_hash = device_skill_state_hash(self.msm.device_skill_state)
        with patch('msm.mycroft_skills_manager.isinstance') as isinstance_mock:
            isinstance_mock.return_value = True
            with self.assertRaises(MsmException):
                self.msm.remove(skill_to_remove)

        self.assertListEqual([call.remove()], skill_to_remove.method_calls)
        self.assertIsNotNone(self.msm._local_skills)
        self.assertIn('all_skills', self.msm._cache)
        post_install_hash = device_skill_state_hash(
            self.msm.device_skill_state)
        self.assertEqual(pre_install_hash, post_install_hash)

    def test_update(self):
        """Remove a skill

        Test that the remove method was called on the skill being installed
        and that the new skill was removed from the device's skill state.
        """
        skill_to_update = self.skill_entry_mock()
        skill_to_update.name = 'skill-foo'
        skill_to_update.is_beta = False
        pre_install_hash = device_skill_state_hash(self.msm.device_skill_state)
        with patch('msm.mycroft_skills_manager.time') as time_mock:
            time_mock.time.return_value = 100
            self.msm.update(skill_to_update)

        with open(self.skills_json_path) as skills_json:
            device_skill_state = json.load(skills_json)

        skill_names = [skill['name'] for skill in device_skill_state['skills']]
        self.assertIn('skill-foo', skill_names)

        for skill in self.msm.device_skill_state['skills']:
            if skill['name'] == 'skill-foo':
                self.assertEqual(100, skill['updated'])
        self.assertListEqual([call.update()], skill_to_update.method_calls)
        self.assertNotIn('all_skills', self.msm._cache)
        self.assertIsNone(self.msm._local_skills)
        post_install_hash = device_skill_state_hash(
            self.msm.device_skill_state)
        self.assertNotEqual(pre_install_hash, post_install_hash)