Skip to content

Commit

Permalink
Refactor events to troubleshoot GUI plugin test failures
Browse files Browse the repository at this point in the history
Add unit tests for events module
  • Loading branch information
NeonDaniel committed Jun 8, 2023
1 parent f35d954 commit 4858235
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 28 deletions.
61 changes: 37 additions & 24 deletions ovos_utils/events.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@

import time
from datetime import datetime, timedelta
from inspect import signature

from typing import Callable, Optional
from ovos_utils.intents.intent_service_interface import to_alnum
from ovos_utils.log import LOG
from ovos_utils.messagebus import FakeBus, FakeMessage as Message
Expand All @@ -27,7 +28,7 @@ def unmunge_message(message: Message, skill_id: str) -> Message:
return message


def get_handler_name(handler) -> str:
def get_handler_name(handler: Callable) -> str:
"""
Name (including class if available) of handler function.
Expand All @@ -43,17 +44,20 @@ def get_handler_name(handler) -> str:
return handler.__name__


def create_wrapper(handler, skill_id, on_start, on_end, on_error) -> callable:
def create_wrapper(handler: Callable, skill_id: str,
on_start: Callable, on_end: Callable,
on_error: Callable) -> Callable:
"""
Create the default skill handler wrapper.
This wrapper handles things like metrics, reporting handler start/stop
and errors.
handler (callable): method/function to call
skill_id: skill_id for associated skill
on_start (function): function to call before executing the handler
on_end (function): function to call after executing the handler
on_error (function): function to call for error reporting
@param handler: method/function to call
@param skill_id: skill_id for associated skill
@param on_start: function to call before executing the handler
@param on_end: function to call after executing the handler
@param on_error: function to call for error reporting
@return: callable implementing the passed methods
"""

def wrapper(message):
Expand All @@ -80,8 +84,10 @@ def wrapper(message):
return wrapper


def create_basic_wrapper(handler, on_error=None) -> callable:
"""Create the default skill handler wrapper.
def create_basic_wrapper(handler: Callable,
on_error: Optional[Callable] = None) -> Callable:
"""
Create the default skill handler wrapper.
This wrapper handles things like metrics, reporting handler start/stop
and errors.
Expand All @@ -108,7 +114,8 @@ def wrapper(message):


class EventContainer:
"""Container tracking messagbus handlers.
"""
Container tracking messagebus handlers.
This container tracks events added by a skill, allowing unregistering
all events on shutdown.
Expand All @@ -121,14 +128,14 @@ def __init__(self, bus=None):
def set_bus(self, bus):
self.bus = bus

def add(self, name, handler, once=False):
"""Create event handler for executing intent or other event.
def add(self, name: str, handler: Callable, once: bool = False):
"""
Create event handler for executing intent or other event.
Arguments:
name (string): IntentParser name
handler (func): Method to call
once (bool, optional): Event handler will be removed after it has
been run once.
name: IntentParser name
handler: Method to call
once: Event handler will be removed after it has been run once.
"""

def once_wrapper(message):
Expand All @@ -147,8 +154,9 @@ def once_wrapper(message):

LOG.debug('Added event: {}'.format(name))

def remove(self, name):
"""Removes an event from bus emitter and events list.
def remove(self, name: str) -> bool:
"""
Removes an event from bus emitter and events list.
Args:
name (string): Name of Intent or Scheduler Event
Expand Down Expand Up @@ -181,7 +189,8 @@ def __iter__(self):
return iter(self.events)

def clear(self):
"""Unregister all registered handlers and clear the list of registered
"""
Unregister all registered handlers and clear the list of registered
events.
"""
for e, f in self.events:
Expand All @@ -193,7 +202,8 @@ class EventSchedulerInterface:
"""Interface for accessing the event scheduler over the message bus."""

def __init__(self, name=None, sched_id=None, bus=None, skill_id=None):
# NOTE: can not rename or move sched_id/name arguments to keep api compatibility
# NOTE: can not rename or move sched_id/name arguments to keep api
# compatibility
if name:
LOG.warning("name argument has been deprecated! use skill_id instead")
if sched_id:
Expand Down Expand Up @@ -252,7 +262,10 @@ def _schedule_event(self, handler, when, data, name,
context (dict, optional): message context to send
when the handler is called
"""
if isinstance(when, (int, float)) and when >= 0:
if isinstance(when, (int, float)):
if when < 0:
raise ValueError(f"Expected datetime or positive int/float. "
f"got: {when}")
when = datetime.now() + timedelta(seconds=when)
if not name:
name = self.skill_id + handler.__name__
Expand All @@ -267,7 +280,7 @@ def on_error(e):

