Skip to content

Commit 249d9c6

Browse files
authored
Merge pull request #573 from splitio/T-FME-3998-prereq-evaluator
Updated evaluator
2 parents 488757f + 2214cd5 commit 249d9c6

File tree

3 files changed

+142
-18
lines changed

3 files changed

+142
-18
lines changed

splitio/engine/evaluator.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from splitio.models.grammar.matchers.misc import DependencyMatcher
88
from splitio.models.grammar.matchers.keys import UserDefinedSegmentMatcher
99
from splitio.models.grammar.matchers import RuleBasedSegmentMatcher
10+
from splitio.models.grammar.matchers.prerequisites import PrerequisitesMatcher
1011
from splitio.models.rule_based_segments import SegmentType
1112
from splitio.optional.loaders import asyncio
1213

@@ -56,12 +57,22 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx):
5657
label = Label.KILLED
5758
_treatment = feature.default_treatment
5859
else:
59-
treatment, label = self._treatment_for_flag(feature, key, bucketing, attrs, ctx)
60-
if treatment is None:
61-
label = Label.NO_CONDITION_MATCHED
62-
_treatment = feature.default_treatment
63-
else:
64-
_treatment = treatment
60+
if feature.prerequisites is not None:
61+
prerequisites_matcher = PrerequisitesMatcher(feature.prerequisites)
62+
if not prerequisites_matcher.match(key, attrs, {
63+
'evaluator': self,
64+
'bucketing_key': bucketing,
65+
'ec': ctx}):
66+
label = Label.PREREQUISITES_NOT_MET
67+
_treatment = feature.default_treatment
68+
69+
if _treatment == CONTROL:
70+
treatment, label = self._treatment_for_flag(feature, key, bucketing, attrs, ctx)
71+
if treatment is None:
72+
label = Label.NO_CONDITION_MATCHED
73+
_treatment = feature.default_treatment
74+
else:
75+
_treatment = treatment
6576

6677
return {
6778
'treatment': _treatment,
@@ -133,7 +144,6 @@ def context_for(self, key, feature_names):
133144
rb_segments
134145
)
135146

136-
137147
class AsyncEvaluationDataFactory:
138148

139149
def __init__(self, split_storage, segment_storage, rbs_segment_storage):
@@ -199,6 +209,7 @@ def get_pending_objects(features, splits, rbsegments, rb_segments, pending_membe
199209
pending_rbs = set()
200210
for feature in features.values():
201211
cf, cs, crbs = get_dependencies(feature)
212+
cf.extend(get_prerequisites(feature))
202213
pending.update(filter(lambda f: f not in splits, cf))
203214
pending_memberships.update(cs)
204215
pending_rbs.update(filter(lambda f: f not in rb_segments, crbs))
@@ -223,4 +234,6 @@ def update_objects(fetched, fetched_rbs, splits, rb_segments):
223234
rb_segments.update(rbsegments)
224235

225236
return features, rbsegments, splits, rb_segments
226-
237+
238+
def get_prerequisites(feature):
239+
return [prerequisite.feature_flag_name for prerequisite in feature.prerequisites]

splitio/models/impressions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,8 @@ class Label(object): # pylint: disable=too-few-public-methods
6060
# Treatment: control
6161
# Label: not ready
6262
NOT_READY = 'not ready'
63+
64+
# Condition: Prerequisites not met
65+
# Treatment: Default treatment
66+
# Label: prerequisites not met
67+
PREREQUISITES_NOT_MET = "prerequisites not met"

tests/engine/test_evaluator.py

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
import copy
77

8-
from splitio.models.splits import Split, Status
8+
from splitio.models.splits import Split, Status, from_raw, Prerequisites
99
from splitio.models import segments
1010
from splitio.models.grammar.condition import Condition, ConditionType
1111
from splitio.models.impressions import Label
@@ -127,6 +127,7 @@ def test_evaluate_treatment_killed_split(self, mocker):
127127
mocked_split.killed = True
128128
mocked_split.change_number = 123
129129
mocked_split.get_configurations_for.return_value = '{"some_property": 123}'
130+
mocked_split.prerequisites = []
130131

131132
ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), rbs_segments={})
132133
result = e.eval_with_context('some_key', 'some_bucketing_key', 'some', {}, ctx)
@@ -146,6 +147,8 @@ def test_evaluate_treatment_ok(self, mocker):
146147
mocked_split.killed = False
147148
mocked_split.change_number = 123
148149
mocked_split.get_configurations_for.return_value = '{"some_property": 123}'
150+
mocked_split.prerequisites = []
151+
149152
ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), rbs_segments={})
150153
result = e.eval_with_context('some_key', 'some_bucketing_key', 'some', {}, ctx)
151154
assert result['treatment'] == 'on'
@@ -165,6 +168,8 @@ def test_evaluate_treatment_ok_no_config(self, mocker):
165168
mocked_split.killed = False
166169
mocked_split.change_number = 123
167170
mocked_split.get_configurations_for.return_value = None
171+
mocked_split.prerequisites = []
172+
168173
ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), rbs_segments={})
169174
result = e.eval_with_context('some_key', 'some_bucketing_key', 'some', {}, ctx)
170175
assert result['treatment'] == 'on'
@@ -184,13 +189,15 @@ def test_evaluate_treatments(self, mocker):
184189
mocked_split.killed = False
185190
mocked_split.change_number = 123
186191
mocked_split.get_configurations_for.return_value = '{"some_property": 123}'
192+
mocked_split.prerequisites = []
187193

