Skip to content

Commit

Permalink
feat: portal ocp (#35)
Browse files Browse the repository at this point in the history
* tests

* feat: glados mp3s to OCP playback

* use OCP API, stub and start writing tests

* Update skill.json

* rename unit test file

* broader coverage

* syntax

* remove 3.12 tests

resolves ModuleNotFoundError: No module named 'setuptools'

* remove errant todo

* remove unsupported use case

* remove confusing and unnecessary section from README

* Update skill.json

---------

Co-authored-by: mikejgray <mikejgray@users.noreply.github.com>
  • Loading branch information
mikejgray and mikejgray authored Jan 16, 2024
1 parent 7146587 commit 06ecbff
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 17 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/skill_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ on:
jobs:
py_build_tests:
uses: neongeckocom/.github/.github/workflows/python_build_tests.yml@master
# skill_unit_tests: # One day
# uses: neongeckocom/.github/.github/workflows/skill_tests.yml@master
skill_unit_tests: # One day
uses: neongeckocom/.github/.github/workflows/skill_tests.yml@master
with:
neon_versions: "[3.7, 3.8, 3.9, '3.10', '3.11']"
ovos_versions: "[3.7, 3.8, 3.9, '3.10', '3.11']"
# skill_intent_tests:
# uses: neongeckocom/.github/.github/workflows/skill_test_intents.yml@master
skill_resource_tests:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,5 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.vscode/*

test/skill_fs/*
54 changes: 40 additions & 14 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# pylint: disable=unused-import,missing-docstring,invalid-name
import random
from os import listdir
from os.path import dirname
from os.path import dirname, join

from ovos_workshop.intents import IntentBuilder
from ovos_workshop.decorators import skill_api_method, intent_handler
from ovos_workshop.skills import OVOSSkill
from ovos_bus_client.apis.ocp import OCPInterface

from .stardate import StarDate
from .constants import SPICY_SOUNDS
from skill_easter_eggs.stardate import StarDate
from skill_easter_eggs.constants import SPICY_SOUNDS


class EasterEggsSkill(OVOSSkill):
def initialize(self):
self.ocp = OCPInterface(bus=self.bus) # pylint: disable=attribute-defined-outside-init

@property
def grandma_mode(self):
return self.settings.get("grandma_mode_enabled", True)
Expand Down Expand Up @@ -79,24 +83,31 @@ def handle_number_of_languages_intent(self, _):

@intent_handler(IntentBuilder("portal_intent").require("portal_keyword").build())
def handle_portal_intent(self, _):
path, files = self.get_reference_files("/sounds/portal", "mp3")
path, files = self.get_reference_files("sounds/portal", "mp3")
if len(files):
mp3 = path + "/" + random.choice(files)
self.play_audio(mp3)
self._play_in_ocp(mp3, title="Portal Easter Egg")
else:
self.speak_dialog("bad_file")

def get_reference_files(self, path_ending, extension):
path = dirname(__file__) + path_ending
def get_reference_files(self, path_ending: str, extension: str):
"""Get a list of files in a directory
If grandma mode is enabled, filter out spicy sounds
path_ending: str, path to directory, should not start with /
extension: str, file extension to filter by
"""
path_ending = path_ending.strip("/")
path = join(dirname(__file__), path_ending)
if self.grandma_mode:
files = [sound for sound in listdir(path) if f".{extension}" in sound and sound not in SPICY_SOUNDS]
files = [sound for sound in listdir(path) if f".{extension}" in sound and f"{path_ending}/{sound}" not in SPICY_SOUNDS]
else:
files = [sound for sound in listdir(path) if f".{extension}" in sound]
return path, files

@intent_handler(IntentBuilder("hal_intent").require("hal_keyword").build())
def handle_hal_intent(self, _):
path, files = self.get_reference_files("/sounds/hal", "mp3")
path, files = self.get_reference_files("sounds/hal", "mp3")
if len(files):
mp3 = path + "/" + random.choice(files)
self.play_audio(mp3)
Expand All @@ -106,7 +117,7 @@ def handle_hal_intent(self, _):
@intent_handler(IntentBuilder("duke_nukem_intent").require("duke_nukem_keyword").build())
def handle_dukenukem_intent(self, _):
if not self.grandma_mode:
path, files = self.get_reference_files("/sounds/dukenukem", "wav")
path, files = self.get_reference_files("sounds/dukenukem", "wav")
if len(files):
wav = path + "/" + random.choice(files)
self.play_audio(wav)
Expand All @@ -117,7 +128,7 @@ def handle_dukenukem_intent(self, _):

@intent_handler(IntentBuilder("arnold_intent").require("arnold_keyword").build())
def handle_arnold_intent(self, _):
path, files = self.get_reference_files("/sounds/arnold", "wav")
path, files = self.get_reference_files("sounds/arnold", "wav")
if len(files):
wav = path + "/" + random.choice(files)
self.play_audio(wav)
Expand All @@ -126,7 +137,7 @@ def handle_arnold_intent(self, _):

@intent_handler(IntentBuilder("bender_intent").require("bender_keyword").build())
def handle_bender_intent(self, _):
path, files = self.get_reference_files("/sounds/bender", "mp3")
path, files = self.get_reference_files("sounds/bender", "mp3")
if len(files):
mp3 = path + "/" + random.choice(files)
self.play_audio(mp3)
Expand All @@ -135,13 +146,28 @@ def handle_bender_intent(self, _):

@intent_handler(IntentBuilder("glados_intent").require("glados_keyword").build())
def handle_glados_intent(self, _):
path, files = self.get_reference_files("/sounds/glados", "mp3")
path, files = self.get_reference_files("sounds/glados", "mp3")
if len(files):
mp3 = path + "/" + random.choice(files)
self.play_audio(mp3)
self._play_in_ocp(mp3, title="GlaDOS says...")
else:
self.speak_dialog("bad_file")

@skill_api_method
def get_display_date(self):
return StarDate().getStardate()

def _play_in_ocp(self, media, title="Easter Egg!"):
data = {
"match_confidence": 100,
"media_type": 1, # MediaType.AUDIO
"length": 0,
"uri": media,
"playback": 2, # PlaybackType.AUDIO
"image": "",
"bg_image": "",
"skill_icon": "",
"title": title,
"skill_id": self.skill_id,
}
self.ocp.play(tracks=[data])
4 changes: 4 additions & 0 deletions requirements/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
neon-minerva>=0.1.0
ovos_plugin_manager
pytest
pytest-cov
1 change: 1 addition & 0 deletions requirements.txt → requirements/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
python-dateutil
ovos-utils~=0.0, >=0.0.38
ovos_workshop~=0.0, >=0.0.15
ovos_bus_client~=0.0, >=0.0.6
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def find_resource_files():
version=get_version(),
url=f"https://github.com/OpenVoiceOS/{SKILL_NAME}",
license="BSD-3-Clause",
install_requires=get_requirements("requirements.txt"),
install_requires=get_requirements("requirements/requirements.txt"),
author="Jarbas",
author_email="jarbas@openvoiceos.com",
long_description=long_description,
Expand All @@ -102,4 +102,5 @@ def find_resource_files():
package_data={SKILL_PKG: find_resource_files()},
include_package_data=True,
entry_points={"ovos.plugin.skill": PLUGIN_ENTRY_POINT},
extras_require={"test": get_requirements("requirements/requirements-dev.txt")}
)
1 change: 1 addition & 0 deletions skill.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"requirements": {
"python": [
"ovos-utils~=0.0, >=0.0.38",
"ovos_bus_client~=0.0, >=0.0.6",
"ovos_workshop~=0.0, >=0.0.15",
"python-dateutil"
],
Expand Down
202 changes: 202 additions & 0 deletions test/test_skill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Pytest boilerplate
from genericpath import isdir
from json import dumps
from os.path import dirname, join
from os import environ, getenv, makedirs
from unittest.mock import Mock, patch
import shutil

from ovos_plugin_manager.skills import find_skill_plugins
from ovos_utils.messagebus import FakeBus
import pytest

from skill_easter_eggs import EasterEggsSkill
from skill_easter_eggs.constants import SPICY_SOUNDS


@pytest.fixture(scope="session")
def test_skill(test_skill_id="skill-easter-eggs.openvoiceos", bus=FakeBus()):
# Get test skill
bus.emitter = bus.ee
bus.run_forever()
skill_entrypoint = getenv("TEST_SKILL_ENTRYPOINT")
if not skill_entrypoint:
skill_entrypoints = list(find_skill_plugins().keys())
assert test_skill_id in skill_entrypoints
skill_entrypoint = test_skill_id

skill = EasterEggsSkill(skill_id=test_skill_id, bus=bus)
skill.speak = Mock()
skill.speak_dialog = Mock()
skill.play_audio = Mock()
yield skill
shutil.rmtree(join(dirname(__file__), "skill_fs"), ignore_errors=False)


@pytest.fixture
def reset_skill_mocks(test_skill):
# Reset mocks before each test
test_skill.speak.reset_mock()
test_skill.speak_dialog.reset_mock()
test_skill.play_audio.reset_mock()
yield


class TestEasterEggSkill:
test_fs = join(dirname(__file__), "skill_fs")
data_dir = join(test_fs, "data")
conf_dir = join(test_fs, "config")
environ["XDG_DATA_HOME"] = data_dir
environ["XDG_CONFIG_HOME"] = conf_dir
if not isdir(test_fs):
makedirs(data_dir)
makedirs(conf_dir)

with open(join(conf_dir, "mycroft.conf"), "w", encoding="utf-8") as f:
f.write(dumps({"Audio": {"backends": {"ocp": {"active": True}}}}))

def test_grandma_mode_set_by_default(self, test_skill):
assert test_skill.grandma_mode is True

def test_ocp_api_available(self, test_skill):
assert test_skill.ocp is not None

def test_ocp_api_unavailable_when_ocp_is_disabled(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_grandma_mode(self, test_skill):
test_skill.handle_grandma_mode(None)
test_skill.speak.assert_called_once_with("Ok, we'll tone it down a bit.")
assert test_skill.settings["grandma_mode_enabled"] is True

def test_handle_adult_mode(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_stardate_intent(self, test_skill):
test_skill.handle_stardate_intent(None)
test_skill.speak_dialog.assert_called_once_with(
"stardate", {"stardate": test_skill._create_spoken_stardate()}
)

def test_create_spoken_stardate(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_pod_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_robotic_laws_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_rock_paper_scissors_lizard_spock_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_number_of_languages_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_portal_intent(self, test_skill):
with patch("skill_easter_eggs.EasterEggsSkill._play_in_ocp") as mock_ocp_play:
test_skill.handle_portal_intent(None)
mock_ocp_play.assert_called_once()

def test_get_reference_files_grandma_mode(self, test_skill):
spicy_arnold_sounds = [
gubernator.replace("sounds/arnold/", "")
for gubernator in SPICY_SOUNDS
if gubernator.startswith("sounds/arnold")
]
with patch("skill_easter_eggs.EasterEggsSkill.grandma_mode", True):
_, arnold_safe = test_skill.get_reference_files(
"/sounds/arnold", extension="wav"
)
for spicy_arnold in spicy_arnold_sounds:
assert spicy_arnold not in arnold_safe
with patch("skill_easter_eggs.EasterEggsSkill.grandma_mode", False):
_, arnold_spicy = test_skill.get_reference_files(
"sounds/arnold", extension="wav"
)
for spicy_arnold in [
gubernator.replace("sounds/arnold/", "")
for gubernator in SPICY_SOUNDS
if gubernator.startswith("sounds/arnold")
]:
assert spicy_arnold in arnold_spicy

def test_handle_hal_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_dukenukem_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_handle_arnold_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_bender_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_handle_glados_intent(self, test_skill):
# TODO: Fully implement
assert True

def test_get_display_date(self, test_skill):
# TODO: Fully implement
assert True

def test_play_in_ocp(self, test_skill):
with open(join(self.conf_dir, "mycroft.conf"), "w", encoding="utf-8") as f:
f.write(dumps({"Audio": {"backends": {"ocp": {"active": True}}}}))
media_path = "~/sounds/test.mp3"

with patch("ovos_bus_client.apis.ocp.OCPInterface.play") as mock_ocp_play:
test_skill._play_in_ocp(media_path)
mock_ocp_play.assert_called_once_with(
tracks=[
{
"match_confidence": 100,
"media_type": 1,
"length": 0,
"uri": media_path,
"playback": 2,
"image": "",
"bg_image": "",
"skill_icon": "",
"title": "Easter Egg!",
"skill_id": test_skill.skill_id,
}
]
)

def test_play_in_ocp_custom_title(self, test_skill):
media_path = "~/sounds/test.mp3"
test_skill.ocp = Mock()
test_skill._play_in_ocp(media=media_path, title="GladOS says...")
test_skill.ocp.play.assert_called_once_with(
tracks=[
{
"match_confidence": 100,
"media_type": 1,
"length": 0,
"uri": media_path,
"playback": 2,
"image": "",
"bg_image": "",
"skill_icon": "",
"title": "GladOS says...",
"skill_id": test_skill.skill_id,
}
]
)


if __name__ == "__main__":
pytest.main()
3 changes: 3 additions & 0 deletions test/test_stardate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# TODO: Implement tests for stardate.py
def test_stardate():
assert True

0 comments on commit 06ecbff

Please sign in to comment.