Skip to content

Commit

Permalink
Merge pull request #198 from launchdarkly/eb/sc-180187/eval-4-segment…
Browse files Browse the repository at this point in the history
…-target

(U2C 10) support includedContexts/excludedContexts in segment
  • Loading branch information
eli-darkly authored Dec 20, 2022
2 parents 65f50c6 + 9a1f932 commit 364e2eb
Showing 4 changed files with 119 additions and 146 deletions.
4 changes: 0 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -29,10 +29,6 @@ TEST_HARNESS_PARAMS := $(TEST_HARNESS_PARAMS) \
-skip 'evaluation/parameterized/attribute references' \
-skip 'evaluation/parameterized/bad attribute reference errors' \
-skip 'evaluation/parameterized/prerequisites' \
-skip 'evaluation/parameterized/segment match/included list is specific to user kind' \
-skip 'evaluation/parameterized/segment match/includedContexts' \
-skip 'evaluation/parameterized/segment match/excluded list is specific to user kind' \
-skip 'evaluation/parameterized/segment match/excludedContexts' \
-skip 'evaluation/parameterized/segment recursion' \
-skip 'events'

26 changes: 18 additions & 8 deletions ldclient/impl/evaluator.py
Original file line number Diff line number Diff line change
@@ -206,16 +206,20 @@ def _segment_matches_context(self, segment: dict, context: Context, state: EvalR
return self._simple_segment_match_context(segment, context, state, True)

def _simple_segment_match_context(self, segment: dict, context: Context, state: EvalResult, use_includes_and_excludes: bool) -> bool:
key = context.key
if key is not None:
if use_includes_and_excludes:
if key in segment.get('included', []):
if use_includes_and_excludes:
if _context_key_is_in_target_list(context, None, segment.get('included')):
return True
for t in segment.get('includedContexts') or []:
if _context_key_is_in_target_list(context, t.get('contextKind'), t.get('values')):
return True
if key in segment.get('excluded', []):
if _context_key_is_in_target_list(context, None, segment.get('excluded')):
return False
for t in segment.get('excludedContexts') or []:
if _context_key_is_in_target_list(context, t.get('contextKind'), t.get('values')):
return False
for rule in segment.get('rules', []):
if self._segment_rule_matches_context(rule, context, state, segment['key'], segment.get('salt', '')):
return True
for rule in segment.get('rules', []):
if self._segment_rule_matches_context(rule, context, state, segment['key'], segment.get('salt', '')):
return True
return False

def _segment_rule_matches_context(self, rule: dict, context: Context, state: EvalResult, segment_key: str, salt: str) -> bool:
@@ -352,6 +356,12 @@ def _bucketable_string_value(u_value) -> Optional[str]:

return None

def _context_key_is_in_target_list(context: Context, context_kind: Optional[str], keys: Optional[List[str]]) -> bool:
if keys is None or len(keys) == 0:
return False
match_context = context.get_individual_context(context_kind or Context.DEFAULT_KIND)
return match_context is not None and match_context.key in keys

def _match_single_context_value(op: str, context_value: Any, values: List[Any]) -> bool:
op_fn = operators.ops.get(op)
if op_fn is None:
19 changes: 18 additions & 1 deletion testing/builders.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ def _append(self, key: str, item: dict):
self.data[key].append(item)
return self

def _append_all(self, key: str, items: List[dict]):
def _append_all(self, key: str, items: List[Any]):
self.data[key].extend(items)
return self

@@ -86,6 +86,8 @@ def __init__(self, key):
'version': 1,
'included': [],
'excluded': [],
'includedContexts': [],
'excludedContexts': [],
'rules': [],
'unbounded': False
})
@@ -96,6 +98,18 @@ def key(self, key: str) -> SegmentBuilder:
def version(self, version: int) -> SegmentBuilder:
return self._set('key', version)

def excluded(self, *keys: str) -> SegmentBuilder:
return self._append_all('excluded', list(keys))

def excluded_contexts(self, context_kind: str, *keys: str) -> SegmentBuilder:
return self._append('excludedContexts', {'contextKind': context_kind, 'values': list(keys)})

def included(self, *keys: str) -> SegmentBuilder:
return self._append_all('included', list(keys))

def included_contexts(self, context_kind: str, *keys: str) -> SegmentBuilder:
return self._append('includedContexts', {'contextKind': context_kind, 'values': list(keys)})

def salt(self, salt: str) -> SegmentBuilder:
return self._set('salt', salt)

@@ -141,6 +155,9 @@ def make_clause_matching_context(context: Context) -> dict:
def make_clause_matching_segment_key(*segment_keys: str) -> dict:
return {'attribute': '', 'op': 'segmentMatch', 'values': list(segment_keys)}

def make_segment_rule_matching_context(context: Context) -> dict:
return SegmentRuleBuilder().clauses(make_clause_matching_context(context)).build()