188194
mocked_split2 = mocker.Mock(spec=Split)
189195
mocked_split2.name = 'feature4'
190196
mocked_split2.default_treatment = 'on'
191197
mocked_split2.killed = False
192198
mocked_split2.change_number = 123
193199
mocked_split2.get_configurations_for.return_value = None
200+
mocked_split2.prerequisites = []
194201

195202
ctx = EvaluationContext(flags={'feature2': mocked_split, 'feature4': mocked_split2}, segment_memberships=set(), rbs_segments={})
196203
results = e.eval_many_with_context('some_key', 'some_bucketing_key', ['feature2', 'feature4'], {}, ctx)
@@ -215,6 +222,8 @@ def test_get_gtreatment_for_split_no_condition_matches(self, mocker):
215222
mocked_split.change_number = '123'
216223
mocked_split.conditions = []
217224
mocked_split.get_configurations_for = None
225+
mocked_split.prerequisites = []
226+
218227
ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), rbs_segments={})
219228
assert e._treatment_for_flag(mocked_split, 'some_key', 'some_bucketing', {}, ctx) == (
220229
'off',
@@ -232,6 +241,8 @@ def test_get_gtreatment_for_split_non_rollout(self, mocker):
232241
mocked_split = mocker.Mock(spec=Split)
233242
mocked_split.killed = False
234243
mocked_split.conditions = [mocked_condition_1]
244+
mocked_split.prerequisites = []
245+
235246
treatment, label = e._treatment_for_flag(mocked_split, 'some_key', 'some_bucketing', {}, EvaluationContext(None, None, None))
236247
assert treatment == 'on'
237248
assert label == 'some_label'
@@ -240,7 +251,7 @@ def test_evaluate_treatment_with_rule_based_segment(self, mocker):
240251
"""Test that a non-killed split returns the appropriate treatment."""
241252
e = evaluator.Evaluator(splitters.Splitter())
242253

243-
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False)
254+
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [])
244255

245256
ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), rbs_segments={'sample_rule_based_segment': rule_based_segments.from_raw(rbs_raw)})
246257
result = e.eval_with_context('bilal@split.io', 'bilal@split.io', 'some', {'email': 'bilal@split.io'}, ctx)
@@ -257,7 +268,7 @@ def test_evaluate_treatment_with_rbs_in_condition(self):
257268
with open(rbs_segments, 'r') as flo:
258269
data = json.loads(flo.read())
259270

