Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 2 additions & 34 deletions ldclient/impl/async_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
_match_clause_by_kind,
_match_single_context_value,
_maybe_negate,
_target_match_result,
_variation_index_for_context,
check_targets,
error_reason
)
from ldclient.impl.events.types import EventFactory
Expand Down Expand Up @@ -80,7 +80,7 @@ async def _evaluate(self, flag: FeatureFlag, context: Context, state: EvalResult
return _get_off_value(flag, prereq_failure_reason)

# Check to see if any context targets match:
target_result = self._check_targets(flag, context)
target_result = check_targets(flag, context)
if target_result is not None:
return target_result

Expand Down Expand Up @@ -140,38 +140,6 @@ async def _check_prerequisites(self, flag: FeatureFlag, context: Context, state:
if state.prereq_stack is not None and len(state.prereq_stack) != 0:
state.prereq_stack.pop()

def _check_targets(self, flag: FeatureFlag, context: Context) -> Optional[EvaluationDetail]:
user_targets = flag.targets
context_targets = flag.context_targets
if len(context_targets) == 0:
# old-style data has only targets for users
if len(user_targets) != 0:
user_context = context.get_individual_context(Context.DEFAULT_KIND)
if user_context is None:
return None
key = user_context.key
for t in user_targets:
if key in t.values:
return _target_match_result(flag, t.variation)
return None
for t in context_targets:
kind = t.context_kind or Context.DEFAULT_KIND
var = t.variation
actual_context = context.get_individual_context(kind)
if actual_context is None:
continue
key = actual_context.key
if kind == Context.DEFAULT_KIND:
for ut in user_targets:
if ut.variation == var:
if key in ut.values:
return _target_match_result(flag, var)
break
continue
if key in t.values:
return _target_match_result(flag, var)
return None

async def _rule_matches_context(self, rule: FlagRule, context: Context, state: EvalResult) -> bool:
for clause in rule.clauses:
if not await self._clause_matches_context(clause, context, state):
Expand Down
36 changes: 2 additions & 34 deletions ldclient/impl/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
_match_clause_by_kind,
_match_single_context_value,
_maybe_negate,
_target_match_result,
_variation_index_for_context,
check_targets,
error_reason
)
from ldclient.impl.events.types import EventFactory
Expand Down Expand Up @@ -79,7 +79,7 @@ def _evaluate(self, flag: FeatureFlag, context: Context, state: EvalResult, even
return _get_off_value(flag, prereq_failure_reason)

# Check to see if any context targets match:
target_result = self._check_targets(flag, context)
target_result = check_targets(flag, context)
if target_result is not None:
return target_result

Expand Down Expand Up @@ -136,38 +136,6 @@ def _check_prerequisites(self, flag: FeatureFlag, context: Context, state: EvalR
if state.prereq_stack is not None and len(state.prereq_stack) != 0:
state.prereq_stack.pop()

def _check_targets(self, flag: FeatureFlag, context: Context) -> Optional[EvaluationDetail]:
user_targets = flag.targets
context_targets = flag.context_targets
if len(context_targets) == 0:
# old-style data has only targets for users
if len(user_targets) != 0:
user_context = context.get_individual_context(Context.DEFAULT_KIND)
if user_context is None:
return None
key = user_context.key
for t in user_targets:
if key in t.values:
return _target_match_result(flag, t.variation)
return None
for t in context_targets:
kind = t.context_kind or Context.DEFAULT_KIND
var = t.variation
actual_context = context.get_individual_context(kind)
if actual_context is None:
continue
key = actual_context.key
if kind == Context.DEFAULT_KIND:
for ut in user_targets:
if ut.variation == var:
if key in ut.values:
return _target_match_result(flag, var)
break
continue
if key in t.values:
return _target_match_result(flag, var)
return None

def _rule_matches_context(self, rule: FlagRule, context: Context, state: EvalResult) -> bool:
for clause in rule.clauses:
if not self._clause_matches_context(clause, context, state):
Expand Down
33 changes: 33 additions & 0 deletions ldclient/impl/evaluator_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,38 @@ def _target_match_result(flag: FeatureFlag, var: int) -> EvaluationDetail:
return _get_variation(flag, var, {'kind': 'TARGET_MATCH'})


def check_targets(flag: FeatureFlag, context: Context) -> Optional[EvaluationDetail]:
user_targets = flag.targets
context_targets = flag.context_targets
if len(context_targets) == 0:
# old-style data has only targets for users
if len(user_targets) != 0:
user_context = context.get_individual_context(Context.DEFAULT_KIND)
if user_context is None:
return None
key = user_context.key
for t in user_targets:
if key in t.values:
return _target_match_result(flag, t.variation)
return None
for t in context_targets:
kind = t.context_kind or Context.DEFAULT_KIND
var = t.variation
actual_context = context.get_individual_context(kind)
if actual_context is None:
continue
key = actual_context.key
if kind == Context.DEFAULT_KIND:
for ut in user_targets:
if ut.variation == var:
if key in ut.values:
return _target_match_result(flag, var)
break
continue
if key in t.values:
return _target_match_result(flag, var)
return None


def error_reason(error_kind: str) -> dict:
return {'kind': 'ERROR', 'errorKind': error_kind}
12 changes: 0 additions & 12 deletions ldclient/testing/impl/test_async_big_segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@
)
from ldclient.interfaces import AsyncBigSegmentStore, BigSegmentStoreMetadata

# ---------------------------------------------------------------------------
# Test doubles
# ---------------------------------------------------------------------------

user_key = 'user-key'
user_hash = _hash_for_user_key(user_key)

Expand Down Expand Up @@ -66,20 +62,12 @@ def membership_queries(self):
return list(self._membership_queries)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

async def make_started_manager(store, **kwargs):
config = AsyncBigSegmentsConfig(store=store, **kwargs)
# The constructor starts the polling task (it requires a running event loop).
return AsyncBigSegmentStoreManager(config)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_membership_query_uncached_result_healthy_status():
store = MockAsyncBigSegmentStore()
Expand Down
24 changes: 0 additions & 24 deletions ldclient/testing/impl/test_async_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
from ldclient.impl.model import *
from ldclient.testing.builders import *

# ---------------------------------------------------------------------------
# Test infrastructure
# ---------------------------------------------------------------------------

basic_user = Context.create('user-key')
fake_timestamp = 0
event_factory = EventFactory(False, lambda: fake_timestamp)
Expand Down Expand Up @@ -88,10 +84,6 @@ def assert_eval_result(result, expected_detail, expected_events):
assert result.events == expected_events


# ---------------------------------------------------------------------------
# Basic flag evaluation (on/off/fallthrough)
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_flag_returns_off_variation_if_flag_is_off():
flag = FlagBuilder('feature').on(False).off_variation(1).variations('a', 'b', 'c').build()
Expand Down Expand Up @@ -156,10 +148,6 @@ async def test_flag_returns_fallthrough_variation():
assert_eval_result(await basic_evaluator.evaluate(flag, user, event_factory), detail, None)


# ---------------------------------------------------------------------------
# Rule matching
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_flag_matches_user_from_rules():
rule = {'id': 'id', 'clauses': [{'attribute': 'key', 'op': 'in', 'values': ['userkey']}], 'variation': 0}
Expand Down Expand Up @@ -187,10 +175,6 @@ async def test_flag_returns_error_if_rule_variation_is_negative():
assert_eval_result(await basic_evaluator.evaluate(flag, user, event_factory), detail, None)


# ---------------------------------------------------------------------------
# Prerequisite evaluation
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_flag_returns_off_variation_if_prerequisite_not_found():
flag = FlagBuilder('feature').on(True).off_variation(1).variations('a', 'b', 'c').fallthrough_variation(1).prerequisite('badfeature', 1).build()
Expand Down Expand Up @@ -250,10 +234,6 @@ async def test_prerequisite_cycle_detection(depth: int):
assert_eval_result(await evaluator.evaluate(flags[0], context, event_factory), detail, None)


# ---------------------------------------------------------------------------
# Segment matching
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_segment_match_clause_retrieves_segment_from_store():
segment = SegmentBuilder('segkey').included('foo').build()
Expand Down Expand Up @@ -321,10 +301,6 @@ async def test_segment_cycle_detection(depth: int):
assert result.detail.reason == {'kind': 'ERROR', 'errorKind': 'MALFORMED_FLAG'}


# ---------------------------------------------------------------------------
# Big segment matching — verifies await is correctly called
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_big_segment_with_no_generation_is_not_matched():
segment = SegmentBuilder('key').version(1).included(basic_user.key).unbounded(True).build()
Expand Down
12 changes: 0 additions & 12 deletions ldclient/testing/integrations/test_async_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@
DEFAULT_PREFIX = 'launchdarkly'
FAKE_USER_HASH = 'userhash'

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def sync_redis_client():
"""Return a synchronous Redis client for test setup/teardown."""
Expand Down Expand Up @@ -85,10 +81,6 @@ def make_store(prefix=None):
return Redis.async_big_segment_store(prefix=prefix)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(params=[None, 'testprefix'])
def prefix(request):
return request.param
Expand All @@ -110,10 +102,6 @@ def clear_before_each(prefix):
pass


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
@pytest.mark.skipif(skip_database_tests, reason="skipping database tests")
@pytest.mark.skipif(not have_sync_redis, reason="skipping: sync redis not available for test setup")
Expand Down
12 changes: 0 additions & 12 deletions ldclient/testing/test_async_flag_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@
from ldclient.impl.listeners import Listeners
from ldclient.interfaces import FlagChange, FlagValueChange

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def context():
Expand Down Expand Up @@ -49,10 +45,6 @@ async def tracker(listeners, eval_fn):
return AsyncFlagTrackerImpl(listeners, eval_fn)


# ---------------------------------------------------------------------------
# AsyncFlagValueChangeListener tests
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_create_initializes_value_without_notifying(tracker, context, eval_fn):
changes = []
Expand Down Expand Up @@ -123,10 +115,6 @@ def sync_listener(change: FlagValueChange):
assert len(was_called) == 1


# ---------------------------------------------------------------------------
# AsyncFlagTrackerImpl tests
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_add_flag_value_change_listener_returns_listener(tracker, context):
listener = await tracker.add_flag_value_change_listener('flag-key', context, lambda c: None)
Expand Down
18 changes: 1 addition & 17 deletions ldclient/testing/test_async_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,8 @@
log = logging.getLogger('ldclient')


# ---------------------------------------------------------------------------
# Helpers: inline dispatch function that mirrors AsyncLDClient behaviour
# ---------------------------------------------------------------------------

async def _try_execute_stage_async(method, hook_name, coro_or_fn):
"""Execute a single hook stage, catching and logging any exceptions."""
"""Execute a single hook stage the way AsyncLDClient does, catching and logging any exceptions."""
try:
return await coro_or_fn()
except BaseException as e:
Expand Down Expand Up @@ -63,10 +59,6 @@ async def _evaluate_with_hooks(hooks, series_context, eval_fn):
return detail


# ---------------------------------------------------------------------------
# Concrete hook implementations for testing
# ---------------------------------------------------------------------------

class RecordingAsyncHook(AsyncHook):
"""Async hook that records calls and threads a counter through data."""

Expand All @@ -89,10 +81,6 @@ async def after_evaluation(self, series_context: EvaluationSeriesContext, data:
return {**data, self._name + '_after': True}


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def series_context():
return EvaluationSeriesContext(
Expand All @@ -115,10 +103,6 @@ async def _fn():
return _fn


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_async_hook_before_and_after_awaited(series_context, eval_fn):
hook = RecordingAsyncHook('async1')
Expand Down
Loading