Coverage for test/unittests/test_skill_installer.py: 98%
173 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-17 13:44 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-17 13:44 +0000
1from unittest.mock import Mock
3import pytest
5from ovos_bus_client import Message
6from ovos_core.skill_installer import SkillsStore
9class MessageBusMock:
10 """Replaces actual message bus calls in unit tests.
12 The message bus should not be running during unit tests so mock it
13 out in a way that makes it easy to test code that calls it.
14 """
16 def __init__(self):
17 self.message_types = []
18 self.message_data = []
19 self.event_handlers = []
21 def emit(self, message):
22 self.message_types.append(message.msg_type)
23 self.message_data.append(message.data)
25 def on(self, event, _):
26 self.event_handlers.append(event)
28 def remove(self, event, _):
29 self.event_handlers.remove(event)
31 def once(self, event, _):
32 self.event_handlers.append(event)
34 def wait_for_response(self, message):
35 self.emit(message)
38@pytest.fixture(scope="function", autouse=True)
39def skills_store(request):
40 config = getattr(request, 'param', {})
41 return SkillsStore(bus=MessageBusMock(), config=config)
44def test_shutdown(skills_store):
45 assert skills_store.shutdown() is None
48def test_play_error_sound(skills_store):
49 skills_store.play_error_sound()
50 assert skills_store.bus.message_data[-1] == {
51 "uri": "snd/error.mp3"
52 }
53 assert skills_store.bus.message_types[-1] == "mycroft.audio.play_sound"
56@pytest.mark.parametrize("skills_store", [{"sounds": {"pip_error": "snd/custom_error.mp3"}}], indirect=True)
57def test_play_error_sound_custom(skills_store):
58 skills_store.play_error_sound()
59 assert skills_store.bus.message_data[-1] == {
60 "uri": "snd/custom_error.mp3"
61 }
62 assert skills_store.bus.message_types[-1] == "mycroft.audio.play_sound"
65def test_play_success_sound(skills_store):
66 skills_store.play_success_sound()
67 assert skills_store.bus.message_data[-1] == {
68 "uri": "snd/acknowledge.mp3"
69 }
70 assert skills_store.bus.message_types[-1] == "mycroft.audio.play_sound"
73@pytest.mark.parametrize("skills_store", [{"sounds": {"pip_success": "snd/custom_success.mp3"}}], indirect=True)
74def test_play_success_sound_custom(skills_store):
75 skills_store.play_success_sound()
76 assert skills_store.bus.message_data[-1] == {
77 "uri": "snd/custom_success.mp3"
78 }
79 assert skills_store.bus.message_types[-1] == "mycroft.audio.play_sound"
82def test_pip_install_no_packages(skills_store):
83 # TODO: This method should be refactored in 0.1.0 for easier unit testing
84 skills_store.play_error_sound = Mock()
85 res = skills_store.pip_install([])
86 assert res is False
87 skills_store.play_error_sound.assert_called_once()
90def test_pip_install_no_constraints(skills_store):
91 skills_store.play_error_sound = Mock()
92 res = skills_store.pip_install(["foo", "bar"], constraints="not/real")
93 assert res is False
94 skills_store.play_error_sound.assert_called_once()
97def test_pip_install_happy_path():
98 # TODO: This method should be refactored in 0.1.0 for easier unit testing
99 assert True
102def test_pip_uninstall_no_packages(skills_store):
103 # TODO: This method should be refactored in 0.1.0 for easier unit testing
104 skills_store.play_error_sound = Mock()
105 res = skills_store.pip_uninstall([])
106 assert res is False
107 skills_store.play_error_sound.assert_called_once()
110def test_pip_uninstall_no_constraints(skills_store):
111 skills_store.play_error_sound = Mock()
112 res = skills_store.pip_uninstall(["foo", "bar"], constraints="not/real")
113 assert res is False
114 skills_store.play_error_sound.assert_called_once()
117def test_pip_uninstall_happy_path():
118 # TODO: This method should be refactored in 0.1.0 for easier unit testing
119 assert True
122def test_validate_skill(skills_store):
123 assert skills_store.validate_skill("https://github.com/openvoiceos/skill-foo") is True
124 assert skills_store.validate_skill("https://gitlab.com/foo/skill-bar") is False
125 assert skills_store.validate_skill("literally-anything-else") is False
128@pytest.mark.parametrize('skills_store', [{"allow_pip": False}], indirect=True)
129def test_handle_install_skill_not_allowed(skills_store):
130 skills_store.play_error_sound = Mock()
131 skills_store.validate_skill = Mock()
132 skills_store.handle_install_skill(Message(msg_type="test", data={}))
133 skills_store.play_error_sound.assert_called_once()
134 assert skills_store.bus.message_types[-1] == "ovos.skills.install.failed"
135 assert skills_store.bus.message_data[-1] == {"error": "pip disabled in mycroft.conf"}
136 skills_store.validate_skill.assert_not_called()
139@pytest.mark.parametrize('skills_store', [{"allow_pip": True}], indirect=True)
140def test_handle_install_skill_not_from_github(skills_store):
141 skills_store.play_error_sound = Mock()
142 skills_store.handle_install_skill(Message(msg_type="test", data={"url": "beautifulsoup4"}))
143 skills_store.play_error_sound.assert_called_once()
144 assert skills_store.bus.message_types[-1] == "ovos.skills.install.failed"
145 assert skills_store.bus.message_data[-1] == {"error": "skill url validation failed"}
148@pytest.mark.parametrize('skills_store', [{"allow_pip": True}], indirect=True)
149def test_handle_install_skill_from_github(skills_store):
150 skills_store.play_error_sound = Mock()
151 skills_store.pip_install = Mock(return_value=True)
152 skills_store.handle_install_skill(
153 Message(msg_type="test", data={"url": "https://github.com/OpenVoiceOS/skill-foo"}))
154 skills_store.play_error_sound.assert_not_called()
155 skills_store.pip_install.assert_called_once_with(["git+https://github.com/OpenVoiceOS/skill-foo"])
156 assert skills_store.bus.message_types[-1] == "ovos.skills.install.complete"
157 assert skills_store.bus.message_data[-1] == {}
160@pytest.mark.parametrize('skills_store', [{"allow_pip": True}], indirect=True)
161def test_handle_install_skill_from_github_failure(skills_store):
162 skills_store.play_error_sound = Mock()
163 skills_store.pip_install = Mock(return_value=False)
164 skills_store.handle_install_skill(
165 Message(msg_type="test", data={"url": "https://github.com/OpenVoiceOS/skill-foo"}))
166 skills_store.play_error_sound.assert_not_called()
167 skills_store.pip_install.assert_called_once_with(["git+https://github.com/OpenVoiceOS/skill-foo"])
168 assert skills_store.bus.message_types[-1] == "ovos.skills.install.failed"
171@pytest.mark.parametrize('skills_store', [{"allow_pip": False}], indirect=True)
172def test_handle_uninstall_skill_not_allowed(skills_store):
173 skills_store.play_error_sound = Mock()
174 skills_store.handle_uninstall_skill(Message(msg_type="test", data={}))
175 skills_store.play_error_sound.assert_called_once()
176 assert skills_store.bus.message_types[-1] == "ovos.skills.uninstall.failed"
177 assert skills_store.bus.message_data[-1] == {"error": "pip disabled in mycroft.conf"}
180@pytest.mark.parametrize('skills_store', [{"allow_pip": True}], indirect=True)
181def test_handle_uninstall_skill(skills_store):
182 skills_store.play_error_sound = Mock()
183 skills_store.handle_uninstall_skill(Message(msg_type="test", data={}))
184 skills_store.play_error_sound.assert_called_once()
185 assert skills_store.bus.message_types[-1] == "ovos.skills.uninstall.failed"
186 assert skills_store.bus.message_data[-1] == {"error": "not implemented"}
189@pytest.mark.parametrize('skills_store', [{"allow_pip": False}], indirect=True)
190def test_handle_install_python_not_allowed(skills_store):
191 skills_store.play_error_sound = Mock()
192 skills_store.pip_install = Mock()
193 skills_store.handle_install_python(Message(msg_type="test", data={}))
194 skills_store.play_error_sound.assert_called_once()
195 assert skills_store.bus.message_types[-1] == "ovos.pip.install.failed"
196 assert skills_store.bus.message_data[-1] == {"error": "pip disabled in mycroft.conf"}
197 skills_store.pip_install.assert_not_called()
200@pytest.mark.parametrize('skills_store', [{"allow_pip": True}], indirect=True)
201def test_handle_install_python_no_packages(skills_store):
202 skills_store.pip_install = Mock()
203 skills_store.handle_install_python(Message(msg_type="test", data={}))
204 assert skills_store.bus.message_types[-1] == "ovos.pip.install.failed"
205 assert skills_store.bus.message_data[-1] == {"error": "no packages to install"}
206 skills_store.pip_install.assert_not_called()
209@pytest.mark.parametrize('skills_store', [{"allow_pip": True}], indirect=True)
210def test_handle_install_python_success(skills_store):
211 skills_store.play_error_sound = Mock()
212 skills_store.pip_install = Mock()
213 packages = ["requests", "fastapi"]
214 skills_store.handle_install_python(Message(msg_type="test", data={"packages": packages}))
215 skills_store.play_error_sound.assert_not_called()
216 skills_store.pip_install.assert_called_once_with(packages)
217 assert skills_store.bus.message_types[-1] == "ovos.pip.install.complete"
218 assert skills_store.bus.message_data[-1] == {}
221@pytest.mark.parametrize('skills_store', [{"allow_pip": False}], indirect=True)
222def test_handle_uninstall_python_not_allowed(skills_store):
223 skills_store.play_error_sound = Mock()
224 skills_store.pip_uninstall = Mock()
225 skills_store.handle_uninstall_python(Message(msg_type="test", data={}))
226 skills_store.play_error_sound.assert_called_once()
227 assert skills_store.bus.message_types[-1] == "ovos.pip.uninstall.failed"
228 assert skills_store.bus.message_data[-1] == {"error": "pip disabled in mycroft.conf"}
229 skills_store.pip_uninstall.assert_not_called()
232@pytest.mark.parametrize('skills_store', [{"allow_pip": True}], indirect=True)
233def test_handle_uninstall_python_no_packages(skills_store):
234 skills_store.pip_uninstall = Mock()
235 skills_store.handle_uninstall_python(Message(msg_type="test", data={}))
236 assert skills_store.bus.message_types[-1] == "ovos.pip.uninstall.failed"
237 assert skills_store.bus.message_data[-1] == {"error": "no packages to install"}
238 skills_store.pip_uninstall.assert_not_called()
241@pytest.mark.parametrize('skills_store', [{"allow_pip": True}], indirect=True)
242def test_handle_uninstall_python_success(skills_store):
243 skills_store.play_error_sound = Mock()
244 skills_store.pip_uninstall = Mock()
245 packages = ["requests", "fastapi"]
246 skills_store.handle_uninstall_python(Message(msg_type="test", data={"packages": packages}))
247 skills_store.play_error_sound.assert_not_called()
248 skills_store.pip_uninstall.assert_called_once_with(packages)
249 assert skills_store.bus.message_types[-1] == "ovos.pip.uninstall.complete"
250 assert skills_store.bus.message_data[-1] == {}
253if __name__ == "__main__":
254 pytest.main()