Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: portal ocp #35

Merged
merged 12 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = {
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved
"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