diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index 80a75eec..3bd11512 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -133,16 +133,19 @@ def context_for(self, key, feature_names): key_membership = False segment_memberhsip = False for rbs_segment in pending_rbs_memberships: - key_membership = key in self._rbs_segment_storage.get(rbs_segment).excluded.get_excluded_keys() + rbs_segment_obj = self._rbs_segment_storage.get(rbs_segment) + pending_memberships.update(rbs_segment_obj.get_condition_segment_names()) + + key_membership = key in rbs_segment_obj.excluded.get_excluded_keys() segment_memberhsip = False - for segment_name in self._rbs_segment_storage.get(rbs_segment).excluded.get_excluded_segments(): + for segment_name in rbs_segment_obj.excluded.get_excluded_segments(): if self._segment_storage.segment_contains(segment_name, key): segment_memberhsip = True break rbs_segment_memberships.update({rbs_segment: segment_memberhsip or key_membership}) if not (segment_memberhsip or key_membership): - rbs_segment_conditions.update({rbs_segment: [condition for condition in self._rbs_segment_storage.get(rbs_segment).conditions]}) + rbs_segment_conditions.update({rbs_segment: [condition for condition in rbs_segment_obj.conditions]}) return EvaluationContext( splits, @@ -184,18 +187,14 @@ async def context_for(self, key, feature_names): pending_memberships.update(cs) pending_rbs_memberships.update(crbs) - segment_names = list(pending_memberships) - segment_memberships = await asyncio.gather(*[ - self._segment_storage.segment_contains(segment, key) - for segment in segment_names - ]) - rbs_segment_memberships = {} rbs_segment_conditions = {} key_membership = False segment_memberhsip = False for rbs_segment in pending_rbs_memberships: rbs_segment_obj = await self._rbs_segment_storage.get(rbs_segment) + pending_memberships.update(rbs_segment_obj.get_condition_segment_names()) + key_membership = key in rbs_segment_obj.excluded.get_excluded_keys() segment_memberhsip = False for segment_name in rbs_segment_obj.excluded.get_excluded_segments(): @@ -207,6 +206,11 @@ async def context_for(self, key, feature_names): if not (segment_memberhsip or key_membership): rbs_segment_conditions.update({rbs_segment: [condition for condition in rbs_segment_obj.conditions]}) + segment_names = list(pending_memberships) + segment_memberships = await asyncio.gather(*[ + self._segment_storage.segment_contains(segment, key) + for segment in segment_names + ]) return EvaluationContext( splits, dict(zip(segment_names, segment_memberships)), diff --git a/splitio/models/rule_based_segments.py b/splitio/models/rule_based_segments.py index 66ec7ddf..f611a792 100644 --- a/splitio/models/rule_based_segments.py +++ b/splitio/models/rule_based_segments.py @@ -76,6 +76,14 @@ def to_json(self): 'excluded': self.excluded.to_json() } + def get_condition_segment_names(self): + segments = set() + for condition in self._conditions: + for matcher in condition.matchers: + if matcher._matcher_type == 'IN_SEGMENT': + segments.add(matcher.to_json()['userDefinedSegmentMatcherData']['segmentName']) + return segments + def from_raw(raw_rule_based_segment): """ Parse a Rule based segment from a JSON portion of splitChanges. diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 98fc0543..c3fb09ec 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -200,7 +200,7 @@ def get_segment_names(self): """ with self._lock: return list(self._rule_based_segments.keys()) - + def get_large_segment_names(self): """ Retrieve a list of all excluded large segments names. diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index f547a701..699f4871 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -53,6 +53,7 @@ def update_rule_based_segment_storage(rule_based_segment_storage, rule_based_seg if rule_based_segment.status == "ACTIVE": to_add.append(rule_based_segment) segment_list.update(set(rule_based_segment.excluded.get_excluded_segments())) + segment_list.update(rule_based_segment.get_condition_segment_names()) else: if rule_based_segment_storage.get(rule_based_segment.name) is not None: to_delete.append(rule_based_segment.name) @@ -109,6 +110,7 @@ async def update_rule_based_segment_storage_async(rule_based_segment_storage, ru if rule_based_segment.status == "ACTIVE": to_add.append(rule_based_segment) segment_list.update(set(rule_based_segment.excluded.get_excluded_segments())) + segment_list.update(rule_based_segment.get_condition_segment_names()) else: if await rule_based_segment_storage.get(rule_based_segment.name) is not None: to_delete.append(rule_based_segment.name) diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index 6268ad1d..de8f9325 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -1,6 +1,7 @@ """Evaluator tests module.""" import logging import pytest +import copy from splitio.models.splits import Split, Status from splitio.models.grammar.condition import Condition, ConditionType @@ -243,7 +244,7 @@ def test_evaluate_treatment_with_rule_based_segment(self, mocker): ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), segment_rbs_memberships={'sample_rule_based_segment': True}, segment_rbs_conditions={'sample_rule_based_segment': []}) result = e.eval_with_context('bilal@split.io', 'bilal@split.io', 'some', {'email': 'bilal@split.io'}, ctx) assert result['treatment'] == 'off' - + class EvaluationDataFactoryTests(object): """Test evaluation factory class.""" @@ -254,37 +255,75 @@ def test_get_context(self): segment_storage = InMemorySegmentStorage() rbs_segment_storage = InMemoryRuleBasedSegmentStorage() flag_storage.update([mocked_split], [], -1) - rbs = rule_based_segments.from_raw(rbs_raw) + rbs = copy.deepcopy(rbs_raw) + rbs['conditions'].append( + {"matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": False, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": None + } + ] + }, + }) + rbs = rule_based_segments.from_raw(rbs) rbs_segment_storage.update([rbs], [], -1) eval_factory = EvaluationDataFactory(flag_storage, segment_storage, rbs_segment_storage) ec = eval_factory.context_for('bilal@split.io', ['some']) assert ec.segment_rbs_conditions == {'sample_rule_based_segment': rbs.conditions} assert ec.segment_rbs_memberships == {'sample_rule_based_segment': False} + assert ec.segment_memberships == {"employees": False} + segment_storage.update("employees", {"mauro@split.io"}, {}, 1234) ec = eval_factory.context_for('mauro@split.io', ['some']) assert ec.segment_rbs_conditions == {} assert ec.segment_rbs_memberships == {'sample_rule_based_segment': True} - + assert ec.segment_memberships == {"employees": True} + class EvaluationDataFactoryAsyncTests(object): """Test evaluation factory class.""" @pytest.mark.asyncio async def test_get_context(self): """Test context.""" - mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False) + mocked_split = Split('some', 123, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False) flag_storage = InMemorySplitStorageAsync([]) segment_storage = InMemorySegmentStorageAsync() rbs_segment_storage = InMemoryRuleBasedSegmentStorageAsync() await flag_storage.update([mocked_split], [], -1) - rbs = rule_based_segments.from_raw(rbs_raw) + rbs = copy.deepcopy(rbs_raw) + rbs['conditions'].append( + {"matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": False, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": None + } + ] + }, + }) + rbs = rule_based_segments.from_raw(rbs) await rbs_segment_storage.update([rbs], [], -1) eval_factory = AsyncEvaluationDataFactory(flag_storage, segment_storage, rbs_segment_storage) ec = await eval_factory.context_for('bilal@split.io', ['some']) assert ec.segment_rbs_conditions == {'sample_rule_based_segment': rbs.conditions} assert ec.segment_rbs_memberships == {'sample_rule_based_segment': False} + assert ec.segment_memberships == {"employees": False} + await segment_storage.update("employees", {"mauro@split.io"}, {}, 1234) ec = await eval_factory.context_for('mauro@split.io', ['some']) assert ec.segment_rbs_conditions == {} assert ec.segment_rbs_memberships == {'sample_rule_based_segment': True} + assert ec.segment_memberships == {"employees": True} diff --git a/tests/models/test_rule_based_segments.py b/tests/models/test_rule_based_segments.py index 96cbdd30..9a822903 100644 --- a/tests/models/test_rule_based_segments.py +++ b/tests/models/test_rule_based_segments.py @@ -1,6 +1,6 @@ """Split model tests module.""" import copy - +import pytest from splitio.models import rule_based_segments from splitio.models import splits from splitio.models.grammar.condition import Condition @@ -79,4 +79,26 @@ def test_incorrect_matcher(self): rbs['conditions'].append(rbs['conditions'][0]) rbs['conditions'][0]['matcherGroup']['matchers'][0]['matcherType'] = 'INVALID_MATCHER' parsed = rule_based_segments.from_raw(rbs) - assert parsed.conditions[0].to_json() == splits._DEFAULT_CONDITIONS_TEMPLATE \ No newline at end of file + assert parsed.conditions[0].to_json() == splits._DEFAULT_CONDITIONS_TEMPLATE + + def test_get_condition_segment_names(self): + rbs = copy.deepcopy(self.raw) + rbs['conditions'].append( + {"matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": False, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": None + } + ] + }, + }) + rbs = rule_based_segments.from_raw(rbs) + + assert rbs.get_condition_segment_names() == {"employees"} + \ No newline at end of file diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index a290d721..283eb8e3 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -1386,11 +1386,11 @@ def test_get(self): for sprefix in [None, 'myprefix']: pluggable_rbs_storage = PluggableRuleBasedSegmentsStorage(self.mock_adapter, prefix=sprefix) - rbs1 = rule_based_segments.from_raw(rbsegments_json[0]['segment1']) - rbs_name = rbsegments_json[0]['segment1']['name'] + rbs1 = rule_based_segments.from_raw(rbsegments_json[0]) + rbs_name = rbsegments_json[0]['name'] self.mock_adapter.set(pluggable_rbs_storage._prefix.format(segment_name=rbs_name), rbs1.to_json()) - assert(pluggable_rbs_storage.get(rbs_name).to_json() == rule_based_segments.from_raw(rbsegments_json[0]['segment1']).to_json()) + assert(pluggable_rbs_storage.get(rbs_name).to_json() == rule_based_segments.from_raw(rbsegments_json[0]).to_json()) assert(pluggable_rbs_storage.get('not_existing') == None) def test_get_change_number(self): @@ -1408,8 +1408,8 @@ def test_get_segment_names(self): self.mock_adapter._keys = {} for sprefix in [None, 'myprefix']: pluggable_rbs_storage = PluggableRuleBasedSegmentsStorage(self.mock_adapter, prefix=sprefix) - rbs1 = rule_based_segments.from_raw(rbsegments_json[0]['segment1']) - rbs2_temp = copy.deepcopy(rbsegments_json[0]['segment1']) + rbs1 = rule_based_segments.from_raw(rbsegments_json[0]) + rbs2_temp = copy.deepcopy(rbsegments_json[0]) rbs2_temp['name'] = 'another_segment' rbs2 = rule_based_segments.from_raw(rbs2_temp) self.mock_adapter.set(pluggable_rbs_storage._prefix.format(segment_name=rbs1.name), rbs1.to_json()) @@ -1420,8 +1420,8 @@ def test_contains(self): self.mock_adapter._keys = {} for sprefix in [None, 'myprefix']: pluggable_rbs_storage = PluggableRuleBasedSegmentsStorage(self.mock_adapter, prefix=sprefix) - rbs1 = rule_based_segments.from_raw(rbsegments_json[0]['segment1']) - rbs2_temp = copy.deepcopy(rbsegments_json[0]['segment1']) + rbs1 = rule_based_segments.from_raw(rbsegments_json[0]) + rbs2_temp = copy.deepcopy(rbsegments_json[0]) rbs2_temp['name'] = 'another_segment' rbs2 = rule_based_segments.from_raw(rbs2_temp) self.mock_adapter.set(pluggable_rbs_storage._prefix.format(segment_name=rbs1.name), rbs1.to_json()) @@ -1445,12 +1445,12 @@ async def test_get(self): for sprefix in [None, 'myprefix']: pluggable_rbs_storage = PluggableRuleBasedSegmentsStorageAsync(self.mock_adapter, prefix=sprefix) - rbs1 = rule_based_segments.from_raw(rbsegments_json[0]['segment1']) - rbs_name = rbsegments_json[0]['segment1']['name'] + rbs1 = rule_based_segments.from_raw(rbsegments_json[0]) + rbs_name = rbsegments_json[0]['name'] await self.mock_adapter.set(pluggable_rbs_storage._prefix.format(segment_name=rbs_name), rbs1.to_json()) rbs = await pluggable_rbs_storage.get(rbs_name) - assert(rbs.to_json() == rule_based_segments.from_raw(rbsegments_json[0]['segment1']).to_json()) + assert(rbs.to_json() == rule_based_segments.from_raw(rbsegments_json[0]).to_json()) assert(await pluggable_rbs_storage.get('not_existing') == None) @pytest.mark.asyncio @@ -1470,8 +1470,8 @@ async def test_get_segment_names(self): self.mock_adapter._keys = {} for sprefix in [None, 'myprefix']: pluggable_rbs_storage = PluggableRuleBasedSegmentsStorageAsync(self.mock_adapter, prefix=sprefix) - rbs1 = rule_based_segments.from_raw(rbsegments_json[0]['segment1']) - rbs2_temp = copy.deepcopy(rbsegments_json[0]['segment1']) + rbs1 = rule_based_segments.from_raw(rbsegments_json[0]) + rbs2_temp = copy.deepcopy(rbsegments_json[0]) rbs2_temp['name'] = 'another_segment' rbs2 = rule_based_segments.from_raw(rbs2_temp) await self.mock_adapter.set(pluggable_rbs_storage._prefix.format(segment_name=rbs1.name), rbs1.to_json()) @@ -1483,8 +1483,8 @@ async def test_contains(self): self.mock_adapter._keys = {} for sprefix in [None, 'myprefix']: pluggable_rbs_storage = PluggableRuleBasedSegmentsStorageAsync(self.mock_adapter, prefix=sprefix) - rbs1 = rule_based_segments.from_raw(rbsegments_json[0]['segment1']) - rbs2_temp = copy.deepcopy(rbsegments_json[0]['segment1']) + rbs1 = rule_based_segments.from_raw(rbsegments_json[0]) + rbs2_temp = copy.deepcopy(rbsegments_json[0]) rbs2_temp['name'] = 'another_segment' rbs2 = rule_based_segments.from_raw(rbs2_temp) await self.mock_adapter.set(pluggable_rbs_storage._prefix.format(segment_name=rbs1.name), rbs1.to_json()) diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index 7608306d..7c9c04fc 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -1,14 +1,43 @@ """Storage Helper tests.""" import pytest -from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets -from splitio.storage.inmemmory import InMemorySplitStorage -from splitio.models import splits +from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets, \ + update_rule_based_segment_storage, update_rule_based_segment_storage_async, update_feature_flag_storage_async +from splitio.storage.inmemmory import InMemorySplitStorage, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync, \ + InMemorySplitStorageAsync +from splitio.models import splits, rule_based_segments from splitio.storage import FlagSetsFilter from tests.sync.test_splits_synchronizer import splits_raw as split_sample class StorageHelperTests(object): + rbs = rule_based_segments.from_raw({ + "changeNumber": 123, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":['excluded_segment'] + }, + "conditions": [ + {"matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": False, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": None + } + ] + }, + } + ] + }) + def test_update_feature_flag_storage(self, mocker): storage = mocker.Mock(spec=InMemorySplitStorage) split = splits.from_raw(split_sample[0]) @@ -126,4 +155,137 @@ def test_combine_valid_flag_sets(self): assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} results_set = ['set1', {'set2', 'set3'}] - assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} \ No newline at end of file + assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} + + def test_update_rule_base_segment_storage(self, mocker): + storage = mocker.Mock(spec=InMemoryRuleBasedSegmentStorage) + self.added = [] + self.deleted = [] + self.change_number = 0 + def update(to_add, to_delete, change_number): + self.added = to_add + self.deleted = to_delete + self.change_number = change_number + storage.update = update + + segments = update_rule_based_segment_storage(storage, [self.rbs], 123) + assert self.added[0] == self.rbs + assert self.deleted == [] + assert self.change_number == 123 + assert segments == {'excluded_segment', 'employees'} + + @pytest.mark.asyncio + async def test_update_rule_base_segment_storage_async(self, mocker): + storage = mocker.Mock(spec=InMemoryRuleBasedSegmentStorageAsync) + self.added = [] + self.deleted = [] + self.change_number = 0 + async def update(to_add, to_delete, change_number): + self.added = to_add + self.deleted = to_delete + self.change_number = change_number + storage.update = update + + segments = await update_rule_based_segment_storage_async(storage, [self.rbs], 123) + assert self.added[0] == self.rbs + assert self.deleted == [] + assert self.change_number == 123 + assert segments == {'excluded_segment', 'employees'} + + @pytest.mark.asyncio + async def test_update_feature_flag_storage_async(self, mocker): + storage = mocker.Mock(spec=InMemorySplitStorageAsync) + split = splits.from_raw(split_sample[0]) + + self.added = [] + self.deleted = [] + self.change_number = 0 + async def get(flag_name): + return None + storage.get = get + + async def update(to_add, to_delete, change_number): + self.added = to_add + self.deleted = to_delete + self.change_number = change_number + storage.update = update + + async def is_flag_set_exist(flag_set): + return False + storage.is_flag_set_exist = is_flag_set_exist + + class flag_set_filter(): + def should_filter(): + return False + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + + await update_feature_flag_storage_async(storage, [split], 123) + assert self.added[0] == split + assert self.deleted == [] + assert self.change_number == 123 + + class flag_set_filter2(): + def should_filter(): + return True + def intersect(sets): + return False + storage.flag_set_filter = flag_set_filter2 + storage.flag_set_filter.flag_sets = set({'set1', 'set2'}) + + async def get(flag_name): + return split + storage.get = get + + await update_feature_flag_storage_async(storage, [split], 123) + assert self.added == [] + assert self.deleted[0] == split.name + + class flag_set_filter3(): + def should_filter(): + return True + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter3 + storage.flag_set_filter.flag_sets = set({'set1', 'set2'}) + + async def is_flag_set_exist2(flag_set): + return True + storage.is_flag_set_exist = is_flag_set_exist2 + await update_feature_flag_storage_async(storage, [split], 123) + assert self.added[0] == split + assert self.deleted == [] + + split_json = split_sample[0] + split_json['conditions'].append({ + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": False, + "userDefinedSegmentMatcherData": { + "segmentName": "segment1" + }, + "whitelistMatcherData": None + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 30 + }, + { + "treatment": "off", + "size": 70 + } + ] + } + ) + + split = splits.from_raw(split_json) + storage.config_flag_sets_used = 0 + assert await update_feature_flag_storage_async(storage, [split], 123) == {'segment1'} \ No newline at end of file