260-
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False)
271+
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [])
261272
rbs = rule_based_segments.from_raw(data["rbs"]["d"][0])
262273
rbs2 = rule_based_segments.from_raw(data["rbs"]["d"][1])
263274
rbs_storage.update([rbs, rbs2], [], 12)
@@ -279,7 +290,7 @@ def test_using_segment_in_excluded(self):
279290
segment_storage = InMemorySegmentStorage()
280291
evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage)
281292

282-
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False)
293+
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [])
283294
rbs = rule_based_segments.from_raw(data["rbs"]["d"][0])
284295
rbs_storage.update([rbs], [], 12)
285296
splits_storage.update([mocked_split], [], 12)
@@ -303,7 +314,7 @@ def test_using_rbs_in_excluded(self):
303314
segment_storage = InMemorySegmentStorage()
304315
evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage)
305316

306-
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False)
317+
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [])
307318
rbs = rule_based_segments.from_raw(data["rbs"]["d"][0])
308319
rbs2 = rule_based_segments.from_raw(data["rbs"]["d"][1])
309320
rbs_storage.update([rbs, rbs2], [], 12)
@@ -315,7 +326,52 @@ def test_using_rbs_in_excluded(self):
315326
assert e.eval_with_context('bilal', 'bilal', 'some', {'email': 'bilal'}, ctx)['treatment'] == "on"
316327
ctx = evaluation_facctory.context_for('bilal2@split.io', ['some'])
317328
assert e.eval_with_context('bilal2@split.io', 'bilal2@split.io', 'some', {'email': 'bilal2@split.io'}, ctx)['treatment'] == "off"
318-
329+
330+
def test_prerequisites(self):
331+
splits_load = os.path.join(os.path.dirname(__file__), '../models/grammar/files', 'splits_prereq.json')
332+
with open(splits_load, 'r') as flo:
333+
data = json.loads(flo.read())
334+
e = evaluator.Evaluator(splitters.Splitter())
335+
splits_storage = InMemorySplitStorage()
336+
rbs_storage = InMemoryRuleBasedSegmentStorage()
337+
segment_storage = InMemorySegmentStorage()
338+
evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage)
339+
340+
rbs = rule_based_segments.from_raw(data["rbs"]["d"][0])
341+
split1 = from_raw(data["ff"]["d"][0])
342+
split2 = from_raw(data["ff"]["d"][1])
343+
split3 = from_raw(data["ff"]["d"][2])
344+
split4 = from_raw(data["ff"]["d"][3])
345+
rbs_storage.update([rbs], [], 12)
346+
splits_storage.update([split1, split2, split3, split4], [], 12)
347+
segment = segments.from_raw({'name': 'segment-test', 'added': ['pato@split.io'], 'removed': [], 'till': 123})
348+
segment_storage.put(segment)
349+
350+
ctx = evaluation_facctory.context_for('bilal@split.io', ['test_prereq'])
351+
assert e.eval_with_context('bilal@split.io', 'bilal@split.io', 'test_prereq', {'email': 'bilal@split.io'}, ctx)['treatment'] == "on"
352+
assert e.eval_with_context('bilal@split.io', 'bilal@split.io', 'test_prereq', {}, ctx)['treatment'] == "def_treatment"
353+
354+
ctx = evaluation_facctory.context_for('mauro@split.io', ['test_prereq'])
355+
assert e.eval_with_context('mauro@split.io', 'mauro@split.io', 'test_prereq', {'email': 'mauro@split.io'}, ctx)['treatment'] == "def_treatment"
356+
357+
ctx = evaluation_facctory.context_for('pato@split.io', ['test_prereq'])
358+
assert e.eval_with_context('pato@split.io', 'pato@split.io', 'test_prereq', {'email': 'pato@split.io'}, ctx)['treatment'] == "def_treatment"
359+
360+
ctx = evaluation_facctory.context_for('nico@split.io', ['test_prereq'])
361+
assert e.eval_with_context('nico@split.io', 'nico@split.io', 'test_prereq', {'email': 'nico@split.io'}, ctx)['treatment'] == "on"
362+
363+
ctx = evaluation_facctory.context_for('bilal@split.io', ['prereq_chain'])
364+
assert e.eval_with_context('bilal@split.io', 'bilal@split.io', 'prereq_chain', {'email': 'bilal@split.io'}, ctx)['treatment'] == "on_whitelist"
365+
366+
ctx = evaluation_facctory.context_for('nico@split.io', ['prereq_chain'])
367+
assert e.eval_with_context('nico@split.io', 'nico@split.io', 'test_prereq', {'email': 'nico@split.io'}, ctx)['treatment'] == "on"
368+
369+
ctx = evaluation_facctory.context_for('pato@split.io', ['prereq_chain'])
370+
assert e.eval_with_context('pato@split.io', 'pato@split.io', 'prereq_chain', {'email': 'pato@split.io'}, ctx)['treatment'] == "on_default"
371+
372+
ctx = evaluation_facctory.context_for('mauro@split.io', ['prereq_chain'])
373+
assert e.eval_with_context('mauro@split.io', 'mauro@split.io', 'prereq_chain', {'email': 'mauro@split.io'}, ctx)['treatment'] == "on_default"
374+
319375
@pytest.mark.asyncio
320376
async def test_evaluate_treatment_with_rbs_in_condition_async(self):
321377
e = evaluator.Evaluator(splitters.Splitter())
@@ -388,16 +444,63 @@ async def test_using_rbs_in_excluded_async(self):
388444
ctx = await evaluation_facctory.context_for('bilal2@split.io', ['some'])
389445
assert e.eval_with_context('bilal2@split.io', 'bilal2@split.io', 'some', {'email': 'bilal2@split.io'}, ctx)['treatment'] == "off"
390446

447+
@pytest.mark.asyncio
448+
async def test_prerequisites(self):
449+
splits_load = os.path.join(os.path.dirname(__file__), '../models/grammar/files', 'splits_prereq.json')
450+
with open(splits_load, 'r') as flo:
451+
data = json.loads(flo.read())
452+
e = evaluator.Evaluator(splitters.Splitter())
453+
splits_storage = InMemorySplitStorageAsync()
454+
rbs_storage = InMemoryRuleBasedSegmentStorageAsync()
455+
segment_storage = InMemorySegmentStorageAsync()
456+
evaluation_facctory = AsyncEvaluationDataFactory(splits_storage, segment_storage, rbs_storage)
457+
458+
rbs = rule_based_segments.from_raw(data["rbs"]["d"][0])
459+
split1 = from_raw(data["ff"]["d"][0])
460+
split2 = from_raw(data["ff"]["d"][1])
461+
split3 = from_raw(data["ff"]["d"][2])
462+
split4 = from_raw(data["ff"]["d"][3])
463+
await rbs_storage.update([rbs], [], 12)
464+
await splits_storage.update([split1, split2, split3, split4], [], 12)
465+
segment = segments.from_raw({'name': 'segment-test', 'added': ['pato@split.io'], 'removed': [], 'till': 123})
466+
await segment_storage.put(segment)
467+
468+
ctx = await evaluation_facctory.context_for('bilal@split.io', ['test_prereq'])
469+
assert e.eval_with_context('bilal@split.io', 'bilal@split.io', 'test_prereq', {'email': 'bilal@split.io'}, ctx)['treatment'] == "on"
470+
assert e.eval_with_context('bilal@split.io', 'bilal@split.io', 'test_prereq', {}, ctx)['treatment'] == "def_treatment"
471+
472+
ctx = await evaluation_facctory.context_for('mauro@split.io', ['test_prereq'])
473+
assert e.eval_with_context('mauro@split.io', 'mauro@split.io', 'test_prereq', {'email': 'mauro@split.io'}, ctx)['treatment'] == "def_treatment"
474+
475+
ctx = await evaluation_facctory.context_for('pato@split.io', ['test_prereq'])
476+
assert e.eval_with_context('pato@split.io', 'pato@split.io', 'test_prereq', {'email': 'pato@split.io'}, ctx)['treatment'] == "def_treatment"
477+
478+
ctx = await evaluation_facctory.context_for('nico@split.io', ['test_prereq'])
479+
assert e.eval_with_context('nico@split.io', 'nico@split.io', 'test_prereq', {'email': 'nico@split.io'}, ctx)['treatment'] == "on"
480+
481+
ctx = await evaluation_facctory.context_for('bilal@split.io', ['prereq_chain'])
482+
assert e.eval_with_context('bilal@split.io', 'bilal@split.io', 'prereq_chain', {'email': 'bilal@split.io'}, ctx)['treatment'] == "on_whitelist"
483+
484+
ctx = await evaluation_facctory.context_for('nico@split.io', ['prereq_chain'])
485+
assert e.eval_with_context('nico@split.io', 'nico@split.io', 'test_prereq', {'email': 'nico@split.io'}, ctx)['treatment'] == "on"
486+
487+
ctx = await evaluation_facctory.context_for('pato@split.io', ['prereq_chain'])
488+
assert e.eval_with_context('pato@split.io', 'pato@split.io', 'prereq_chain', {'email': 'pato@split.io'}, ctx)['treatment'] == "on_default"
489+
490+
ctx = await evaluation_facctory.context_for('mauro@split.io', ['prereq_chain'])
491+
assert e.eval_with_context('mauro@split.io', 'mauro@split.io', 'prereq_chain', {'email': 'mauro@split.io'}, ctx)['treatment'] == "on_default"
492+
391493
class EvaluationDataFactoryTests(object):
392494
"""Test evaluation factory class."""
393495

