Skip to content

Commit

Permalink
Reduce complexity of EvaluationRule
Browse files Browse the repository at this point in the history
Creates rules.py containing logic for the small rule snippets for each
entry in the json
  • Loading branch information
forslund committed Sep 1, 2019
1 parent 785430e commit 3922f9a
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 107 deletions.
56 changes: 56 additions & 0 deletions test/integrationtests/skills/colors.py
@@ -0,0 +1,56 @@
# Copyright 2019 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.
#
"""Color definitions for test output."""
import os


class Clr:
PINK = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
DKGRAY = '\033[90m'
# Classes
USER_UTT = '\033[96m' # cyan
MYCROFT = '\033[33m' # bright yellow
HEADER = '\033[94m' # blue
WARNING = '\033[93m' # yellow
FAIL = '\033[91m' # red
RESET = '\033[0m'


class NoClr:
PINK = ''
BLUE = ''
CYAN = ''
GREEN = ''
YELLOW = ''
RED = ''
DKGRAY = ''
USER_UTT = ''
MYCROFT = ''
HEADER = ''
WARNING = ''
FAIL = ''
RESET = ''


# MST as in Mycroft Skill Tester
if 'MST_NO_COLOR' not in os.environ:
color = Clr
else:
color = NoClr
100 changes: 100 additions & 0 deletions test/integrationtests/skills/rules.py
@@ -0,0 +1,100 @@
# Copyright 2019 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.
#
"""A collection of rules for the skill tester."""
import re

from mycroft.util.format import expand_options

from .colors import color


def intent_type_check(intent_type):
return (['or'] +
[['endsWith', 'intent_type', intent_type]] +
[['endsWith', '__type__', intent_type]])


def play_query_check(skill, match, phrase):
d = ['and']
d.append(['equal', '__type__', 'query'])
d.append(['equal', 'skill_id', skill.skill_id])
d.append(['equal', 'phrase', phrase])
d.append(['gt', 'conf', match.get('confidence_threshold', 0.5)])
return d


def question_check(skill, question, expected_answer):
d = ['and']
d.append(['equal', '__type__', 'query.response'])
d.append(['equal', 'skill_id', skill.skill_id])
d.append(['equal', 'phrase', question])
d.append(['match', 'answer', expected_answer])
return d


def expected_data_check(expected_items):
d = ['and']
for item in expected_items:
d.append(['equal', item[0], item[1]])
return d


def load_dialog_list(skill, dialog):
""" Load dialog from files into a single list.
Args:
skill (MycroftSkill): skill to load dialog from
dialog (list): Dialog names (str) to load
Returns:
list: Expanded dialog strings
"""
dialogs = []
try:
for d in dialog:
for e in skill.dialog_renderer.templates[d]:
dialogs += expand_options(e)
except Exception as template_load_exception:
print(color.FAIL +
"Failed to load dialog template " +
"'dialog/en-us/" + d + ".dialog'" +
color.RESET)
raise Exception("Can't load 'excepted_dialog': "
"file '" + d + ".dialog'") \
from template_load_exception
return dialogs


def expected_dialog_check(expected_dialog, skill):
# Check that expected dialog file is used
if isinstance(expected_dialog, str):
dialog = [expected_dialog] # Make list
else:
dialog = expected_dialog
# Extract dialog texts from skill
dialogs = load_dialog_list(skill, dialog)
# Allow custom fields to be anything
d = [re.sub(r'{.*?\}', r'.*', t) for t in dialogs]
# Merge consequtive .*'s into a single .*
d = [re.sub(r'\.\*( \.\*)+', r'.*', t) for t in d]

# Create rule allowing any of the sentences for that dialog
return [['match', 'utterance', r] for r in d]


def changed_context_check(ctx):
if not isinstance(ctx, list):
ctx = [ctx]
return [['endsWith', 'context', str(c)] for c in ctx]
125 changes: 18 additions & 107 deletions test/integrationtests/skills/skill_tester.py
Expand Up @@ -45,12 +45,16 @@
from mycroft.skills.settings import SkillSettings
from mycroft.skills.skill_loader import SkillLoader
from mycroft.configuration import Configuration
from mycroft.util.format import expand_options

from logging import StreamHandler
from io import StringIO
from contextlib import contextmanager

from .colors import color
from .rules import (intent_type_check, play_query_check, question_check,
expected_data_check, expected_dialog_check,
changed_context_check)

MainModule = '__init__'

DEFAULT_EVALUAITON_TIMEOUT = 30
Expand All @@ -63,47 +67,6 @@ class SkillTestError(Exception):
pass


# Easy way to show colors on terminals
class clr:
PINK = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
DKGRAY = '\033[90m'
# Classes
USER_UTT = '\033[96m' # cyan
MYCROFT = '\033[33m' # bright yellow
HEADER = '\033[94m' # blue
WARNING = '\033[93m' # yellow
FAIL = '\033[91m' # red
RESET = '\033[0m'


class no_clr:
PINK = ''
BLUE = ''
CYAN = ''
GREEN = ''
YELLOW = ''
RED = ''
DKGRAY = ''
USER_UTT = ''
MYCROFT = ''
HEADER = ''
WARNING = ''
FAIL = ''
RESET = ''


# MST as in Mycroft Skill Tester
if 'MST_NO_COLOR' not in os.environ:
color = clr
else:
color = no_clr


@contextmanager
def temporary_handler(log, handler):
"""Context manager to replace the default logger with a temporary logger.
Expand Down Expand Up @@ -541,32 +504,6 @@ def results(self, evaluation_rule):
'gui.page.show', 'gui.value.set']


def load_dialog_list(skill, dialog):
""" Load dialog from files into a single list.
Args:
skill (MycroftSkill): skill to load dialog from
dialog (list): Dialog names (str) to load
Returns:
list: Expanded dialog strings
"""
dialogs = []
try:
for d in dialog:
for e in skill.dialog_renderer.templates[d]:
dialogs += expand_options(e)
except Exception as template_load_exception:
print(color.FAIL +
"Failed to load dialog template " +
"'dialog/en-us/" + d + ".dialog'" +
color.RESET)
raise Exception("Can't load 'excepted_dialog': "
"file '" + d + ".dialog'") \
from template_load_exception
return dialogs


class EvaluationRule:
"""
This class initially convert the test_case json file to internal rule
Expand All @@ -592,9 +529,7 @@ def __init__(self, test_case, skill=None):
_x = ['and']
if 'utterance' in test_case and 'intent_type' in test_case:
intent_type = str(test_case['intent_type'])
_x.append(['or'] +
[['endsWith', 'intent_type', intent_type]] +
[['endsWith', '__type__', intent_type]])
_x.append(intent_type_check(intent_type))

# Check for adapt intent info
if test_case.get('intent', None):
Expand All @@ -604,26 +539,16 @@ def __init__(self, test_case, skill=None):
if 'play_query_match' in test_case:
match = test_case['play_query_match']
phrase = match.get('phrase', test_case.get('play_query'))
_d = ['and']
_d.append(['equal', '__type__', 'query'])
_d.append(['equal', 'skill_id', skill.skill_id])
_d.append(['equal', 'phrase', phrase])
_d.append(['gt', 'conf', match.get('confidence_threshold', 0.5)])
self.rule.append(_d)
self.rule.append(play_query_check(skill, match, phrase))
elif 'expected_answer' in test_case:
_d = ['and']
_d.append(['equal', '__type__', 'query.response'])
_d.append(['equal', 'skill_id', skill.skill_id])
_d.append(['equal', 'phrase', test_case['question']])
_d.append(['match', 'answer', test_case['expected_answer']])
self.rule.append(_d)
question = test_case['question']
expected_answer = test_case['expected_answer']
self.rule.append(question_check(skill, question, expected_answer))

# Check for expected data structure
if test_case.get('expected_data'):
_d = ['and']
for item in test_case['expected_data'].items():
_d.append(['equal', item[0], item[1]])
self.rule.append(_d)
expected_items = test_case['expected_data'].items()
self.rule.append(expected_data_check(expected_items))

if _x != ['and']:
self.rule.append(_x)
Expand All @@ -647,29 +572,15 @@ def __init__(self, test_case, skill=None):
'Skill is missing, can\'t run expected_dialog test' +
color.RESET)
else:
# Check that expected dialog file is used
if isinstance(test_case['expected_dialog'], str):
dialog = [test_case['expected_dialog']] # Make list
else:
dialog = test_case['expected_dialog']
# Extract dialog texts from skill
dialogs = load_dialog_list(skill, dialog)
# Allow custom fields to be anything
d = [re.sub(r'{.*?\}', r'.*', t) for t in dialogs]
# Merge consequtive .*'s into a single .*
d = [re.sub(r'\.\*( \.\*)+', r'.*', t) for t in d]

# Create rule allowing any of the sentences for that dialog
rules = [['match', 'utterance', r] for r in d]
self.rule.append(['or'] + rules)
expected_dialog = test_case['expected_dialog']
self.rule.append(['or'] +
expected_dialog_check(expected_dialog,
skill))

if test_case.get('changed_context', None):
ctx = test_case['changed_context']
if isinstance(ctx, list):
for c in ctx:
self.rule.append(['endsWith', 'context', str(c)])
else:
self.rule.append(['equal', 'context', ctx])
for c in changed_context_check(ctx):
self.rule.append(c)

if test_case.get('assert', None):
for _x in ast.literal_eval(test_case['assert']):
Expand Down

0 comments on commit 3922f9a

Please sign in to comment.