Commit 0b937da3 by Gabe Mulley

support loading backends

parent b5ca901f
......@@ -11,14 +11,18 @@ from mock import patch
from mock import sentinel
from eventtracking import track
from eventtracking.backends import BaseBackend
class TestTrack(TestCase): # pylint: disable=missing-docstring
def setUp(self):
self._mock_backend = MagicMock()
self.addCleanup(track.BACKENDS.remove, self._mock_backend)
track.BACKENDS.append(self._mock_backend)
# Ensure all backends are removed after executing
self.addCleanup(track.configure, {})
self._mock_backends = []
self._mock_backend = None
self.configure_mock_backends(1)
self._expected_timestamp = datetime.utcnow()
self._datetime_patcher = patch('eventtracking.track.datetime')
......@@ -26,6 +30,28 @@ class TestTrack(TestCase): # pylint: disable=missing-docstring
mock_datetime = self._datetime_patcher.start()
mock_datetime.utcnow.return_value = self._expected_timestamp # pylint: disable=maybe-no-member
def configure_mock_backends(self, number_of_mocks):
"""Ensure the tracking module has the requisite number of mock backends"""
config = {}
for i in range(number_of_mocks):
name = 'mock{0}'.format(i)
config[name] = {
'ENGINE': 'eventtracking.tests.test_track.TrivialFakeBackend'
}
track.configure(config)
self._mock_backends = []
for i in range(number_of_mocks):
backend = self.get_mock_backend(i)
backend.send = MagicMock()
self._mock_backends.append(backend)
self._mock_backend = self._mock_backends[0]
def get_mock_backend(self, index):
"""Get the mock backend created by `configure_mock_backends`"""
return track.BACKENDS['mock{0}'.format(index)]
def test_event_simple_event_without_data(self):
track.event(sentinel.event_type)
......@@ -36,7 +62,7 @@ class TestTrack(TestCase): # pylint: disable=missing-docstring
if not backend:
backend = self._mock_backend
backend.event.assert_called_once_with(
backend.send.assert_called_once_with(
{
'event_type': event_type,
'timestamp': self._expected_timestamp,
......@@ -60,26 +86,149 @@ class TestTrack(TestCase): # pylint: disable=missing-docstring
)
def test_multiple_backends(self):
another_backend = MagicMock()
track.BACKENDS.append(another_backend)
try:
track.event(sentinel.event_type)
self.configure_mock_backends(2)
track.event(sentinel.event_type)
self.assert_backend_called_with(sentinel.event_type)
for backend in self._mock_backends:
self.assert_backend_called_with(
sentinel.event_type, backend=another_backend)
finally:
track.BACKENDS.remove(another_backend)
sentinel.event_type, backend=backend)
def test_single_backend_failure(self):
self._mock_backend.event.side_effect = Exception
self.configure_mock_backends(2)
self.get_mock_backend(0).send.side_effect = Exception
another_backend = MagicMock()
track.BACKENDS.append(another_backend)
track.event(sentinel.event_type)
self.assert_backend_called_with(
sentinel.event_type, backend=self.get_mock_backend(1))
def test_configure(self):
track.configure({
"fake": {
'ENGINE': 'eventtracking.tests.test_track.TrivialFakeBackend'
}
})
fake_backend = track.BACKENDS['fake']
self.assertTrue(isinstance(fake_backend, TrivialFakeBackend))
def test_ignore_no_engine(self):
track.configure({
"no_engine": {
'OPTIONS': {}
}
})
self.assertEquals(len(track.BACKENDS), 0)
def test_configure_empty_engine(self):
try:
track.configure({
"empty_engine": {
'ENGINE': ''
}
})
self.fail('Expected exception to be thrown when attempting to add a backend with an empty engine')
except ValueError:
pass
def test_configure_invalid_package(self):
try:
track.event(sentinel.event_type)
track.configure({
"invalid_package": {
'ENGINE': 'foo.BarBackend'
}
})
self.fail('Expected exception to be thrown when attempting to add a backend from a non-existent package')
except ValueError:
pass
def test_configure_no_package_invalid_class(self):
try:
track.configure({
"no_package_invalid_class": {
'ENGINE': 'BarBackend'
}
})
self.fail('Expected exception to be thrown when attempting to add a non-existent backend class')
except ValueError:
pass
def test_configure_invalid_class(self):
try:
track.configure({
"invalid_class": {
'ENGINE': 'eventtracking.tests.test_track.BarBackend'
}
})
self.fail('Expected exception to be thrown when attempting to add a non-existent backend class')
except ValueError:
pass
def test_configure_engine_not_a_backend(self):
try:
track.configure({
"not_a_backend": {
'ENGINE': 'eventtracking.tests.test_track.NotABackend'
}
})
self.fail(
'Expected exception to be thrown when attempting to add a backend class that'
' does not subclass BaseBackend'
)
except ValueError:
pass
def test_configure_engine_with_options(self):
track.configure({
'with_options': {
'ENGINE': 'eventtracking.tests.test_track.FakeBackendWithOptions',
'OPTIONS': {
'option': sentinel.option_value
}
}
})
self.assertEquals(track.BACKENDS['with_options'].option, sentinel.option_value)
self.assert_backend_called_with(
sentinel.event_type, backend=another_backend)
finally:
track.BACKENDS.remove(another_backend)
def test_configure_engine_missing_options(self):
track.configure({
'without_options': {
'ENGINE': 'eventtracking.tests.test_track.FakeBackendWithOptions'
}
})
self.assertEquals(track.BACKENDS['without_options'].option, None)
def test_configure_engine_with_extra_options(self):
track.configure({
'extra_options': {
'ENGINE': 'eventtracking.tests.test_track.FakeBackendWithOptions',
'OPTIONS': {
'option': sentinel.option_value,
'extra_option': sentinel.extra_option_value
}
}
})
self.assertEquals(track.BACKENDS['extra_options'].option, sentinel.option_value)
class TrivialFakeBackend(BaseBackend):
"""A trivial fake backend without any options"""
def send(self, event):
pass
class NotABackend(object):
"""
A class that is not a backend
"""
pass
class FakeBackendWithOptions(BaseBackend):
"""A trivial fake backend with options"""
def __init__(self, **kwargs):
super(FakeBackendWithOptions, self).__init__()
self.option = kwargs.get('option', None)
def send(self, event):
pass
......@@ -16,11 +16,77 @@ Best Practices:
from __future__ import absolute_import
from datetime import datetime
from importlib import import_module
import inspect
import logging
from eventtracking.backends import BaseBackend
__all__ = ['configure', 'event']
LOG = logging.getLogger(__name__)
BACKENDS = []
BACKENDS = {}
def configure(config):
"""
Configure event tracking. `config` is expected to be a dictionary of backend engines.
Example::
config = {
'default': {
'ENGINE': 'some.arbitrary.Backend',
'OPTIONS': {
'endpoint': 'http://something/event'
}
},
'anoter_engine': {
'ENGINE': 'some.arbitrary.OtherBackend',
'OPTIONS': {
'user': 'foo'
}
},
}
"""
BACKENDS.clear()
for name, values in config.iteritems():
# Ignore empty values to turn-off default tracker backends
if values and 'ENGINE' in values:
engine = values['ENGINE']
options = values.get('OPTIONS', {})
BACKENDS[name] = _instantiate_backend_from_name(engine, options)
def _instantiate_backend_from_name(name, options):
"""
Instantiate an event tracker backend from the full module path to
the backend class. Useful when setting backends from configuration
files.
"""
# Parse backend name
parts = name.split('.')
module_name = '.'.join(parts[:-1])
class_name = parts[-1]
# Get and verify the backend class
try:
module = import_module(module_name)
cls = getattr(module, class_name)
if not inspect.isclass(cls) or not issubclass(cls, BaseBackend):
raise TypeError
except (ValueError, AttributeError, TypeError, ImportError):
raise ValueError('Cannot find event track backend %s' % name)
backend = cls(**options)
return backend
def event(event_type, data=None):
......@@ -38,10 +104,10 @@ def event(event_type, data=None):
'data': data or {}
}
for backend in BACKENDS:
for name, backend in BACKENDS.iteritems():
try:
backend.event(full_event)
except Exception: # pylint: disable=W0703
backend.send(full_event)
except Exception: # pylint: disable=broad-except
LOG.exception(
'Unable to commit event to backend: {0}'.format(backend)
'Unable to send event to backend: {0}'.format(name)
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment