Skip to content

Commit 5f3e28f

Browse files
committed
add tests and start working on caching layer for storages
1 parent 9ecb290 commit 5f3e28f

File tree

4 files changed

+166
-28
lines changed

4 files changed

+166
-28
lines changed

splitio/client/client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,16 @@ def get_treatments_with_config(self, key, features, attributes=None):
208208
if input_validator.validate_attributes(attributes) is False:
209209
return input_validator.generate_control_treatments(features)
210210

211-
features = input_validator.validate_features_get_treatments(features)
211+
features, missing = input_validator.validate_features_get_treatments(
212+
features,
213+
self.ready,
214+
self._factory._get_storage('splits') # pylint: disable=protected-access
215+
)
212216
if features is None:
213217
return {}
214218

215219
bulk_impressions = []
216-
treatments = {}
220+
treatments = {name: (CONTROL, None) for name in missing}
217221

218222
for feature in features:
219223
try:

splitio/client/input_validator.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -381,22 +381,22 @@ def validate_manager_feature_name(feature_name):
381381
return feature_name
382382

383383

384-
def validate_features_get_treatments(features): #pylint: disable=invalid-name
384+
def validate_features_get_treatments(features, should_validate_existance=False, split_storage=None): #pylint: disable=invalid-name
385385
"""
386386
Check if features is valid for get_treatments.
387387
388388
:param features: array of features
389389
:type features: list
390390
:return: filtered_features
391-
:rtype: list|None
391+
:rtype: tuple
392392
"""
393393
operation = _get_first_split_sdk_call()
394394
if features is None or not isinstance(features, list):
395395
_LOGGER.error("%s: feature_names must be a non-empty array.", operation)
396-
return None
396+
return None, None
397397
if not features:
398398
_LOGGER.error("%s: feature_names must be a non-empty array.", operation)
399-
return []
399+
return None, None
400400
filtered_features = set(
401401
_remove_empty_spaces(feature, operation) for feature in features
402402
if feature is not None and
@@ -405,8 +405,20 @@ def validate_features_get_treatments(features): #pylint: disable=invalid-name
405405
)
406406
if not filtered_features:
407407
_LOGGER.error("%s: feature_names must be a non-empty array.", operation)
408-
return None
409-
return filtered_features
408+
return None, None
409+
410+
if not should_validate_existance:
411+
return filtered_features, []
412+
413+
valid_missing_features = set(f for f in filtered_features if split_storage.get(f) is None)
414+
for missing_feature in valid_missing_features:
415+
_LOGGER.error(
416+
"%s: you passed \"%s\" that does not exist in this environment, "
417+
"please double check what Splits exist in the web console.",
418+
operation,
419+
missing_feature
420+
)
421+
return filtered_features - valid_missing_features, valid_missing_features
410422

411423

412424
def generate_control_treatments(features):
@@ -418,7 +430,7 @@ def generate_control_treatments(features):
418430
:return: dict
419431
:rtype: dict|None
420432
"""
421-
return {feature: (CONTROL, None) for feature in validate_features_get_treatments(features)}
433+
return {feature: (CONTROL, None) for feature in validate_features_get_treatments(features)[0]}
422434

423435

424436
def validate_attributes(attributes):
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Caching trait module."""
2+
3+
import threading
4+
import time
5+
6+
7+
class ElementExpiredException(Exception):
8+
"""Exception to be thrown when an element requested is present but expired."""
9+
10+
pass
11+
12+
13+
class ElementNotPresentException(Exception):
14+
"""Exception to be thrown when an element requested is not present."""
15+
16+
pass
17+
18+
19+
class LocalMemoryCache(object):
20+
"""Key/Value local memory cache. with deprecation."""
21+
22+
def __init__(self, max_age_seconds=5):
23+
"""Class constructor."""
24+
self._data = {}
25+
self._lock = threading.RLock()
26+
self._max_age_seconds = max_age_seconds
27+
28+
def set(self, key, value):
29+
"""
30+
Set a key/value pair.
31+
32+
:param key: Key used to reference the value.
33+
:type key: str
34+
:param value: Value to store.
35+
:type value: object
36+
"""
37+
with self._lock:
38+
self._data[key] = (value, time.time())
39+
40+
def get(self, key):
41+
"""
42+
Attempt to get a value based on a key.
43+
44+
:param key: Key associated with the value.
45+
:type key: str
46+
47+
:return: The value associated with the key. None if it doesn't exist.
48+
:rtype: object
49+
"""
50+
try:
51+
value, set_time = self._data[key]
52+
except KeyError:
53+
raise ElementNotPresentException('Element %s not present in local storage' % key)
54+
55+
if (time.time() - set_time) > self._max_age_seconds:
56+
raise ElementExpiredException('Element %s present but expired' % key)
57+
58+
return value