def negate_clause(clause: dict) -> dict:
c = clause.copy()
c['negate'] = not c.get('negate')
216 changes: 83 additions & 133 deletions testing/impl/test_evaluator_segment.py
Original file line number Diff line number Diff line change
@@ -48,107 +48,81 @@ def verify_rollout(


def test_explicit_include_user():
s = {
"key": "test",
"included": [ "foo" ],
"version": 1
}
u = Context.create('foo')
assert _segment_matches_context(s, u) is True
user = Context.create('foo')
segment = SegmentBuilder('test').included(user.key).build()
assert _segment_matches_context(segment, user) is True

def test_explicit_exclude_user():
s = {
"key": "test",
"excluded": [ "foo" ],
"version": 1
}
u = Context.create('foo')
assert _segment_matches_context(s, u) is False
user = Context.create('foo')
segment = SegmentBuilder('test').excluded(user.key) \
.rules(make_segment_rule_matching_context(user)) \
.build()
assert _segment_matches_context(segment, user) is False

def test_explicit_include_has_precedence():
s = {
"key": "test",
"included": [ "foo" ],
"excluded": [ "foo" ],
"version": 1
}
u = Context.create('foo')
assert _segment_matches_context(s, u) is True
user = Context.create('foo')
segment = SegmentBuilder('test').included(user.key).excluded(user.key).build()
assert _segment_matches_context(segment, user) is True

def test_included_key_for_context_kind():
c1 = Context.create('key1', 'kind1')
c2 = Context.create('key2', 'kind2')
multi = Context.create_multi(c1, c2)
segment = SegmentBuilder('test').included_contexts('kind1', 'key1').build()
assert _segment_matches_context(segment, c1) is True
assert _segment_matches_context(segment, c2) is False
assert _segment_matches_context(segment, multi) is True

def test_excluded_key_for_context_kind():
c1 = Context.create('key1', 'kind1')
c2 = Context.create('key2', 'kind2')
multi = Context.create_multi(c1, c2)
segment = SegmentBuilder('test') \
.excluded_contexts('kind1', 'key1') \
.rules(
make_segment_rule_matching_context(c1),
make_segment_rule_matching_context(c2)
) \
.build()
assert _segment_matches_context(segment, c1) is False
assert _segment_matches_context(segment, c2) is True
assert _segment_matches_context(segment, multi) is False

def test_matching_rule_with_no_weight():
s = {
"key": "test",
"rules": [
{
"clauses": [
{
"attribute": "email",
"op": "in",
"values": [ "test@example.com" ]
}
]
}
]
}
u = Context.builder('foo').set('email', 'test@example.com').build()
assert _segment_matches_context(s, u) is True
context = Context.create('foo')
segment = SegmentBuilder('test') \
.rules(
SegmentRuleBuilder().clauses(make_clause_matching_context(context)).build()
) \
.build()
assert _segment_matches_context(segment, context) is True

def test_matching_rule_with_none_weight():
s = {
"key": "test",
"rules": [
{
"clauses": [
{
"attribute": "email",
"op": "in",
"values": [ "test@example.com" ]
}
],
"weight": None
}
]
}
u = Context.builder('foo').set('email', 'test@example.com').build()
assert _segment_matches_context(s, u) is True
context = Context.create('foo')
segment = SegmentBuilder('test') \
.rules(
SegmentRuleBuilder().weight(None).clauses(make_clause_matching_context(context)).build()
) \
.build()
assert _segment_matches_context(segment, context) is True

def test_matching_rule_with_full_rollout():
s = {
"key": "test",
"rules": [
{
"clauses": [
{
"attribute": "email",
"op": "in",
"values": [ "test@example.com" ]
}
],
"weight": 100000
}
]
}
u = Context.builder('foo').set('email', 'test@example.com').build()
assert _segment_matches_context(s, u) is True
context = Context.create('foo')
segment = SegmentBuilder('test') \
.rules(
SegmentRuleBuilder().weight(100000).clauses(make_clause_matching_context(context)).build()
) \
.build()
assert _segment_matches_context(segment, context) is True

def test_matching_rule_with_zero_rollout():
s = {
"key": "test",
"rules": [
{
"clauses": [
{
"attribute": "email",
"op": "in",
"values": [ "test@example.com" ]
}
],
"weight": 0
}
]
}
u = Context.builder('foo').set('email', 'test@example.com').build()
assert _segment_matches_context(s, u) is False
context = Context.create('foo')
segment = SegmentBuilder('test') \
.rules(
SegmentRuleBuilder().weight(0).clauses(make_clause_matching_context(context)).build()
) \
.build()
assert _segment_matches_context(segment, context) is False

def test_rollout_calculation_can_bucket_by_key():
context = Context.builder('userkey').name('Bob').build()
@@ -162,49 +136,25 @@ def test_rollout_uses_context_kind():
verify_rollout(multi, context2, expected_bucket_value, 'test', 'salt', None, 'kind2')

def test_matching_rule_with_multiple_clauses():
s = {
"key": "test",
"rules": [
{
"clauses": [
{
"attribute": "email",
"op": "in",
"values": [ "test@example.com" ]
},
{
"attribute": "name",
"op": "in",
"values": [ "bob" ]
}
],
"weight": 100000
}
]
}
u = Context.builder('foo').name('bob').set('email', 'test@example.com').build()
assert _segment_matches_context(s, u) is True
context = Context.builder('foo').name('bob').set('email', 'test@example.com').build()
segment = SegmentBuilder('test') \
.rules(
SegmentRuleBuilder().clauses(
make_clause(None, 'email', 'in', 'test@example.com'),
make_clause(None, 'name', 'in', 'bob')
).build()
) \
.build()
assert _segment_matches_context(segment, context) is True

def test_non_matching_rule_with_multiple_clauses():
s = {
"key": "test",
"rules": [
{
"clauses": [
{
"attribute": "email",
"op": "in",
"values": [ "test@example.com" ]
},
{
"attribute": "name",
"op": "in",
"values": [ "bill" ]
}
],
"weight": 100000
}
]
}
u = Context.builder('foo').name('bob').set('email', 'test@example.com').build()
assert _segment_matches_context(s, u) is False
context = Context.builder('foo').name('bob').set('email', 'test@example.com').build()
segment = SegmentBuilder('test') \
.rules(
SegmentRuleBuilder().clauses(
make_clause(None, 'email', 'in', 'test@example.com'),
make_clause(None, 'name', 'in', 'bill')
).build()
) \
.build()
assert _segment_matches_context(segment, context) is False

0 comments on commit 364e2eb

Please sign in to comment.