diff --git a/ldclient/impl/async_evaluator.py b/ldclient/impl/async_evaluator.py index 43dd6eb9..f4bb25ca 100644 --- a/ldclient/impl/async_evaluator.py +++ b/ldclient/impl/async_evaluator.py @@ -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 @@ -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 @@ -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): diff --git a/ldclient/impl/evaluator.py b/ldclient/impl/evaluator.py index 25584f58..08a84229 100644 --- a/ldclient/impl/evaluator.py +++ b/ldclient/impl/evaluator.py @@ -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 @@ -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 @@ -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): diff --git a/ldclient/impl/evaluator_common.py b/ldclient/impl/evaluator_common.py index 52a03977..04dbebfd 100644 --- a/ldclient/impl/evaluator_common.py +++ b/ldclient/impl/evaluator_common.py @@ -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} diff --git a/ldclient/testing/impl/test_async_big_segments.py b/ldclient/testing/impl/test_async_big_segments.py index fe83ea29..1d970108 100644 --- a/ldclient/testing/impl/test_async_big_segments.py +++ b/ldclient/testing/impl/test_async_big_segments.py @@ -17,10 +17,6 @@ ) from ldclient.interfaces import AsyncBigSegmentStore, BigSegmentStoreMetadata -# --------------------------------------------------------------------------- -# Test doubles -# --------------------------------------------------------------------------- - user_key = 'user-key' user_hash = _hash_for_user_key(user_key) @@ -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() diff --git a/ldclient/testing/impl/test_async_evaluator.py b/ldclient/testing/impl/test_async_evaluator.py index 3af7ac16..52b84dda 100644 --- a/ldclient/testing/impl/test_async_evaluator.py +++ b/ldclient/testing/impl/test_async_evaluator.py @@ -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) @@ -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() @@ -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} @@ -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() @@ -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() @@ -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() diff --git a/ldclient/testing/integrations/test_async_redis.py b/ldclient/testing/integrations/test_async_redis.py index 3ebb88f3..df4afdf4 100644 --- a/ldclient/testing/integrations/test_async_redis.py +++ b/ldclient/testing/integrations/test_async_redis.py @@ -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.""" @@ -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 @@ -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") diff --git a/ldclient/testing/test_async_flag_tracker.py b/ldclient/testing/test_async_flag_tracker.py index 68222d7e..57d750c9 100644 --- a/ldclient/testing/test_async_flag_tracker.py +++ b/ldclient/testing/test_async_flag_tracker.py @@ -15,10 +15,6 @@ from ldclient.impl.listeners import Listeners from ldclient.interfaces import FlagChange, FlagValueChange -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - @pytest.fixture def context(): @@ -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 = [] @@ -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) diff --git a/ldclient/testing/test_async_hooks.py b/ldclient/testing/test_async_hooks.py index 3f0a6c78..cd007b37 100644 --- a/ldclient/testing/test_async_hooks.py +++ b/ldclient/testing/test_async_hooks.py @@ -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: @@ -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.""" @@ -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( @@ -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')