tests/client/test_input_validator.py

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -181,55 +181,68 @@ def _get_storage_mock(storage):
181181
]
182182

183183
client._logger.reset_mock()
184-
assert client.get_treatment(Key('mathcing_key', None), 'some_feature') == CONTROL
184+
assert client.get_treatment(Key('matching_key', None), 'some_feature') == CONTROL
185185
assert client._logger.error.mock_calls == [
186186
mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'bucketing_key', 'bucketing_key')
187187
]
188188

189189
client._logger.reset_mock()
190-
assert client.get_treatment(Key('mathcing_key', True), 'some_feature') == CONTROL
190+
assert client.get_treatment(Key('matching_key', True), 'some_feature') == CONTROL
191191
assert client._logger.error.mock_calls == [
192192
mocker.call('%s: you passed an invalid %s, %s must be a non-empty string.', 'get_treatment', 'bucketing_key', 'bucketing_key')
193193
]
194194

195195
client._logger.reset_mock()
196-
assert client.get_treatment(Key('mathcing_key', []), 'some_feature') == CONTROL
196+
assert client.get_treatment(Key('matching_key', []), 'some_feature') == CONTROL
197197
assert client._logger.error.mock_calls == [
198198
mocker.call('%s: you passed an invalid %s, %s must be a non-empty string.', 'get_treatment', 'bucketing_key', 'bucketing_key')
199199
]
200200

201201
client._logger.reset_mock()
202-
assert client.get_treatment(Key('mathcing_key', ''), 'some_feature') == CONTROL
202+
assert client.get_treatment(Key('matching_key', ''), 'some_feature') == CONTROL
203203
assert client._logger.error.mock_calls == [
204204
mocker.call('%s: you passed an empty %s, %s must be a non-empty string.', 'get_treatment', 'bucketing_key', 'bucketing_key')
205205
]
206206

207207
client._logger.reset_mock()
208-
assert client.get_treatment(Key('mathcing_key', 12345), 'some_feature') == 'default_treatment'
208+
assert client.get_treatment(Key('matching_key', 12345), 'some_feature') == 'default_treatment'
209209
assert client._logger.warning.mock_calls == [
210210
mocker.call('%s: %s %s is not of type string, converting.', 'get_treatment', 'bucketing_key', 12345)
211211
]
212212

213213
client._logger.reset_mock()
214-
assert client.get_treatment('mathcing_key', 'some_feature', True) == CONTROL
214+
assert client.get_treatment('matching_key', 'some_feature', True) == CONTROL
215215
assert client._logger.error.mock_calls == [
216216
mocker.call('%s: attributes must be of type dictionary.', 'get_treatment')
217217
]
218218

219219
client._logger.reset_mock()
220-
assert client.get_treatment('mathcing_key', 'some_feature', {'test': 'test'}) == 'default_treatment'
220+
assert client.get_treatment('matching_key', 'some_feature', {'test': 'test'}) == 'default_treatment'
221221
assert client._logger.error.mock_calls == []
222222

223223
client._logger.reset_mock()
224-
assert client.get_treatment('mathcing_key', 'some_feature', None) == 'default_treatment'
224+
assert client.get_treatment('matching_key', 'some_feature', None) == 'default_treatment'
225225
assert client._logger.error.mock_calls == []
226226

227227
client._logger.reset_mock()
228-
assert client.get_treatment('mathcing_key', ' some_feature ', None) == 'default_treatment'
228+
assert client.get_treatment('matching_key', ' some_feature ', None) == 'default_treatment'
229229
assert client._logger.warning.mock_calls == [
230230
mocker.call('%s: feature_name \'%s\' has extra whitespace, trimming.', 'get_treatment', ' some_feature ')
231231
]
232232

233+
client._logger.reset_mock()
234+
storage_mock.get.return_value = None
235+
assert client.get_treatment('matching_key', 'some_feature', None) == CONTROL
236+
assert client._logger.error.mock_calls == [
237+
mocker.call(
238+
"%s: you passed \"%s\" that does not exist in this environment, "
239+
"please double check what Splits exist in the web console.",
240+
'get_treatment',
241+
'some_feature'
242+
)
243+
]
244+
245+
233246
def test_get_treatment_with_config(self, mocker):
234247
"""Test get_treatment validation."""
235248
split_mock = mocker.Mock(spec=Split)
@@ -396,55 +409,67 @@ def _get_storage_mock(storage):
396409
]
397410

398411
client._logger.reset_mock()
399-
assert client.get_treatment_with_config(Key('mathcing_key', None), 'some_feature') == (CONTROL, None)
412+
assert client.get_treatment_with_config(Key('matching_key', None), 'some_feature') == (CONTROL, None)
400413
assert client._logger.error.mock_calls == [
401414
mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'bucketing_key', 'bucketing_key')
402415
]
403416

404417
client._logger.reset_mock()
405-
assert client.get_treatment_with_config(Key('mathcing_key', True), 'some_feature') == (CONTROL, None)
418+
assert client.get_treatment_with_config(Key('matching_key', True), 'some_feature') == (CONTROL, None)
406419
assert client._logger.error.mock_calls == [
407420
mocker.call('%s: you passed an invalid %s, %s must be a non-empty string.', 'get_treatment_with_config', 'bucketing_key', 'bucketing_key')
408421
]
409422

410423
client._logger.reset_mock()
411-
assert client.get_treatment_with_config(Key('mathcing_key', []), 'some_feature') == (CONTROL, None)
424+
assert client.get_treatment_with_config(Key('matching_key', []), 'some_feature') == (CONTROL, None)
412425
assert client._logger.error.mock_calls == [
413426
mocker.call('%s: you passed an invalid %s, %s must be a non-empty string.', 'get_treatment_with_config', 'bucketing_key', 'bucketing_key')
414427
]
415428

416429
client._logger.reset_mock()
417-
assert client.get_treatment_with_config(Key('mathcing_key', ''), 'some_feature') == (CONTROL, None)
430+
assert client.get_treatment_with_config(Key('matching_key', ''), 'some_feature') == (CONTROL, None)
418431
assert client._logger.error.mock_calls == [
419432
mocker.call('%s: you passed an empty %s, %s must be a non-empty string.', 'get_treatment_with_config', 'bucketing_key', 'bucketing_key')
420433
]
421434

422435
client._logger.reset_mock()
423-
assert client.get_treatment_with_config(Key('mathcing_key', 12345), 'some_feature') == ('default_treatment', '{"some": "property"}')
436+
assert client.get_treatment_with_config(Key('matching_key', 12345), 'some_feature') == ('default_treatment', '{"some": "property"}')
424437
assert client._logger.warning.mock_calls == [
425438
mocker.call('%s: %s %s is not of type string, converting.', 'get_treatment_with_config', 'bucketing_key', 12345)
426439
]
427440

428441
client._logger.reset_mock()
429-
assert client.get_treatment_with_config('mathcing_key', 'some_feature', True) == (CONTROL, None)
442+
assert client.get_treatment_with_config('matching_key', 'some_feature', True) == (CONTROL, None)
430443
assert client._logger.error.mock_calls == [
431444
mocker.call('%s: attributes must be of type dictionary.', 'get_treatment_with_config')
432445
]
433446

434447
client._logger.reset_mock()
435-
assert client.get_treatment_with_config('mathcing_key', 'some_feature', {'test': 'test'}) == ('default_treatment', '{"some": "property"}')
448+
assert client.get_treatment_with_config('matching_key', 'some_feature', {'test': 'test'}) == ('default_treatment', '{"some": "property"}')
436449
assert client._logger.error.mock_calls == []
437450

438451
client._logger.reset_mock()
439-
assert client.get_treatment_with_config('mathcing_key', 'some_feature', None) == ('default_treatment', '{"some": "property"}')
452+
assert client.get_treatment_with_config('matching_key', 'some_feature', None) == ('default_treatment', '{"some": "property"}')
440453
assert client._logger.error.mock_calls == []
441454