394496
def test_get_context(self):
395497
"""Test context."""
396-
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False)
498+
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [Prerequisites('split2', ['on'])])
499+
split2 = Split('split2', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [])
397500
flag_storage = InMemorySplitStorage([])
398501
segment_storage = InMemorySegmentStorage()
399502
rbs_segment_storage = InMemoryRuleBasedSegmentStorage()
400-
flag_storage.update([mocked_split], [], -1)
503+
flag_storage.update([mocked_split, split2], [], -1)
401504
rbs = copy.deepcopy(rbs_raw)
402505
rbs['conditions'].append(
403506
{"matcherGroup": {
@@ -421,6 +524,7 @@ def test_get_context(self):
421524
ec = eval_factory.context_for('bilal@split.io', ['some'])
422525
assert ec.rbs_segments == {'sample_rule_based_segment': rbs}
423526
assert ec.segment_memberships == {"employees": False}
527+
assert ec.flags.get("split2").name == "split2"
424528

425529
segment_storage.update("employees", {"mauro@split.io"}, {}, 1234)
426530
ec = eval_factory.context_for('mauro@split.io', ['some'])
@@ -433,11 +537,12 @@ class EvaluationDataFactoryAsyncTests(object):
433537
@pytest.mark.asyncio
434538
async def test_get_context(self):
435539
"""Test context."""
436-
mocked_split = Split('some', 123, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False)
540+
mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [Prerequisites('split2', ['on'])])
541+
split2 = Split('split2', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [])
437542
flag_storage = InMemorySplitStorageAsync([])
438543
segment_storage = InMemorySegmentStorageAsync()
439544
rbs_segment_storage = InMemoryRuleBasedSegmentStorageAsync()
440-
await flag_storage.update([mocked_split], [], -1)
545+
await flag_storage.update([mocked_split, split2], [], -1)
441546
rbs = copy.deepcopy(rbs_raw)
442547
rbs['conditions'].append(
443548
{"matcherGroup": {
@@ -461,6 +566,7 @@ async def test_get_context(self):
461566
ec = await eval_factory.context_for('bilal@split.io', ['some'])
462567
assert ec.rbs_segments == {'sample_rule_based_segment': rbs}
463568
assert ec.segment_memberships == {"employees": False}
569+
assert ec.flags.get("split2").name == "split2"
464570

465571
await segment_storage.update("employees", {"mauro@split.io"}, {}, 1234)
466572
ec = await eval_factory.context_for('mauro@split.io', ['some'])

0 commit comments

Comments
 (0)