Commit 9a2932b5 authored by J. Fernando Sánchez's avatar J. Fernando Sánchez
Browse files

Merge branch '39-plugin-tests-missing'

parents 9acee508 85aef3d1
......@@ -70,8 +70,13 @@ dev: dev-$(PYMAIN)
test-all: $(addprefix test-,$(PYVERSIONS))
# Run setup.py from in an isolated container, built from the base image.
# This speeds tests up because the image has most (if not all) of the dependencies already.
test-%:
docker run --rm --entrypoint /usr/local/bin/python -w /usr/src/app $(IMAGENAME):python$* setup.py test
docker rm $(NAME)-test-$* || true
docker create -ti --name $(NAME)-test-$* --entrypoint="" -w /usr/src/app/ $(IMAGENAME):python$* python setup.py test
docker cp . $(NAME)-test-$*:/usr/src/app
docker start -a $(NAME)-test-$*
test: test-$(PYMAIN)
......
......@@ -15,25 +15,12 @@ from threading import Thread
import os
import copy
import fnmatch
import inspect
import sys
import importlib
import logging
import traceback
import yaml
import subprocess
logger = logging.getLogger(__name__)
def log_subprocess_output(process):
for line in iter(process.stdout.readline, b''):
logger.info('%r', line)
for line in iter(process.stderr.readline, b''):
logger.error('%r', line)
class Senpy(object):
""" Default Senpy extension for Flask """
......@@ -330,84 +317,6 @@ class Senpy(object):
th.start()
return th
@classmethod
def validate_info(cls, info):
return all(x in info for x in ('name', 'module', 'description', 'version'))
def install_deps(self):
for i in self.plugins.values():
self._install_deps(i)
@classmethod
def _install_deps(cls, info=None):
requirements = info.get('requirements', [])
if requirements:
pip_args = ['pip']
pip_args.append('install')
pip_args.append('--use-wheel')
for req in requirements:
pip_args.append(req)
logger.info('Installing requirements: ' + str(requirements))
process = subprocess.Popen(pip_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
log_subprocess_output(process)
exitcode = process.wait()
if exitcode != 0:
raise Error("Dependencies not properly installed")
@classmethod
def _load_module(cls, name, root):
sys.path.append(root)
tmp = importlib.import_module(name)
sys.path.remove(root)
return tmp
@classmethod
def _load_plugin_from_info(cls, info, root):
if not cls.validate_info(info):
logger.warn('The module info is not valid.\n\t{}'.format(info))
return None, None
module = info["module"]
name = info["name"]
cls._install_deps(info)
tmp = cls._load_module(module, root)
candidate = None
for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule(obj)))
candidate = obj
break
if not candidate:
logger.debug("No valid plugin for: {}".format(module))
return
module = candidate(info=info)
return name, module
@classmethod
def _load_plugin(cls, root, filename):
fpath = os.path.join(root, filename)
logger.debug("Loading plugin: {}".format(fpath))
with open(fpath, 'r') as f:
info = yaml.load(f)
logger.debug("Info: {}".format(info))
return cls._load_plugin_from_info(info, root)
def _load_plugins(self):
plugins = {}
for search_folder in self._search_folders:
for root, dirnames, filenames in os.walk(search_folder):
for filename in fnmatch.filter(filenames, '*.senpy'):
name, plugin = self._load_plugin(root, filename)
if plugin and name:
plugins[name] = plugin
self._outdated = False
return plugins
def teardown(self, exception):
pass
......@@ -415,7 +324,8 @@ class Senpy(object):
def plugins(self):
""" Return the plugins registered for a given application. """
if self._outdated:
self._plugin_list = self._load_plugins()
self._plugin_list = plugins.load_plugins(self._search_folders)
self._outdated = False
return self._plugin_list
def filter_plugins(self, **kwargs):
......
......@@ -218,12 +218,11 @@ class BaseModel(SenpyMixin, dict):
super(BaseModel, self).__init__(temp)
def _get_key(self, key):
if key is 'id':
key = '@id'
key = key.replace("__", ":", 1)
return key
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
......@@ -244,8 +243,6 @@ class BaseModel(SenpyMixin, dict):
def _plain_dict(self):
d = {k: v for (k, v) in self.items() if k[0] != "_"}
if 'id' in d:
d["@id"] = d.pop('id')
return d
......
from future import standard_library
standard_library.install_aliases()
import inspect
import os.path
import os
import pickle
import logging
import tempfile
import copy
from .. import models
import fnmatch
import inspect
import sys
import subprocess
import importlib
import yaml
from .. import models, utils
from ..api import API_PARAMS
logger = logging.getLogger(__name__)
......@@ -37,6 +44,21 @@ class Plugin(models.Plugin):
def deactivate(self):
pass
def test(self):
if not hasattr(self, 'test_cases'):
import inspect
raise AttributeError(('Plugin {} [{}] does not have any defined '
'test cases').format(self.id, inspect.getfile(self.__class__)))
for case in self.test_cases:
res = list(self.analyse_entry(models.Entry(case['entry']),
case['params']))
exp = case['expected']
if not isinstance(exp, list):
exp = [exp]
utils.check_template(res, exp)
for r in res:
r.validate()
SenpyPlugin = Plugin
......@@ -160,3 +182,86 @@ def pfilter(plugins, **kwargs):
if kwargs:
candidates = filter(matches, candidates)
return {p.name: p for p in candidates}
def validate_info(info):
return all(x in info for x in ('name', 'module', 'description', 'version'))
def load_module(name, root):
sys.path.append(root)
tmp = importlib.import_module(name)
sys.path.remove(root)
return tmp
def log_subprocess_output(process):
for line in iter(process.stdout.readline, b''):
logger.info('%r', line)
for line in iter(process.stderr.readline, b''):
logger.error('%r', line)
def install_deps(*plugins):
for info in plugins:
requirements = info.get('requirements', [])
if requirements:
pip_args = ['pip']
pip_args.append('install')
pip_args.append('--use-wheel')
for req in requirements:
pip_args.append(req)
logger.info('Installing requirements: ' + str(requirements))
process = subprocess.Popen(pip_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
log_subprocess_output(process)
exitcode = process.wait()
if exitcode != 0:
raise models.Error("Dependencies not properly installed")
def load_plugin_from_info(info, root, validator=validate_info):
if not validator(info):
logger.warn('The module info is not valid.\n\t{}'.format(info))
return None, None
module = info["module"]
name = info["name"]
install_deps(info)
tmp = load_module(module, root)
candidate = None
for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule(obj)))
candidate = obj
break
if not candidate:
logger.debug("No valid plugin for: {}".format(module))
return
module = candidate(info=info)
return name, module
def load_plugin(root, filename):
fpath = os.path.join(root, filename)
logger.debug("Loading plugin: {}".format(fpath))
with open(fpath, 'r') as f:
info = yaml.load(f)
logger.debug("Info: {}".format(info))
return load_plugin_from_info(info, root)
def load_plugins(folders, loader=load_plugin):
plugins = {}
for search_folder in folders:
for root, dirnames, filenames in os.walk(search_folder):
# Do not look for plugins in hidden or special folders
dirnames[:] = [d for d in dirnames if d[0] not in ['.', '_']]
for filename in fnmatch.filter(filenames, '*.senpy'):
name, plugin = loader(root, filename)
if plugin and name:
plugins[name] = plugin
return plugins
......@@ -100,3 +100,54 @@ class CentroidConversion(EmotionConversionPlugin):
else:
raise Error('EMOTION MODEL NOT KNOWN')
yield e
def test(self, info=None):
if not info:
info = {
"name": "CentroidTest",
"description": "Centroid test",
"version": 0,
"centroids": {
"c1": {"V1": 0.5,
"V2": 0.5},
"c2": {"V1": -0.5,
"V2": 0.5},
"c3": {"V1": -0.5,
"V2": -0.5},
"c4": {"V1": 0.5,
"V2": -0.5}},
"aliases": {
"V1": "X-dimension",
"V2": "Y-dimension"
},
"centroids_direction": ["emoml:big6", "emoml:fsre-dimensions"]
}
c = CentroidConversion(info)
es1 = EmotionSet()
e1 = Emotion()
e1.onyx__hasEmotionCategory = "c1"
es1.onyx__hasEmotion.append(e1)
res = c._forward_conversion(es1)
assert res["X-dimension"] == 0.5
assert res["Y-dimension"] == 0.5
e2 = Emotion()
e2.onyx__hasEmotionCategory = "c2"
es1.onyx__hasEmotion.append(e2)
res = c._forward_conversion(es1)
assert res["X-dimension"] == 0
assert res["Y-dimension"] == 1
e = Emotion()
e["X-dimension"] = -0.2
e["Y-dimension"] = -0.3
res = c._backwards_conversion(e)
assert res["onyx:hasEmotionCategory"] == "c3"
e = Emotion()
e["X-dimension"] = -0.2
e["Y-dimension"] = 0.3
res = c._backwards_conversion(e)
assert res["onyx:hasEmotionCategory"] == "c2"
import random
from senpy.plugins import EmotionPlugin
from senpy.models import EmotionSet, Emotion
from senpy.models import EmotionSet, Emotion, Entry
class RmoRandPlugin(EmotionPlugin):
......@@ -16,3 +16,11 @@ class RmoRandPlugin(EmotionPlugin):
emotionSet.prov__wasGeneratedBy = self.id
entry.emotions.append(emotionSet)
yield entry
def test(self):
params = dict()
results = list()
for i in range(100):
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
res.validate()
results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory'])
import random
from senpy.plugins import SentimentPlugin
from senpy.models import Sentiment
from senpy.models import Sentiment, Entry
class RandPlugin(SentimentPlugin):
......@@ -22,3 +22,13 @@ class RandPlugin(SentimentPlugin):
entry.sentiments.append(sentiment)
entry.language = lang
yield entry
def test(self):
params = dict()
results = list()
for i in range(100):
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
res.validate()
results.append(res.sentiments[0]['marl:hasPolarity'])
assert 'marl:Positive' in results
assert 'marl:Negative' in results
......@@ -12,7 +12,7 @@ class Sentiment140Plugin(SentimentPlugin):
json.dumps({
"language": lang,
"data": [{
"text": entry.text
"text": entry.nif__isString
}]
}))
p = params.get("prefix", None)
......@@ -34,3 +34,20 @@ class Sentiment140Plugin(SentimentPlugin):
entry.sentiments.append(sentiment)
entry.language = lang
yield entry
test_cases = [
{
'entry': {
'nif:isString': 'I love Titanic'
},
'params': {},
'expected': {
"nif:isString": "I love Titanic",
'sentiments': [
{
'marl:hasPolarity': 'marl:Positive',
}
]
}
}
]
......@@ -20,6 +20,9 @@
"@id": "me:hasSuggestions",
"@container": "@set"
},
"onyx:hasEmotion": {
"@container": "@set"
},
"emotions": {
"@id": "onyx:hasEmotionSet",
"@container": "@set"
......
from . import models
def check_template(indict, template):
if isinstance(template, dict) and isinstance(indict, dict):
for k, v in template.items():
if k not in indict:
return '{} not in {}'.format(k, indict)
check_template(indict[k], v)
elif isinstance(template, list) and isinstance(indict, list):
if len(indict) != len(template):
raise models.Error('Different size for {} and {}'.format(indict, template))
for e in template:
found = False
for i in indict:
try:
check_template(i, e)
found = True
except models.Error as ex:
continue
if not found:
raise models.Error('{} not found in {}'.format(e, indict))
else:
if indict != template:
raise models.Error('{} and {} are different'.format(indict, template))
......@@ -21,3 +21,6 @@ class AsyncPlugin(AnalysisPlugin):
values = self._do_async(2)
entry.async_values = values
yield entry
def test(self):
pass
......@@ -6,3 +6,6 @@ class DummyPlugin(SentimentPlugin):
entry.text = entry.text[::-1]
entry.reversed = entry.get('reversed', 0) + 1
yield entry
def test(self):
pass
......@@ -9,3 +9,6 @@ class SleepPlugin(AnalysisPlugin):
def analyse_entry(self, entry, params):
sleep(float(params.get("timeout", self.timeout)))
yield entry
def test(self):
pass
......@@ -17,17 +17,19 @@ def parse_resp(resp):
class BlueprintsTest(TestCase):
def setUp(self):
self.app = Flask("test_extensions")
self.app.debug = False
self.client = self.app.test_client()
self.senpy = Senpy()
self.senpy.init_app(self.app)
self.dir = os.path.join(os.path.dirname(__file__), "..")
self.senpy.add_folder(self.dir)
self.senpy.activate_plugin("Dummy", sync=True)
self.senpy.activate_plugin("DummyRequired", sync=True)
self.senpy.default_plugin = 'Dummy'
@classmethod
def setUpClass(cls):
"""Set up only once, and re-use in every individual test"""
cls.app = Flask("test_extensions")
cls.app.debug = False
cls.client = cls.app.test_client()
cls.senpy = Senpy()
cls.senpy.init_app(cls.app)
cls.dir = os.path.join(os.path.dirname(__file__), "..")
cls.senpy.add_folder(cls.dir)
cls.senpy.activate_plugin("Dummy", sync=True)
cls.senpy.activate_plugin("DummyRequired", sync=True)
cls.senpy.default_plugin = 'Dummy'
def assertCode(self, resp, code):
self.assertEqual(resp.status_code, code)
......
......@@ -10,6 +10,7 @@ except ImportError:
from functools import partial
from senpy.extensions import Senpy
from senpy import plugins
from senpy.models import Error, Results, Entry, EmotionSet, Emotion, Plugin
from flask import Flask
from unittest import TestCase
......@@ -18,7 +19,7 @@ from unittest import TestCase
class ExtensionsTest(TestCase):
def setUp(self):
self.app = Flask('test_extensions')
self.dir = os.path.join(os.path.dirname(__file__))
self.dir = os.path.dirname(__file__)
self.senpy = Senpy(plugin_folder=self.dir,
app=self.app,
default_plugins=False)
......@@ -38,8 +39,8 @@ class ExtensionsTest(TestCase):
print(self.senpy.plugins)
assert "Dummy" in self.senpy.plugins
def test_enabling(self):
""" Enabling a plugin """
def test_installing(self):
""" Installing a plugin """
info = {
'name': 'TestPip',
'module': 'dummy',
......@@ -48,14 +49,13 @@ class ExtensionsTest(TestCase):
'version': 0
}
root = os.path.join(self.dir, 'plugins', 'dummy_plugin')
name, module = self.senpy._load_plugin_from_info(info, root=root)
name, module = plugins.load_plugin_from_info(info, root=root)
assert name == 'TestPip'
assert module
import noop
dir(noop)
self.senpy.install_deps()
def test_installing(self):
def test_enabling(self):
""" Enabling a plugin """
self.senpy.activate_all(sync=True)
assert len(self.senpy.plugins) >= 3
......@@ -72,7 +72,7 @@ class ExtensionsTest(TestCase):
}
root = os.path.join(self.dir, 'plugins', 'dummy_plugin')
with self.assertRaises(Error):
name, module = self.senpy._load_plugin_from_info(info, root=root)
name, module = plugins.load_plugin_from_info(info, root=root)
def test_disabling(self):
""" Disabling a plugin """
......@@ -173,7 +173,7 @@ class ExtensionsTest(TestCase):
'onyx:usesEmotionModel': 'emoml:fsre-dimensions'
})
eSet1 = EmotionSet()
eSet1.prov__wasGeneratedBy = plugin['id']
eSet1.prov__wasGeneratedBy = plugin['@id']
eSet1['onyx:hasEmotion'].append(Emotion({
'emoml:arousal': 1,
'emoml:potency': 0,
......
......@@ -7,11 +7,11 @@ import tempfile
from unittest import TestCase
from senpy.models import Results, Entry, EmotionSet, Emotion
from senpy.plugins import SentimentPlugin, ShelfMixin
from senpy import plugins
from senpy.plugins.conversion.emotion.centroids import CentroidConversion
class ShelfDummyPlugin(SentimentPlugin, ShelfMixin):
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
def activate(self, *args, **kwargs):
if 'counter' not in self.sh:
self.sh['counter'] = 0
......@@ -33,7 +33,6 @@ class PluginsTest(TestCase):
def tearDown(self):
if os.path.exists(self.shelf_dir):
shutil.rmtree(self.shelf_dir)
if os.path.isfile(self.shelf_file):
os.remove(self.shelf_file)
......@@ -51,26 +50,29 @@ class PluginsTest(TestCase):
def test_shelf(self):
''' A shelf is created and the value is stored '''
newfile = self.shelf_file + "new"
a = ShelfDummyPlugin(info={
'name': 'shelve',
'version': 'test',
'shelf_file': self.shelf_file
'shelf_file': newfile
})
assert a.sh == {}