442455
client._logger.reset_mock()
443-
assert client.get_treatment_with_config('mathcing_key', ' some_feature ', None) == ('default_treatment', '{"some": "property"}')
456+
assert client.get_treatment_with_config('matching_key', ' some_feature ', None) == ('default_treatment', '{"some": "property"}')
444457
assert client._logger.warning.mock_calls == [
445458
mocker.call('%s: feature_name \'%s\' has extra whitespace, trimming.', 'get_treatment_with_config', ' some_feature ')
446459
]
447460

461+
client._logger.reset_mock()
462+
storage_mock.get.return_value = None
463+
assert client.get_treatment_with_config('matching_key', 'some_feature', None) == (CONTROL, None)
464+
assert client._logger.error.mock_calls == [
465+
mocker.call(
466+
"%s: you passed \"%s\" that does not exist in this environment, "
467+
"please double check what Splits exist in the web console.",
468+
'get_treatment_with_config',
469+
'some_feature'
470+
)
471+
]
472+
448473
def test_track(self, mocker):
449474
"""Test track method()."""
450475
events_storage_mock = mocker.Mock(spec=EventStorage)
@@ -660,8 +685,16 @@ def test_get_treatments(self, mocker):
660685
storage_mock = mocker.Mock(spec=SplitStorage)
661686
storage_mock.get.return_value = split_mock
662687

688+
def _get_storage_mock(storage):
689+
return {
690+
'splits': storage_mock,
691+
'segments': mocker.Mock(spec=SegmentStorage),
692+
'impressions': mocker.Mock(spec=ImpressionStorage),
693+
'events': mocker.Mock(spec=EventStorage),
694+
'telemetry': mocker.Mock(spec=TelemetryStorage)
695+
}[storage]
663696
factory_mock = mocker.Mock(spec=SplitFactory)
664-
factory_mock._get_storage.return_value = storage_mock
697+
factory_mock._get_storage.side_effect = _get_storage_mock
665698
factory_destroyed = mocker.PropertyMock()
666699
factory_destroyed.return_value = False
667700
type(factory_mock).destroyed = factory_destroyed
@@ -750,6 +783,21 @@ def test_get_treatments(self, mocker):
750783
mocker.call('%s: feature_name \'%s\' has extra whitespace, trimming.', 'get_treatments', 'some ')
751784
]
752785

786+
client._logger.reset_mock()
787+
storage_mock.get.return_value = None
788+
ready_mock = mocker.PropertyMock()
789+
ready_mock.return_value = True
790+
type(factory_mock).ready = ready_mock
791+
assert client.get_treatments('matching_key', ['some_feature'], None) == {'some_feature': CONTROL}
792+
assert client._logger.error.mock_calls == [
793+
mocker.call(
794+
"%s: you passed \"%s\" that does not exist in this environment, "
795+
"please double check what Splits exist in the web console.",
796+
'get_treatments',
797+
'some_feature'
798+
)
799+
]
800+
753801
def test_get_treatments_with_config(self, mocker):
754802
"""Test getTreatments() method."""
755803
split_mock = mocker.Mock(spec=Split)
@@ -856,6 +904,22 @@ def _configs(treatment):
856904
mocker.call('%s: feature_name \'%s\' has extra whitespace, trimming.', 'get_treatments_with_config', 'some_feature ')
857905
]
858906

907+
client._logger.reset_mock()
908+
storage_mock.get.return_value = None
909+
ready_mock = mocker.PropertyMock()
910+
ready_mock.return_value = True
911+
type(factory_mock).ready = ready_mock
912+
assert client.get_treatments('matching_key', ['some_feature'], None) == {'some_feature': CONTROL}
913+
assert client._logger.error.mock_calls == [
914+
mocker.call(
915+
"%s: you passed \"%s\" that does not exist in this environment, "
916+
"please double check what Splits exist in the web console.",
917+
'get_treatments',
918+
'some_feature'
919+
)
920+
]
921+
922+
859923

860924
class ManagerInputValidationTests(object): #pylint: disable=too-few-public-methods
861925
"""Manager input validation test cases."""

0 commit comments

Comments
 (0)