Added message tester utility. Reduced line length to <80 chars in general
parent
a16c2a0ecc
commit
6c226ea4d9
|
@ -0,0 +1,87 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright 2017 Mycroft AI Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
from Tkinter import *
|
||||||
|
import ScrolledText
|
||||||
|
import skill_tester
|
||||||
|
import ast
|
||||||
|
|
||||||
|
EXAMPLE_EVENT = "{'expect_response': False, \
|
||||||
|
'utterance': u'Recording audio for 600 seconds'}"
|
||||||
|
|
||||||
|
EXAMPLE_TEST_CASE = '{ \n\
|
||||||
|
"utterance": "record", \n\
|
||||||
|
"intent_type": "AudioRecordSkillIntent", \n\
|
||||||
|
"intent": { \n\
|
||||||
|
"AudioRecordSkillKeyword": "record" \n\
|
||||||
|
}, \n\
|
||||||
|
"expected_response": ".*(recording|audio)" \n\
|
||||||
|
}'
|
||||||
|
|
||||||
|
|
||||||
|
class MessageTester:
|
||||||
|
"""
|
||||||
|
This message tester lets a user input a Message event and a json test
|
||||||
|
case, and allows the evaluation of the Message event based on the
|
||||||
|
test case
|
||||||
|
|
||||||
|
It is supposed to be run in the Mycroft virtualenv, and python-tk
|
||||||
|
must be installed (on Ubuntu: apt-get install python-tk)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, root):
|
||||||
|
root.title("Message tester")
|
||||||
|
Label(root, text="Enter message event below", bg="light green").pack()
|
||||||
|
self.event_field = ScrolledText.ScrolledText(root,
|
||||||
|
width=180, height=10)
|
||||||
|
self.event_field.pack()
|
||||||
|
|
||||||
|
Label(root, text="Enter test case below", bg="light green").pack()
|
||||||
|
self.test_case_field = ScrolledText.ScrolledText(root,
|
||||||
|
width=180, height=20)
|
||||||
|
self.test_case_field.pack()
|
||||||
|
|
||||||
|
Label(root, text="Test result:", bg="light green").pack()
|
||||||
|
|
||||||
|
self.result_field = ScrolledText.ScrolledText(root,
|
||||||
|
width=180, height=10)
|
||||||
|
self.result_field.pack()
|
||||||
|
self.result_field.config(state=DISABLED)
|
||||||
|
self.button = Button(root, text="Evaluate", fg="red",
|
||||||
|
command=self.clicked)
|
||||||
|
self.button.pack()
|
||||||
|
|
||||||
|
self.event_field.delete('1.0', END)
|
||||||
|
self.event_field.insert('insert', EXAMPLE_EVENT)
|
||||||
|
self.test_case_field.delete('1.0', END)
|
||||||
|
self.test_case_field.insert('insert', EXAMPLE_TEST_CASE)
|
||||||
|
|
||||||
|
def clicked(self):
|
||||||
|
event = self.event_field.get('1.0', END)
|
||||||
|
test_case = self.test_case_field.get('1.0', END)
|
||||||
|
|
||||||
|
evaluation = skill_tester.EvaluationRule(ast.literal_eval(test_case))
|
||||||
|
evaluation.evaluate(ast.literal_eval(event))
|
||||||
|
self.result_field.config(state=NORMAL)
|
||||||
|
self.result_field.delete('1.0', END)
|
||||||
|
self.result_field.insert('insert', evaluation.rule)
|
||||||
|
self.result_field.config(state=DISABLED)
|
||||||
|
|
||||||
|
|
||||||
|
r = Tk()
|
||||||
|
app = MessageTester(r)
|
||||||
|
r.mainloop()
|
|
@ -13,7 +13,6 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
import glob
|
import glob
|
||||||
import sys
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -23,6 +22,7 @@ from test.integrationtests.skills.skill_tester import SkillTest
|
||||||
|
|
||||||
HOME_DIR = os.path.dirname(os.path.abspath(__file__))
|
HOME_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
def discover_tests():
|
def discover_tests():
|
||||||
"""
|
"""
|
||||||
Find test files starting from the directory where this file resides.
|
Find test files starting from the directory where this file resides.
|
||||||
|
@ -54,6 +54,7 @@ class IntentTestSequenceMeta(type):
|
||||||
def gen_test(a, b):
|
def gen_test(a, b):
|
||||||
def test(self):
|
def test(self):
|
||||||
SkillTest(a, b, self.emitter).run(self.loader)
|
SkillTest(a, b, self.emitter).run(self.loader)
|
||||||
|
|
||||||
return test
|
return test
|
||||||
|
|
||||||
tests = discover_tests()
|
tests = discover_tests()
|
||||||
|
@ -80,17 +81,17 @@ class IntentTestSequence(unittest.TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
__metaclass__ = IntentTestSequenceMeta
|
__metaclass__ = IntentTestSequenceMeta
|
||||||
|
loader = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(cls):
|
||||||
self.loader = MockSkillsLoader(HOME_DIR)
|
cls.loader = MockSkillsLoader(HOME_DIR)
|
||||||
self.emitter = self.loader.load_skills()
|
cls.emitter = cls.loader.load_skills()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(self):
|
def tearDownClass(cls):
|
||||||
self.loader.unload_skills()
|
cls.loader.unload_skills()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -24,7 +24,8 @@ from os.path import join, isdir
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from mycroft.messagebus.message import Message
|
from mycroft.messagebus.message import Message
|
||||||
from mycroft.skills.core import create_skill_descriptor, load_skill, MycroftSkill
|
from mycroft.skills.core import create_skill_descriptor, load_skill, \
|
||||||
|
MycroftSkill
|
||||||
|
|
||||||
MainModule = '__init__'
|
MainModule = '__init__'
|
||||||
|
|
||||||
|
@ -38,7 +39,8 @@ def get_skills(skills_folder):
|
||||||
is discovered
|
is discovered
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
skills_folder: Folder to start a search for skills __init__.py files
|
skills_folder: Folder to start a search for skills __init__.py
|
||||||
|
files
|
||||||
"""
|
"""
|
||||||
|
|
||||||
skills = []
|
skills = []
|
||||||
|
@ -85,8 +87,8 @@ def unload_skills(skills):
|
||||||
|
|
||||||
class InterceptEmitter(object):
|
class InterceptEmitter(object):
|
||||||
"""
|
"""
|
||||||
This class intercepts and allows emitting events between the skill_tester and
|
This class intercepts and allows emitting events between the
|
||||||
the skill being tested.
|
skill_tester and the skill being tested.
|
||||||
When a test is running emitted communication is intercepted for analysis
|
When a test is running emitted communication is intercepted for analysis
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -136,8 +138,8 @@ class MockSkillsLoader(object):
|
||||||
|
|
||||||
class SkillTest(object):
|
class SkillTest(object):
|
||||||
"""
|
"""
|
||||||
This class is instantiated for each skill being tested. It holds the data
|
This class is instantiated for each skill being tested. It holds the
|
||||||
needed for the test, and contains the methods doing the test
|
data needed for the test, and contains the methods doing the test
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -172,7 +174,8 @@ class SkillTest(object):
|
||||||
s.emitter.q = q
|
s.emitter.q = q
|
||||||
|
|
||||||
# Set up context before calling intent
|
# Set up context before calling intent
|
||||||
# This option makes it possible to better isolate (reduce dependance) between test_cases
|
# This option makes it possible to better isolate (reduce dependance)
|
||||||
|
# between test_cases
|
||||||
cxt = test_case.get('remove_context', None)
|
cxt = test_case.get('remove_context', None)
|
||||||
if cxt:
|
if cxt:
|
||||||
if isinstance(cxt, list):
|
if isinstance(cxt, list):
|
||||||
|
@ -196,7 +199,8 @@ class SkillTest(object):
|
||||||
|
|
||||||
# Wait up to X seconds for the test_case to complete
|
# Wait up to X seconds for the test_case to complete
|
||||||
timeout = time.time() + int(test_case.get('evaluation_timeout', None)) \
|
timeout = time.time() + int(test_case.get('evaluation_timeout', None)) \
|
||||||
if test_case.get('evaluation_timeout', None) and isinstance(test_case['evaluation_timeout'], int) \
|
if test_case.get('evaluation_timeout', None) and \
|
||||||
|
isinstance(test_case['evaluation_timeout'], int) \
|
||||||
else time.time() + DEFAULT_EVALUAITON_TIMEOUT
|
else time.time() + DEFAULT_EVALUAITON_TIMEOUT
|
||||||
while not evaluation_rule.all_succeeded():
|
while not evaluation_rule.all_succeeded():
|
||||||
try:
|
try:
|
||||||
|
@ -222,19 +226,20 @@ class SkillTest(object):
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add command line utility to test an event against a test_case, allow for debugging tests
|
# TODO: Add command line utility to test an event against a test_case, allow
|
||||||
|
# for debugging tests
|
||||||
|
|
||||||
class EvaluationRule(object):
|
class EvaluationRule(object):
|
||||||
"""
|
"""
|
||||||
This class initially convert the test_case json file to internal rule format, which is
|
This class initially convert the test_case json file to internal rule
|
||||||
stored throughout the testcase run. All Messages on the event bus can be evaluated against the
|
format, which is stored throughout the testcase run. All Messages on
|
||||||
rules (test_case)
|
the event bus can be evaluated against the rules (test_case)
|
||||||
|
|
||||||
This approach makes it easier to add new tests, since Message and rule traversal is already
|
This approach makes it easier to add new tests, since Message and rule
|
||||||
set up for the internal rule format.
|
traversal is already set up for the internal rule format.
|
||||||
The test writer can use the internal rule format directly in the test_case
|
The test writer can use the internal rule format directly in the
|
||||||
using the assert keyword, which allows for more powerfull/individual
|
test_case using the assert keyword, which allows for more
|
||||||
test cases than the standard dictionaly
|
powerfull/individual test cases than the standard dictionaly
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, test_case):
|
def __init__(self, test_case):
|
||||||
|
@ -248,7 +253,8 @@ class EvaluationRule(object):
|
||||||
|
|
||||||
_x = ['and']
|
_x = ['and']
|
||||||
if test_case.get('utterance', None):
|
if test_case.get('utterance', None):
|
||||||
_x.append(['endsWith', 'intent_type', str(test_case['intent_type'])])
|
_x.append(['endsWith', 'intent_type',
|
||||||
|
str(test_case['intent_type'])])
|
||||||
|
|
||||||
if test_case.get('intent', None):
|
if test_case.get('intent', None):
|
||||||
for item in test_case['intent'].items():
|
for item in test_case['intent'].items():
|
||||||
|
@ -258,7 +264,8 @@ class EvaluationRule(object):
|
||||||
self.rule.append(_x)
|
self.rule.append(_x)
|
||||||
|
|
||||||
if test_case.get('expected_response', None):
|
if test_case.get('expected_response', None):
|
||||||
self.rule.append(['match', 'utterance', str(test_case['expected_response'])])
|
self.rule.append(['match', 'utterance',
|
||||||
|
str(test_case['expected_response'])])
|
||||||
|
|
||||||
if test_case.get('changed_context', None):
|
if test_case.get('changed_context', None):
|
||||||
ctx = test_case['changed_context']
|
ctx = test_case['changed_context']
|
||||||
|
@ -304,7 +311,8 @@ class EvaluationRule(object):
|
||||||
|
|
||||||
def _partial_evaluate(self, rule, msg):
|
def _partial_evaluate(self, rule, msg):
|
||||||
"""
|
"""
|
||||||
Evaluate the message against a part of the rules (recursive over rules)
|
Evaluate the message against a part of the rules (recursive over
|
||||||
|
rules)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rule: A rule or a part of the rules to be broken down further
|
rule: A rule or a part of the rules to be broken down further
|
||||||
|
@ -323,11 +331,13 @@ class EvaluationRule(object):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if rule[0] == 'endsWith':
|
if rule[0] == 'endsWith':
|
||||||
if not (self._get_field_value(rule[1], msg) and self._get_field_value(rule[1], msg).endswith(rule[2])):
|
if not (self._get_field_value(rule[1], msg) and
|
||||||
|
self._get_field_value(rule[1], msg).endswith(rule[2])):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if rule[0] == 'match':
|
if rule[0] == 'match':
|
||||||
if not (self._get_field_value(rule[1], msg) and re.match(rule[2], self._get_field_value(rule[1], msg))):
|
if not (self._get_field_value(rule[1], msg) and
|
||||||
|
re.match(rule[2], self._get_field_value(rule[1], msg))):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if rule[0] == 'and':
|
if rule[0] == 'and':
|
||||||
|
@ -350,7 +360,7 @@ class EvaluationRule(object):
|
||||||
Test if all rules succeeded
|
Test if all rules succeeded
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True is all rules succeeded
|
bool: True if all rules succeeded
|
||||||
"""
|
"""
|
||||||
# return len(filter(lambda x: x[-1] != 'succeeded', self.rule)) == 0
|
# return len(filter(lambda x: x[-1] != 'succeeded', self.rule)) == 0
|
||||||
return len([x for x in self.rule if x[-1] != 'succeeded']) == 0
|
return len([x for x in self.rule if x[-1] != 'succeeded']) == 0
|
||||||
|
|
Loading…
Reference in New Issue