def test_flag_returns_error_if_rule_has_rollout_with_no_variations(): rule = { 'id': 'id', 'clauses': [{'attribute': 'key', 'op': 'in', 'values': ['userkey']}], 'rollout': {'variations': []} } flag = make_boolean_flag_with_rules([rule]) user = { 'key': 'userkey' } detail = EvaluationDetail(None, None, {'kind': 'ERROR', 'errorKind': 'MALFORMED_FLAG'}) assert evaluate(flag, user, empty_store, event_factory) == EvalResult(detail, [])
def test_flag_returns_off_variation_and_event_if_prerequisite_is_off(): store = InMemoryFeatureStore() flag = { 'key': 'feature0', 'on': True, 'prerequisites': [{'key': 'feature1', 'variation': 1}], 'fallthrough': { 'variation': 0 }, 'offVariation': 1, 'variations': ['a', 'b', 'c'], 'version': 1 } flag1 = { 'key': 'feature1', 'off': False, 'offVariation': 1, # note that even though it returns the desired variation, it is still off and therefore not a match 'fallthrough': { 'variation': 0 }, 'variations': ['d', 'e'], 'version': 2, 'trackEvents': False } store.upsert(FEATURES, flag1) user = { 'key': 'x' } detail = EvaluationDetail('b', 1, {'kind': 'PREREQUISITE_FAILED', 'prerequisiteKey': 'feature1'}) events_should_be = [{'kind': 'feature', 'key': 'feature1', 'variation': 1, 'value': 'e', 'default': None, 'version': 2, 'user': user, 'prereqOf': 'feature0'}] assert evaluate(flag, user, store, event_factory) == EvalResult(detail, events_should_be)
def test_flag_returns_fallthrough_and_event_if_prereq_is_met_and_there_are_no_rules(): store = InMemoryFeatureStore() flag = { 'key': 'feature0', 'on': True, 'prerequisites': [{ 'key': 'feature1', 'variation': 1 }], 'fallthrough': { 'variation': 0 }, 'offVariation': 1, 'variations': ['a', 'b', 'c'], 'version': 1 } flag1 = { 'key': 'feature1', 'on': True, 'fallthrough': { 'variation': 1 }, 'variations': ['d', 'e'], 'version': 2, 'trackEvents': False } store.upsert(FEATURES, flag1) user = { 'key': 'x' } detail = EvaluationDetail('a', 0, {'kind': 'FALLTHROUGH'}) events_should_be = [{'kind': 'feature', 'key': 'feature1', 'variation': 1, 'value': 'e', 'default': None, 'version': 2, 'user': user, 'prereqOf': 'feature0'}] assert evaluate(flag, user, store, event_factory) == EvalResult(detail, events_should_be)
def test_variation_when_user_has_no_key(): feature = make_off_flag_with_value('feature.key', 'value') store = InMemoryFeatureStore() store.init({FEATURES: {'feature.key': feature}}) client = make_client(store) expected = EvaluationDetail('default', None, {'kind': 'ERROR', 'errorKind': 'USER_NOT_SPECIFIED'}) assert expected == client.variation_detail('feature.key', { }, default='default')
def test_variation_detail_for_existing_feature(): feature = make_off_flag_with_value('feature.key', 'value') store = InMemoryFeatureStore() store.init({FEATURES: {'feature.key': feature}}) client = make_client(store) expected = EvaluationDetail('value', 0, {'kind': 'OFF'}) assert expected == client.variation_detail('feature.key', user, default='default')
def test_flag_returns_none_if_flag_is_off_and_off_variation_is_unspecified(): flag = { 'key': 'feature', 'on': False, 'variations': ['a', 'b', 'c'] } user = { 'key': 'x' } detail = EvaluationDetail(None, None, {'kind': 'OFF'}) assert evaluate(flag, user, empty_store, event_factory) == EvalResult(detail, [])
def test_variation_detail_for_flag_that_evaluates_to_none(): empty_flag = {'key': 'feature.key', 'on': False, 'offVariation': None} store = InMemoryFeatureStore() store.init({FEATURES: {'feature.key': empty_flag}}) client = make_client(store) expected = EvaluationDetail('default', None, {'kind': 'OFF'}) actual = client.variation_detail('feature.key', user, default='default') assert expected == actual assert actual.is_default_value() == True
def test_variation_detail_when_feature_store_throws_error(caplog): store = ErroringFeatureStore() client = make_client(store) expected = EvaluationDetail('default', None, {'kind': 'ERROR', 'errorKind': 'EXCEPTION'}) actual = client.variation_detail('feature.key', { "key": "user" }, default='default') assert expected == actual assert actual.is_default_value() == True errlog = get_log_lines(caplog, 'ERROR') assert errlog == [ 'Unexpected error while retrieving feature flag "feature.key": NotImplementedError()' ]
def test_flag_returns_error_if_off_variation_is_negative(): flag = { 'key': 'feature', 'on': False, 'offVariation': -1, 'variations': ['a', 'b', 'c'] } user = { 'key': 'x' } detail = EvaluationDetail(None, None, {'kind': 'ERROR', 'errorKind': 'MALFORMED_FLAG'}) assert evaluate(flag, user, empty_store, event_factory) == EvalResult(detail, [])
def test_flag_returns_error_if_fallthrough_has_no_variation_or_rollout(): flag = { 'key': 'feature', 'on': True, 'fallthrough': {}, 'variations': ['a', 'b', 'c'] } user = { 'key': 'x' } detail = EvaluationDetail(None, None, {'kind': 'ERROR', 'errorKind': 'MALFORMED_FLAG'}) assert evaluate(flag, user, empty_store, event_factory) == EvalResult(detail, [])
def test_flag_returns_off_variation_if_flag_is_off(): flag = { 'key': 'feature', 'on': False, 'offVariation': 1, 'variations': ['a', 'b', 'c'] } user = {'key': 'x'} detail = EvaluationDetail('b', 1, {'kind': 'OFF'}) assert evaluate(flag, user, empty_store) == EvalResult(detail, [])
def evaluate(flag, user, store, event_factory): sanitized_user = stringify_attrs( user, __USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION__) value = flag.get('value') version = flag.get('version') variation_index = flag.get('variation') reason = flag.get('reason') detail = EvaluationDetail(value, variation_index, reason) return EvalResult(detail=detail, events=None)
def test_variation_detail_for_unknown_feature(): store = InMemoryFeatureStore() client = make_client(store) expected = EvaluationDetail('default', None, { 'kind': 'ERROR', 'errorKind': 'FLAG_NOT_FOUND' }) assert expected == client.variation_detail('feature.key', user, default='default')
def test_flag_returns_off_variation_if_prerequisite_not_found(): flag = { 'key': 'feature0', 'on': True, 'prerequisites': [{'key': 'badfeature', 'variation': 1}], 'fallthrough': { 'variation': 0 }, 'offVariation': 1, 'variations': ['a', 'b', 'c'] } user = { 'key': 'x' } detail = EvaluationDetail('b', 1, {'kind': 'PREREQUISITE_FAILED', 'prerequisiteKey': 'badfeature'}) assert evaluate(flag, user, empty_store, event_factory) == EvalResult(detail, [])
def test_flag_matches_user_from_targets(): flag = { 'key': 'feature0', 'on': True, 'targets': [{ 'values': ['whoever', 'userkey'], 'variation': 2 }], 'fallthrough': { 'variation': 0 }, 'offVariation': 1, 'variations': ['a', 'b', 'c'] } user = { 'key': 'userkey' } detail = EvaluationDetail('c', 2, {'kind': 'TARGET_MATCH'}) assert evaluate(flag, user, empty_store, event_factory) == EvalResult(detail, [])
def _evaluate_internal(self, key, user, default, include_reasons_in_events): default = self._config.get_default(key, default) if self._config.offline: return EvaluationDetail(default, None, error_reason('CLIENT_NOT_READY')) if user is not None: self._sanitize_user(user) def send_event(value, variation=None, flag=None, reason=None): self._send_event({'kind': 'feature', 'key': key, 'user': user, 'value': value, 'variation': variation, 'default': default, 'version': flag.get('version') if flag else None, 'trackEvents': flag.get('trackEvents') if flag else None, 'debugEventsUntilDate': flag.get('debugEventsUntilDate') if flag else None, 'reason': reason if include_reasons_in_events else None}) if not self.is_initialized(): if self._store.initialized: log.warn("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key) else: log.warn("Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: " + str(default) + " for feature key: " + key) reason = error_reason('CLIENT_NOT_READY') send_event(default, None, None, reason) return EvaluationDetail(default, None, reason) if user is not None and user.get('key', "") == "": log.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly.") flag = self._store.get(FEATURES, key, lambda x: x) if not flag: reason = error_reason('FLAG_NOT_FOUND') send_event(default, None, None, reason) return EvaluationDetail(default, None, reason) else: if user is None or user.get('key') is None: reason = error_reason('USER_NOT_SPECIFIED') send_event(default, None, flag, reason) return EvaluationDetail(default, None, reason) try: result = evaluate(flag, user, self._store, include_reasons_in_events) for event in result.events or []: self._send_event(event) detail = result.detail if detail.is_default_value(): detail = EvaluationDetail(default, None, detail.reason) send_event(detail.value, detail.variation_index, flag, detail.reason) return detail except Exception as e: log.error("Unexpected error while evaluating feature flag \"%s\": %s" % (key, e)) log.debug(traceback.format_exc()) reason = error_reason('EXCEPTION') send_event(default, None, flag, reason) return EvaluationDetail(default, None, reason)
def test_flag_returns_error_if_fallthrough_variation_is_too_high(): flag = { 'key': 'feature', 'on': True, 'fallthrough': { 'variation': 999 }, 'variations': ['a', 'b', 'c'] } user = {'key': 'x'} detail = EvaluationDetail(None, None, { 'kind': 'ERROR', 'errorKind': 'MALFORMED_FLAG' }) assert evaluate(flag, user, empty_store) == EvalResult(detail, [])
def test_flag_returns_error_if_rule_variation_is_negative(): rule = { 'id': 'id', 'clauses': [{ 'attribute': 'key', 'op': 'in', 'values': ['userkey'] }], 'variation': -1 } flag = make_boolean_flag_with_rules([rule]) user = {'key': 'userkey'} detail = EvaluationDetail(None, None, { 'kind': 'ERROR', 'errorKind': 'MALFORMED_FLAG' }) assert evaluate(flag, user, empty_store) == EvalResult(detail, [])
def test_flag_returns_off_variation_and_event_if_prerequisite_is_not_met(): store = InMemoryFeatureStore() flag = { 'key': 'feature0', 'on': True, 'prerequisites': [{ 'key': 'feature1', 'variation': 1 }], 'fallthrough': { 'variation': 0 }, 'offVariation': 1, 'variations': ['a', 'b', 'c'], 'version': 1 } flag1 = { 'key': 'feature1', 'on': True, 'fallthrough': { 'variation': 0 }, 'variations': ['d', 'e'], 'version': 2, 'trackEvents': False } store.upsert(FEATURES, flag1) user = {'key': 'x'} detail = EvaluationDetail('b', 1, { 'kind': 'PREREQUISITE_FAILED', 'prerequisiteKey': 'feature1' }) events_should_be = [{ 'kind': 'feature', 'key': 'feature1', 'variation': 0, 'value': 'd', 'version': 2, 'user': user, 'prereqOf': 'feature0', 'trackEvents': False, 'debugEventsUntilDate': None, 'reason': None }] assert evaluate(flag, user, store) == EvalResult(detail, events_should_be)
def test_flag_matches_user_from_rules(): rule = { 'id': 'id', 'clauses': [{ 'attribute': 'key', 'op': 'in', 'values': ['userkey'] }], 'variation': 1 } flag = make_boolean_flag_with_rules([rule]) user = {'key': 'userkey'} detail = EvaluationDetail(True, 1, { 'kind': 'RULE_MATCH', 'ruleIndex': 0, 'ruleId': 'id' }) assert evaluate(flag, user, empty_store) == EvalResult(detail, [])
def _evaluate_internal(self, key, user, default, event_factory): default = self._config.get_default(key, default) if self._config.offline: return EvaluationDetail(default, None, error_reason('CLIENT_NOT_READY')) if not self.is_initialized(): if self._store.initialized: log.warning( "Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key) else: log.warning( "Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: " + str(default) + " for feature key: " + key) reason = error_reason('CLIENT_NOT_READY') self._send_event( event_factory.new_unknown_flag_event( key, user, default, reason)) return EvaluationDetail(default, None, reason) if user is not None and user.get('key', "") == "": log.warning( "User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly." ) try: flag = self._store.get(FEATURES, key, lambda x: x) except Exception as e: log.error( "Unexpected error while retrieving feature flag \"%s\": %s" % (key, repr(e))) log.debug(traceback.format_exc()) reason = error_reason('EXCEPTION') self._send_event( event_factory.new_unknown_flag_event(key, user, default, reason)) return EvaluationDetail(default, None, reason) if not flag: reason = error_reason('FLAG_NOT_FOUND') self._send_event( event_factory.new_unknown_flag_event(key, user, default, reason)) return EvaluationDetail(default, None, reason) else: if user is None or user.get('key') is None: reason = error_reason('USER_NOT_SPECIFIED') self._send_event( event_factory.new_default_event(flag, user, default, reason)) return EvaluationDetail(default, None, reason) try: result = evaluate(flag, user, self._store, event_factory) for event in result.events or []: self._send_event(event) detail = result.detail if detail.is_default_value(): detail = EvaluationDetail(default, None, detail.reason) self._send_event( event_factory.new_eval_event(flag, user, detail, default)) return detail except Exception as e: log.error( "Unexpected error while evaluating feature flag \"%s\": %s" % (key, repr(e))) log.debug(traceback.format_exc()) reason = error_reason('EXCEPTION') self._send_event( event_factory.new_default_event(flag, user, default, reason)) return EvaluationDetail(default, None, reason)