wrapped = create_basic_wrapper(handler, on_error)
self.events.add(unique_name, wrapped, once=not repeat_interval)
event_data = {'time': time.mktime(when.timetuple()),
event_data = {'time': when.timestamp(),
'event': unique_name,
'repeat': repeat_interval,
'data': data}
Expand Down
112 changes: 108 additions & 4 deletions test/unittests/test_events.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import unittest
import datetime

from os.path import join, dirname
from threading import Event
from time import time
from unittest.mock import Mock

from ovos_utils.messagebus import FakeBus, Message


class TestEvents(unittest.TestCase):
bus = FakeBus()
test_schedule = join(dirname(__file__), "schedule.json")

def test_unmunge_message(self):
from ovos_utils.events import unmunge_message
# TODO
test_message = Message("test", {"TESTSKILLTESTSKILL": True,
"TESTSKILLdata": "nothing"})
self.assertEqual(unmunge_message(test_message, "OtherSkill"),
test_message)
unmunged = unmunge_message(test_message, "TESTSKILL")
self.assertEqual(unmunged.msg_type, test_message.msg_type)
self.assertEqual(unmunged.data, {"TESTSKILL": True,
"data": "nothing"})

def test_get_handler_name(self):
from ovos_utils.events import get_handler_name
# TODO

class Test:
def __init__(self):
self.name = "test"

def handler(self, msg):
print(f"{self.name}: {msg}")

self.assertEqual(get_handler_name(Test().handler), "test.handler")

def handler():
print("")

self.assertEqual(get_handler_name(handler), "handler")

def test_create_wrapper(self):
from ovos_utils.events import create_wrapper
Expand All @@ -23,5 +54,78 @@ def test_event_container(self):
# TODO

def test_event_scheduler_interface(self):
from ovos_utils.events import EventSchedulerInterface
# TODO
from ovos_utils.events import EventSchedulerInterface, EventContainer
interface = EventSchedulerInterface(bus=self.bus, name="test")
self.assertEqual(interface.bus, self.bus)
self.assertIsInstance(interface.skill_id, str)
test_id = "testing"
interface.set_id(test_id)
self.assertEqual(interface.skill_id, test_id)
self.assertIsInstance(interface.events, EventContainer)
self.assertEqual(interface.events.bus, self.bus)
self.assertEqual(interface.scheduled_repeats, list())

now_time = datetime.datetime.now(datetime.timezone.utc)
self.assertAlmostEqual(now_time.timestamp(), time(), 0)
event_time_tzaware = now_time + datetime.timedelta(hours=1)
event_time_seconds = event_time_tzaware.timestamp()
event_time_tznaive = datetime.datetime.fromtimestamp(event_time_seconds)

scheduled = Event()
messages = list()

def on_schedule(msg):
nonlocal messages
messages.append(msg)
scheduled.set()

self.bus.on('mycroft.scheduler.schedule_event', on_schedule)

context = {
"test": time()
}

data = {
"test": True
}

callback = Mock()
callback.__name__ = "test"

# Schedule TZ Aware
scheduled.clear()
interface.schedule_event(callback, event_time_tzaware, data,
context=context)
self.assertTrue(scheduled.wait(2))
self.assertEqual(len(messages), 1)

# Schedule TZ Naive
scheduled.clear()
interface.schedule_event(callback, event_time_tznaive, data,
context=context)
self.assertTrue(scheduled.wait(2))
self.assertEqual(len(messages), 2)

# Schedule duration
interface.schedule_event(callback, event_time_seconds -
datetime.datetime.now().timestamp(),
data, context=context)
self.assertTrue(scheduled.wait(2))
self.assertEqual(len(messages), 3)

for event in messages:
self.assertIsInstance(event, Message)
self.assertEqual(event.context, context)
self.assertEqual(event.data['data'], data)
self.assertIsInstance(event.data['event'], str)
self.assertIsNone(event.data['repeat'])
self.assertAlmostEqual(event.data['time'], event_time_seconds, 0)

# Schedule invalid
with self.assertRaises(ValueError):
interface.schedule_event(callback, -3.0)

# TODO: Test Repeating, Update, Cancel, Get Status

interface.shutdown()
self.assertEqual(interface.events.events, list())

0 comments on commit 4858235

Please sign in to comment.