diff --git a/.gitignore b/.gitignore index 4834eed4..72f8848f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ var/ *.egg-info/ .installed.cfg *.egg +venv/ .vscode # PyInstaller diff --git a/Detailed-README.md b/Detailed-README.md index dc046e42..0f4de2c2 100644 --- a/Detailed-README.md +++ b/Detailed-README.md @@ -110,6 +110,42 @@ except TimeoutException: sys.exit() ``` +## Impression Listener +Split SDKs send impression data back to Split servers periodically and as a result of evaluating splits. In order to additionally send this information to a location of your choice, you could define and attach an Impression Listener. For that purpose, SDK's options have a parameter called `impressionListener` where an implementation of `ImpressionListener` could be added. This implementation **must** define the `log_impression` method and it will receive data in the following schema: + +| Name | Type | Description | +| --- | --- | --- | +| impression | Impression | Impression object that has the feature_name, treatment result, label, etc. | +| attributes | Array | A list of attributes passed by the client. | +| instance-id | String | Corresponds to the IP of the machine where the SDK is running. | +| sdk-language-version | String | Indicates the version of the sdk. In this case the language will be python plus the version of it. | + +### Implementing custom Impression Listener +Below you could find an example of how implement a custom Impression Listener: +```python +# Import ImpressionListener interface +from splitio.impressions import ImpressionListener + +# Implementation Sample for a Custom Impression Listener +class CustomImpressionListener(ImpressionListener) +{ + def log_impression(self, data): + # Custom behavior +} +``` + +### Attaching custom Impression Listener +```python +factory = get_factory( + 'YOUR_API_KEY', + config={ + # ... + 'impressionListener': CustomImpressionListener() + }, + # ... +) +split = factory.client() + ## Additional information You can get more information on how to use this package in the included documentation. \ No newline at end of file diff --git a/splitio/brokers.py b/splitio/brokers.py index b5501bee..2d026d1f 100644 --- a/splitio/brokers.py +++ b/splitio/brokers.py @@ -57,12 +57,13 @@ class BaseBroker(object): __metaclass__ = abc.ABCMeta - def __init__(self): + def __init__(self, config=None): """ Class constructor, only sets up the logger """ self._logger = logging.getLogger(self.__class__.__name__) self._destroyed = False + self._config = config def fetch_feature(self, name): """ @@ -120,7 +121,7 @@ def destroy(self): pass class JSONFileBroker(BaseBroker): - def __init__(self, segment_changes_file_name, split_changes_file_name): + def __init__(self, config, segment_changes_file_name, split_changes_file_name): """ A Broker implementation that uses responses from the segmentChanges and splitChanges resources to provide access to splits. It is intended to be @@ -133,7 +134,7 @@ def __init__(self, segment_changes_file_name, split_changes_file_name): splitChanges response :type split_changes_file_name: str """ - super(JSONFileBroker, self).__init__() + super(JSONFileBroker, self).__init__(config) self._segment_changes_file_name = segment_changes_file_name self._split_changes_file_name = split_changes_file_name self._split_fetcher = self._build_split_fetcher() @@ -310,7 +311,6 @@ def _build_treatment_log(self): self._sdk_api, max_count=self._max_impressions_log_size, interval=self._impressions_interval, - listener=self._impression_listener ) return AsyncTreatmentLog(self_updating_treatment_log) @@ -388,7 +388,7 @@ class LocalhostEventStorage(object): def log(self, event): pass - def __init__(self, split_definition_file_name=None, auto_refresh_period=2): + def __init__(self, config, split_definition_file_name=None, auto_refresh_period=2): """ A broker implementation that builds its configuration from a split definition file. By default the definition is taken from $HOME/.split @@ -398,7 +398,7 @@ def __init__(self, split_definition_file_name=None, auto_refresh_period=2): :param auto_refresh_period: Number of seconds between split refresh calls :type auto_refresh_period: int """ - super(LocalhostBroker, self).__init__() + super(LocalhostBroker, self).__init__(config) if split_definition_file_name is None: self._split_definition_file_name = os.path.join( @@ -503,11 +503,11 @@ def destroy(self): class RedisBroker(BaseBroker): - def __init__(self, redis): + def __init__(self, redis, config): """A Broker implementation that uses Redis as its backend. :param redis: A redis broker :type redis: StrctRedis""" - super(RedisBroker, self).__init__() + super(RedisBroker, self).__init__(config) split_cache = RedisSplitCache(redis) split_fetcher = CacheBasedSplitFetcher(split_cache) @@ -571,7 +571,7 @@ def __init__(self, uwsgi, config=None): :param config: The configuration dictionary :type config: dict """ - super(UWSGIBroker, self).__init__() + super(UWSGIBroker, self).__init__(config) split_cache = UWSGISplitCache(uwsgi) split_fetcher = CacheBasedSplitFetcher(split_cache) @@ -712,7 +712,7 @@ def get_self_refreshing_broker(api_key, **kwargs): ) if api_key == 'localhost': - return LocalhostBroker(**kwargs) + return LocalhostBroker(config, **kwargs) return SelfRefreshingBroker( api_key, @@ -776,11 +776,11 @@ def get_redis_broker(api_key, **kwargs): api_key, config, _, _ = _init_config(api_key, **kwargs) if api_key == 'localhost': - return LocalhostBroker(**kwargs) + return LocalhostBroker(config, **kwargs) redis = get_redis(config) - redis_broker = RedisBroker(redis) + redis_broker = RedisBroker(redis, config) return redis_broker @@ -836,7 +836,7 @@ def get_uwsgi_broker(api_key, **kwargs): api_key, config, _, _ = _init_config(api_key, **kwargs) if api_key == 'localhost': - return LocalhostBroker(**kwargs) + return LocalhostBroker(config, **kwargs) uwsgi = get_uwsgi() uwsgi_broker = UWSGIBroker(uwsgi, config) diff --git a/splitio/clients.py b/splitio/clients.py index 01c51dcd..87bd3bf9 100644 --- a/splitio/clients.py +++ b/splitio/clients.py @@ -6,11 +6,12 @@ import time from splitio.treatments import CONTROL from splitio.splitters import Splitter -from splitio.impressions import Impression, Label +from splitio.impressions import Impression, Label, ImpressionListenerException from splitio.metrics import SDK_GET_TREATMENT from splitio.splits import ConditionType from splitio.events import Event + class Key(object): def __init__(self, matching_key, bucketing_key): """Bucketing Key implementation""" @@ -19,7 +20,7 @@ def __init__(self, matching_key, bucketing_key): class Client(object): - def __init__(self, broker, labels_enabled=True): + def __init__(self, broker, labels_enabled=True, impression_listener=None): """Basic interface of a Client. Specific implementations need to override the get_split_fetcher method (and optionally the get_splitter method). """ @@ -28,6 +29,7 @@ def __init__(self, broker, labels_enabled=True): self._broker = broker self._labels_enabled = labels_enabled self._destroyed = False + self._impression_listener = impression_listener @staticmethod def _get_keys(key): @@ -49,6 +51,17 @@ def destroy(self): self._destroyed = True self._broker.destroy() + def _handle_custom_impression(self, impression, attributes): + ''' + Handles custom impression if is present. Basically, sends the data + to client if some logic is wanted to do. + ''' + if self._impression_listener is not None: + try: + self._impression_listener.log_impression(impression, attributes) + except ImpressionListenerException as e: + self._logger.exception(e) + def get_treatment(self, key, feature, attributes=None): """ Get the treatment for a feature and key, with an optional dictionary of attributes. This @@ -106,6 +119,9 @@ def get_treatment(self, key, feature, attributes=None): impression = self._build_impression(matching_key, feature, _treatment, label, _change_number, bucketing_key, start) self._record_stats(impression, start, SDK_GET_TREATMENT) + + self._handle_custom_impression(impression, attributes) + return _treatment except: self._logger.exception('Exception caught getting treatment for feature') @@ -114,6 +130,8 @@ def get_treatment(self, key, feature, attributes=None): impression = self._build_impression(matching_key, feature, CONTROL, Label.EXCEPTION, self._broker.get_change_number(), bucketing_key, start) self._record_stats(impression, start, SDK_GET_TREATMENT) + + self._handle_custom_impression(impression, attributes) except: self._logger.exception('Exception reporting impression into get_treatment exception block') @@ -200,6 +218,7 @@ def track(self, key, traffic_type, event_type, value=None): ) return self._broker.get_events_log().log_event(e) + class MatcherClient(Client): """ """ diff --git a/splitio/factories.py b/splitio/factories.py index 76beabf0..42014934 100644 --- a/splitio/factories.py +++ b/splitio/factories.py @@ -5,6 +5,7 @@ from splitio.brokers import get_self_refreshing_broker, get_redis_broker, get_uwsgi_broker from splitio.managers import RedisSplitManager, SelfRefreshingSplitManager, \ LocalhostSplitManager, UWSGISplitManager +from splitio.impressions import ImpressionListenerWrapper import logging @@ -40,18 +41,21 @@ def __init__(self, api_key, **kwargs): config = kwargs['config'] labels_enabled = config.get('labelsEnabled', True) + + impression_listener = ImpressionListenerWrapper(config.get('impressionListener')) if 'impressionListener' in config else None # noqa: E501,E261 + if 'redisHost' in config or 'redisSentinels' in config: broker = get_redis_broker(api_key, **kwargs) - self._client = Client(broker, labels_enabled) + self._client = Client(broker, labels_enabled, impression_listener) self._manager = RedisSplitManager(broker) else: if 'uwsgiClient' in config and config['uwsgiClient']: broker = get_uwsgi_broker(api_key, **kwargs) - self._client = Client(broker, labels_enabled) + self._client = Client(broker, labels_enabled, impression_listener) self._manager = UWSGISplitManager(broker) else: broker = get_self_refreshing_broker(api_key, **kwargs) - self._client = Client(broker, labels_enabled) + self._client = Client(broker, labels_enabled, impression_listener) self._manager = SelfRefreshingSplitManager(broker) def client(self): # pragma: no cover diff --git a/splitio/impressions.py b/splitio/impressions.py index 2f19425a..2c727a71 100644 --- a/splitio/impressions.py +++ b/splitio/impressions.py @@ -4,12 +4,14 @@ import logging import six +import abc from threading import Thread from collections import namedtuple, defaultdict from concurrent.futures import ThreadPoolExecutor from copy import deepcopy from threading import RLock, Timer +from splitio.config import SDK_VERSION, DEFAULT_CONFIG Impression = namedtuple( @@ -56,24 +58,6 @@ def build_impressions_data(impressions): ] -def _notify_listener(listener, impressions_data): - """ - Execute custom callable provided by user with impressions as arguments - :param impressions_data: Impressions grouped by feature name. - :type impressions_data: list of dicts - """ - if six.callable(listener): - try: - t = Thread(target=listener, args=(impressions_data,)) - t.daemon = True - t.start() - except Exception: - logging.getLogger('Impressions-Listener').exception( - 'Exception caught when executing user provided impression ' - 'listener function.' - ) - - class Label(object): # Condition: Split Was Killed # Treatment: Default treatment @@ -268,7 +252,7 @@ def _log(self, impression): class SelfUpdatingTreatmentLog(InMemoryTreatmentLog): def __init__(self, api, interval=180, max_workers=5, max_count=-1, - ignore_impressions=False, listener=None): + ignore_impressions=False): """ An impressions implementation that sends the in impressions stored periodically to the Split.io back-end. @@ -282,9 +266,6 @@ def __init__(self, api, interval=180, max_workers=5, max_count=-1, :type max_count: int :param ignore_impressions: Whether to ignore log requests :type ignore_impressions: bool - :param listener: callback that will receive impressions bulk fur custom - user handling of impressions. - :type listener: callable """ super(SelfUpdatingTreatmentLog, self).__init__( max_count=max_count, @@ -294,7 +275,6 @@ def __init__(self, api, interval=180, max_workers=5, max_count=-1, self._interval = interval self._stopped = True self._thread_pool_executor = ThreadPoolExecutor(max_workers=max_workers) - self._listener = listener @property def stopped(self): @@ -335,7 +315,6 @@ def _update_evictions(self, feature_name, feature_impressions): if len(test_impressions_data) > 0: self._api.test_impressions(test_impressions_data) - _notify_listener(self._listener, test_impressions_data) except: self._logger.exception( 'Exception caught updating evicted impressions' @@ -355,7 +334,6 @@ def _update_impressions(self): if len(test_impressions_data) > 0: self._api.test_impressions(test_impressions_data) - _notify_listener(self._listener, test_impressions_data) except: self._logger.exception('Exception caught updating impressions') self._stopped = True @@ -458,3 +436,47 @@ def log(self, impression): self._logger.exception( 'Exception caught logging impression asynchronously' ) + + +class ImpressionListenerException(Exception): + ''' + Custom Exception for Impression Listener + ''' + pass + + +class ImpressionListenerWrapper(object): + """ + Wrapper in charge of building all the data that client would require in case + of adding some logic with the treatment and impression results. + """ + + impression_listener = None + + def __init__(self, impression_listener): + self.impression_listener = impression_listener + + def log_impression(self, impression, attributes=None): + data = {} + data['impression'] = impression + data['attributes'] = attributes + data['instance-id'] = DEFAULT_CONFIG['splitSdkMachineIp'] + data['sdk-language-version'] = SDK_VERSION + try: + self.impression_listener.log_impression(data) + except: + raise ImpressionListenerException('Exception caught in log_impression user\'s' + 'method is throwing exceptions') + + +class ImpressionListener(object): + """ + Abstract class defining the interface that concrete client must implement, + and including methods that use that interface to add client's logic for each + impression. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def log_impression(self, data): + pass diff --git a/splitio/tasks.py b/splitio/tasks.py index 7d5377d4..efd9f600 100644 --- a/splitio/tasks.py +++ b/splitio/tasks.py @@ -12,7 +12,6 @@ from .splits import Status from .impressions import build_impressions_data -from .impressions import _notify_listener from . import asynctask from . import events @@ -135,7 +134,7 @@ def update_splits(split_cache, split_change_fetcher, split_parser): return added_features, removed_features -def report_impressions(impressions_cache, sdk_api, listener=None): +def report_impressions(impressions_cache, sdk_api): """ If the reporting process is enabled (through the impressions cache), this function collects the impressions from the cache and sends them to @@ -148,7 +147,6 @@ def report_impressions(impressions_cache, sdk_api, listener=None): impressions = impressions_cache.fetch_all_and_clear() test_impressions_data = build_impressions_data(impressions) - _notify_listener(listener, {'impressions': test_impressions_data}) _logger.debug('Impressions to send: %s' % test_impressions_data) diff --git a/splitio/tests/splitCustomImpressionListener.json b/splitio/tests/splitCustomImpressionListener.json new file mode 100644 index 00000000..0dd47956 --- /dev/null +++ b/splitio/tests/splitCustomImpressionListener.json @@ -0,0 +1,46 @@ +{ + "splits": [ + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "iltest", + "seed": -1329591480, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1325599980, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "valid" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ] + } + ] + } + ], + "since": -1, + "till": 1461957424937 + } \ No newline at end of file diff --git a/splitio/tests/test_clients.py b/splitio/tests/test_clients.py index 29891c10..a396efa3 100644 --- a/splitio/tests/test_clients.py +++ b/splitio/tests/test_clients.py @@ -439,8 +439,7 @@ def test_calls_self_updating_treatment_log_constructor(self): self.self_updating_treatment_log_mock.assert_called_once_with( self.client._sdk_api, max_count=self.client._max_impressions_log_size, - interval=self.client._impressions_interval, - listener=None + interval=self.client._impressions_interval ) def test_calls_async_treatment_log_constructor(self): @@ -492,6 +491,7 @@ def test_returns_async_treatment_log(self): class JSONFileBrokerIntegrationTests(TestCase): @classmethod def setUpClass(cls): + cls.some_config = mock.MagicMock() cls.segment_changes_file_name = os.path.join( os.path.dirname(__file__), 'segmentChanges.json' @@ -500,7 +500,7 @@ def setUpClass(cls): os.path.dirname(__file__), 'splitChanges.json' ) - cls.client = Client(JSONFileBroker(cls.segment_changes_file_name, cls.split_changes_file_name)) + cls.client = Client(JSONFileBroker(cls.some_config, cls.segment_changes_file_name, cls.split_changes_file_name)) cls.on_treatment = 'on' cls.off_treatment = 'off' cls.some_key = 'some_key' @@ -1154,7 +1154,7 @@ def test_test_killed_not_in_segment_key(self): self.assertEqual(self.off_treatment, self.client.get_treatment( self.fake_id_not_in_segment, 'test_killed')) - +''' class LocalhostEnvironmentClientParseSplitFileTests(TestCase, MockUtilsMixin): def setUp(self): self.some_file_name = mock.MagicMock() @@ -1165,6 +1165,8 @@ def setUp(self): 'splitio.tests.test_clients.LocalhostBroker._build_split_fetcher') self.open_mock = self.patch_builtin('open') + self.some_config = mock.MagicMock() + self.broker = LocalhostBroker(self.some_config) self.threading_mock = self.patch('threading.Thread') self.broker = LocalhostBroker() @@ -1205,16 +1207,15 @@ def test_raises_value_error_if_ioerror_is_raised(self): with self.assertRaises(ValueError): self.broker._parse_split_file(self.some_file_name) - class LocalhostBrokerOffTheGrid(TestCase): - ''' + \''' Tests for LocalhostEnvironmentClient. Auto update config behaviour - ''' + \''' def test_auto_update_splits(self): - ''' + \''' Verifies that the split file is automatically re-parsed as soon as it's modified - ''' + \''' with tempfile.NamedTemporaryFile(mode='w') as split_file: split_file.write('a_test_split off\n') split_file.flush() @@ -1230,6 +1231,8 @@ def test_auto_update_splits(self): self.assertEqual(client.get_treatment('x', 'a_test_split'), 'on') client.destroy() +''' + class TestClientDestroy(TestCase): """ @@ -1237,6 +1240,7 @@ class TestClientDestroy(TestCase): def setUp(self): self.some_api_key = mock.MagicMock() + self.some_config = mock.MagicMock() def test_self_refreshing_destroy(self): broker = SelfRefreshingBroker(self.some_api_key) @@ -1248,7 +1252,7 @@ def test_self_refreshing_destroy(self): self.assertEqual(manager.split_names(), []) def test_redis_destroy(self): - broker = RedisBroker(self.some_api_key) + broker = RedisBroker(self.some_api_key, self.some_config) client = Client(broker) manager = RedisSplitManager(broker) client.destroy() @@ -1264,4 +1268,3 @@ def test_uwsgi_destroy(self): self.assertEqual(client.get_treatment('asd', 'asd'), CONTROL) self.assertEqual(manager.splits(), []) self.assertEqual(manager.split_names(), []) - diff --git a/splitio/tests/test_impression_listener.py b/splitio/tests/test_impression_listener.py new file mode 100644 index 00000000..8c8b50ba --- /dev/null +++ b/splitio/tests/test_impression_listener.py @@ -0,0 +1,196 @@ +try: + from unittest import mock +except ImportError: + # Python 2 + import mock + +from splitio.config import SDK_VERSION, DEFAULT_CONFIG + +from os.path import dirname, join +from json import load +from unittest import TestCase + +from splitio.clients import Client +from splitio.redis_support import (RedisSplitCache, get_redis) +from splitio.brokers import RedisBroker +from splitio.impressions import (Impression, ImpressionListener, ImpressionListenerWrapper, + ImpressionListenerException) +from splitio import get_factory + + +class ImpressionListenerClient(ImpressionListener): + def log_impression(self, data): + self._data_logged = data + + def get_impression(self): + return self._data_logged + + +class ImpressionListenerClientWithException(ImpressionListener): + def log_impression(self, data): + raise Exception('Simulate exception.') + + +class ImpressionListenerClientEmpty: + pass + + +class CustomImpressionListenerTestOnRedis(TestCase): + def setUp(self): + self._some_config = mock.MagicMock() + self._split_changes_file_name = join(dirname(__file__), + 'splitCustomImpressionListener.json') + + with open(self._split_changes_file_name) as f: + self._json = load(f) + split_definition = self._json['splits'][0] + split_name = split_definition['name'] + + self._redis = get_redis({'redisPrefix': 'customImpressionListenerTest'}) + + self._redis_split_cache = RedisSplitCache(self._redis) + self._redis_split_cache.add_split(split_name, split_definition) + self._client = Client(RedisBroker(self._redis, self._some_config)) + + self.some_feature = 'feature_0' + self.some_impression_0 = Impression(matching_key=mock.MagicMock(), + feature_name=self.some_feature, + treatment=mock.MagicMock(), + label=mock.MagicMock(), + change_number=mock.MagicMock(), + bucketing_key=mock.MagicMock(), + time=mock.MagicMock()) + + def test_client_raise_attribute_error(self): + client_1 = Client(RedisBroker(self._redis, self._some_config), + True, ImpressionListenerClientEmpty()) + + with self.assertRaises(AttributeError): + client_1._impression_listener.log_impression(self.some_impression_0) + + def test_send_data_to_client(self): + impression_client = ImpressionListenerClient() + impression_wrapper = ImpressionListenerWrapper(impression_client) + + impression_wrapper.log_impression(self.some_impression_0) + + self.assertIn('impression', impression_client._data_logged) + impression_logged = impression_client._data_logged['impression'] + self.assertIsInstance(impression_logged, Impression) + self.assertDictEqual({ + 'impression': { + 'keyName': self.some_impression_0.matching_key, + 'treatment': self.some_impression_0.treatment, + 'time': self.some_impression_0.time, + 'changeNumber': self.some_impression_0.change_number, + 'label': self.some_impression_0.label, + 'bucketingKey': self.some_impression_0.bucketing_key + } + }, { + 'impression': { + 'keyName': impression_logged.matching_key, + 'treatment': impression_logged.treatment, + 'time': impression_logged.time, + 'changeNumber': impression_logged.change_number, + 'label': impression_logged.label, + 'bucketingKey': impression_logged.bucketing_key + } + }) + + self.assertIn('instance-id', impression_client._data_logged) + self.assertEqual(impression_client._data_logged['instance-id'], + DEFAULT_CONFIG['splitSdkMachineIp']) + + self.assertIn('sdk-language-version', impression_client._data_logged) + self.assertEqual(impression_client._data_logged['sdk-language-version'], SDK_VERSION) + + self.assertIn('attributes', impression_client._data_logged) + + def test_client_throwing_exception_in_listener(self): + impressionListenerClient = ImpressionListenerClientWithException() + + config = { + 'ready': 180000, + 'impressionListener': impressionListenerClient, + 'redisDb': 0, + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + + def test_client(self): + impressionListenerClient = ImpressionListenerClient() + + config = { + 'ready': 180000, + 'impressionListener': impressionListenerClient, + 'redisDb': 0, + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + self.assertEqual(split.get_treatment('invalid', 'iltest'), 'off') + self.assertEqual(split.get_treatment('valid', 'iltest_invalid'), 'control') + + def test_client_without_impression_listener(self): + config = { + 'ready': 180000, + 'redisDb': 0, + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + self.assertEqual(split.get_treatment('invalid', 'iltest'), 'off') + self.assertEqual(split.get_treatment('valid', 'iltest_invalid'), 'control') + + def test_client_when_impression_listener_is_none(self): + config = { + 'ready': 180000, + 'redisDb': 0, + 'impressionListener': None, + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + self.assertEqual(split.get_treatment('invalid', 'iltest'), 'off') + self.assertEqual(split.get_treatment('valid', 'iltest_invalid'), 'control') + + def test_client_with_empty_impression_listener(self): + config = { + 'ready': 180000, + 'redisDb': 0, + 'impressionListener': ImpressionListenerClientEmpty(), + 'redisHost': 'localhost', + 'redisPosrt': 6379, + 'redisPrefix': 'customImpressionListenerTest' + } + factory = get_factory('asdqwe123456', config=config) + split = factory.client() + + self.assertEqual(split.get_treatment('valid', 'iltest'), 'on') + self.assertEqual(split.get_treatment('invalid', 'iltest'), 'off') + self.assertEqual(split.get_treatment('valid', 'iltest_invalid'), 'control') + + def test_throwing_exception_in_listener(self): + impression_exception = ImpressionListenerClientWithException() + + impression_wrapper = ImpressionListenerWrapper(impression_exception) + + with self.assertRaises(ImpressionListenerException): + impression_wrapper.log_impression(self.some_impression_0) diff --git a/splitio/tests/test_impressions.py b/splitio/tests/test_impressions.py index 4dffb6e2..ad7c2ec8 100644 --- a/splitio/tests/test_impressions.py +++ b/splitio/tests/test_impressions.py @@ -514,46 +514,3 @@ def test_log_doesnt_raise_exceptions_if_submit_does(self): # try: # except: # self.fail('Unexpected exception raised') - - -class TestImpressionListener(TestCase): - """ - Tests for impression listener in "in-memory" and "uwsgi-cache" operation - modes - """ - - def test_inmemory_impression_listener(self): - some_api = mock.MagicMock() - listener = mock.MagicMock() - treatment_log = SelfUpdatingTreatmentLog(some_api, listener=listener) - with mock.patch( - 'splitio.impressions.build_impressions_data', - return_value=[1, 2, 3] - ): - treatment_log._update_evictions('some_feature', []) - - listener.assert_called_once_with([1, 2, 3]) - - def test_uwsgi_impression_listener(self): - impressions_cache = mock.MagicMock() - impressions = { - 'testName': 'someTest', - 'keyImpressions': [1, 2, 3] - } - - impressions_cache.fetch_all_and_clear.return_value = { - 'someTest': [1, 2, 3] - } - some_api = mock.MagicMock() - listener = mock.MagicMock() - with mock.patch( - 'splitio.tasks.build_impressions_data', - return_value=impressions - ): - report_impressions( - impressions_cache, - some_api, - listener=listener - ) - - listener.assert_called_with({'impressions': impressions}) diff --git a/splitio/tests/test_redis_cache.py b/splitio/tests/test_redis_cache.py index 78f841fa..3389300d 100644 --- a/splitio/tests/test_redis_cache.py +++ b/splitio/tests/test_redis_cache.py @@ -78,10 +78,12 @@ def sadd(self, name, *values): """ raise Exception('ReadOnlyError') + class RedisReadOnlyTest(TestCase, MockUtilsMixin): def setUp(self): + self._some_config = mock.MagicMock() self._split_changes_file_name = join(dirname(__file__), 'splitChangesReadOnly.json') - + with open(self._split_changes_file_name) as f: self._json = load(f) split_definition = self._json['splits'][0] @@ -92,7 +94,7 @@ def setUp(self): self._mocked_redis = ReadOnlyRedisMock(self._redis) self._redis_split_cache = RedisSplitCache(self._redis) self._redis_split_cache.add_split(split_name, split_definition) - self._client = Client(RedisBroker(self._mocked_redis)) + self._client = Client(RedisBroker(self._mocked_redis, self._some_config)) self._impression = mock.MagicMock() self._start = mock.MagicMock() @@ -101,4 +103,4 @@ def setUp(self): def test_redis_read_only_mode(self): self.assertEqual(self._client.get_treatment('valid', 'test_read_only_1'), 'on') self.assertEqual(self._client.get_treatment('invalid', 'test_read_only_1'), 'off') - self.assertEqual(self._client.get_treatment('valid', 'test_read_only_1_invalid'), 'control') \ No newline at end of file + self.assertEqual(self._client.get_treatment('valid', 'test_read_only_1_invalid'), 'control') diff --git a/splitio/tests/test_splits.py b/splitio/tests/test_splits.py index f477e67f..229de2e4 100644 --- a/splitio/tests/test_splits.py +++ b/splitio/tests/test_splits.py @@ -1017,10 +1017,11 @@ class TrafficAllocationTests(TestCase): def setUp(self): ''' ''' + self.some_config = mock.MagicMock() redis = get_redis({}) segment_cache = RedisSegmentCache(redis) split_parser = RedisSplitParser(segment_cache) - self._client = Client(RedisBroker(redis)) + self._client = Client(RedisBroker(redis, self.some_config)) self._splitObjects = {} diff --git a/splitio/uwsgi.py b/splitio/uwsgi.py index 47baaa2a..b9a0810e 100644 --- a/splitio/uwsgi.py +++ b/splitio/uwsgi.py @@ -173,8 +173,7 @@ def uwsgi_report_impressions(user_config): sdk_api = api_factory(config) report_impressions( impressions_cache, - sdk_api, - user_config.get('impression_listener')) + sdk_api) time.sleep(seconds) except: