From 50f5b761304febc9d0a231f56cf932d3c1fbf226 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 28 Aug 2025 10:43:20 -0700 Subject: [PATCH 1/4] Update client class --- splitio/client/client.py | 89 +++-- splitio/client/input_validator.py | 11 +- splitio/client/util.py | 19 + splitio/engine/evaluator.py | 23 +- tests/client/test_client.py | 577 ++++++++++++++++++++++++++++++ 5 files changed, 661 insertions(+), 58 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 257c9b97..947da98c 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -2,6 +2,7 @@ import logging import json from collections import namedtuple +import copy from splitio.engine.evaluator import Evaluator, CONTROL, EvaluationDataFactory, AsyncEvaluationDataFactory from splitio.engine.splitters import Splitter @@ -9,6 +10,7 @@ from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies from splitio.client import input_validator +from splitio.client.util import get_fallback_treatment_and_label from splitio.util.time import get_current_epoch_time_ms, utctime_ms @@ -39,7 +41,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes 'impressions_disabled': False } - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -64,6 +66,7 @@ def __init__(self, factory, recorder, labels_enabled=True): self._evaluator = Evaluator(self._splitter) self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer + self._fallback_treatments_configuration = fallback_treatments_configuration @property def ready(self): @@ -203,11 +206,23 @@ def _validate_track(self, key, traffic_type, event_type, value=None, properties= def _get_properties(self, evaluation_options): return evaluation_options.properties if evaluation_options != None else None + def _get_fallback_treatment_with_config(self, treatment, feature): + label = "" + + label, treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature, treatment, label, _LOGGER) + return treatment, config + + def _get_fallback_eval_results(self, eval_result, feature): + result = copy.deepcopy(eval_result) + result["impression"]["label"], result["treatment"], result["configurations"] = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature, result["treatment"], result["impression"]["label"], _LOGGER) + return result class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -222,7 +237,7 @@ def __init__(self, factory, recorder, labels_enabled=True): :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled) + ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration) self._context_factory = EvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) def destroy(self): @@ -254,10 +269,11 @@ def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_opti try: treatment, _ = self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, evaluation_options) return treatment - + except: _LOGGER.error('get_treatment failed') - return CONTROL + treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + return treatment def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ @@ -282,8 +298,8 @@ def get_treatment_with_config(self, key, feature_flag_name, attributes=None, eva except Exception: _LOGGER.error('get_treatment_with_config failed') - return CONTROL, None - + return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None): """ Validate key, feature flag name and object, and get the treatment and config with an optional dictionary of attributes. @@ -302,7 +318,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio :rtype: dict """ if not self._client_is_usable(): # not destroyed & not waiting for a fork - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) start = get_current_epoch_time_ms() if not self.ready: @@ -312,9 +328,10 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio try: key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) - result = self._NON_READY_EVAL_RESULT + result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature) + if self.ready: try: ctx = self._context_factory.context_for(key, [feature]) @@ -324,15 +341,15 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) self._telemetry_evaluation_producer.record_exception(method) - result = self._FAILED_EVAL_RESULT + result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] != Label.SPLIT_NOT_FOUND: + if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1: impression_decorated = self._build_impression(key, bucketing, feature, result, properties) self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] - + def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ Evaluate multiple feature flags and return a dictionary with all the feature flag/treatments. @@ -356,7 +373,7 @@ def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_op return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: - return {feature: CONTROL for feature in feature_flag_names} + return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names} def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ @@ -380,7 +397,7 @@ def get_treatments_with_config(self, key, feature_flag_names, attributes=None, e return self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options) except Exception: - return {feature: (CONTROL, None) for feature in feature_flag_names} + return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names} def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ @@ -604,7 +621,7 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt """ start = get_current_epoch_time_ms() if not self._client_is_usable(): - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) if not self.ready: _LOGGER.error("Client is not ready - no calls possible") @@ -613,9 +630,9 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt try: key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options) except _InvalidInputError: - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) - results = {n: self._NON_READY_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features} if self.ready: try: ctx = self._context_factory.context_for(key, features) @@ -625,12 +642,12 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) self._telemetry_evaluation_producer.record_exception(method) - results = {n: self._FAILED_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, n) for n in features} properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) - if i.Impression.label != Label.SPLIT_NOT_FOUND + if i.Impression.label.find(Label.SPLIT_NOT_FOUND) == -1 ] self._record_stats(imp_decorated_attrs, start, method) @@ -706,7 +723,7 @@ def track(self, key, traffic_type, event_type, value=None, properties=None): class ClientAsync(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -721,7 +738,7 @@ def __init__(self, factory, recorder, labels_enabled=True): :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled) + ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration) self._context_factory = AsyncEvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) async def destroy(self): @@ -756,7 +773,8 @@ async def get_treatment(self, key, feature_flag_name, attributes=None, evaluatio except: _LOGGER.error('get_treatment failed') - return CONTROL + treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + return treatment async def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ @@ -781,7 +799,7 @@ async def get_treatment_with_config(self, key, feature_flag_name, attributes=Non except Exception: _LOGGER.error('get_treatment_with_config failed') - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) async def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None): """ @@ -801,7 +819,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation :rtype: dict """ if not self._client_is_usable(): # not destroyed & not waiting for a fork - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) start = get_current_epoch_time_ms() if not self.ready: @@ -811,9 +829,9 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation try: key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) - result = self._NON_READY_EVAL_RESULT + result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature) if self.ready: try: ctx = await self._context_factory.context_for(key, [feature]) @@ -823,10 +841,10 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) await self._telemetry_evaluation_producer.record_exception(method) - result = self._FAILED_EVAL_RESULT + result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] != Label.SPLIT_NOT_FOUND: + if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1: impression_decorated = self._build_impression(key, bucketing, feature, result, properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] @@ -854,7 +872,7 @@ async def get_treatments(self, key, feature_flag_names, attributes=None, evaluat return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: - return {feature: CONTROL for feature in feature_flag_names} + return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names} async def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ @@ -878,8 +896,7 @@ async def get_treatments_with_config(self, key, feature_flag_names, attributes=N return await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options) except Exception: - _LOGGER.error("AA", exc_info=True) - return {feature: (CONTROL, None) for feature in feature_flag_names} + return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names} async def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ @@ -1017,7 +1034,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati """ start = get_current_epoch_time_ms() if not self._client_is_usable(): - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) if not self.ready: _LOGGER.error("Client is not ready - no calls possible") @@ -1026,9 +1043,9 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati try: key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options) except _InvalidInputError: - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) - results = {n: self._NON_READY_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features} if self.ready: try: ctx = await self._context_factory.context_for(key, features) @@ -1038,7 +1055,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) await self._telemetry_evaluation_producer.record_exception(method) - results = {n: self._FAILED_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, n) for n in features} properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 51b8b0d2..2367816d 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -7,6 +7,7 @@ from splitio.client.key import Key from splitio.client import client +from splitio.client.util import get_fallback_treatment_and_label from splitio.engine.evaluator import CONTROL @@ -501,7 +502,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name valid_feature_flags.append(ff) return valid_feature_flags -def generate_control_treatments(feature_flags): +def generate_control_treatments(feature_flags, fallback_treatments_configuration): """ Generate valid feature flags to control. @@ -516,7 +517,13 @@ def generate_control_treatments(feature_flags): to_return = {} for feature_flag in feature_flags: if isinstance(feature_flag, str) and len(feature_flag.strip())> 0: - to_return[feature_flag] = (CONTROL, None) + treatment = CONTROL + config = None + label = "" + label, treatment, config = get_fallback_treatment_and_label(fallback_treatments_configuration, + feature_flag, treatment, label, _LOGGER) + + to_return[feature_flag] = (treatment, config) return to_return diff --git a/splitio/client/util.py b/splitio/client/util.py index e4892512..6541b9df 100644 --- a/splitio/client/util.py +++ b/splitio/client/util.py @@ -51,3 +51,22 @@ def get_metadata(config): version = 'python-%s' % __version__ ip_address, hostname = _get_hostname_and_ip(config) return SdkMetadata(version, hostname, ip_address) + +def get_fallback_treatment_and_label(fallback_treatments_configuration, feature_name, treatment, label, _logger): + if fallback_treatments_configuration == None or fallback_treatments_configuration.fallback_config == None: + return label, treatment, None + + if fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \ + fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None: + _logger.debug('Using Fallback Treatment for feature: %s', feature_name) + return fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ + fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \ + fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config + + if fallback_treatments_configuration.fallback_config.global_fallback_treatment != None: + _logger.debug('Using Global Fallback Treatment.') + return fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \ + fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \ + fallback_treatments_configuration.fallback_config.global_fallback_treatment.config + + return label, treatment, None diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index 8088b450..2a564d3a 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -2,6 +2,7 @@ import logging from collections import namedtuple +from splitio.client.util import get_fallback_treatment_and_label from splitio.models.impressions import Label from splitio.models.grammar.condition import ConditionType from splitio.models.grammar.matchers.misc import DependencyMatcher @@ -52,7 +53,8 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): if not feature: _LOGGER.warning('Unknown or invalid feature: %s', feature) label = Label.SPLIT_NOT_FOUND - label, _treatment, config = self._get_fallback_treatment_and_label(feature_name, _treatment, label) + label, _treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature_name, _treatment, label, _LOGGER) else: _change_number = feature.change_number if feature.killed: @@ -72,25 +74,6 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): }, 'impressions_disabled': feature.impressions_disabled if feature else None } - - def _get_fallback_treatment_and_label(self, feature_name, treatment, label): - if self._fallback_treatments_configuration == None or self._fallback_treatments_configuration.fallback_config == None: - return label, treatment, None - - if self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None: - _LOGGER.debug('Using Fallback Treatment for feature: %s', feature_name) - return self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config - - if self._fallback_treatments_configuration.fallback_config.global_fallback_treatment != None: - _LOGGER.debug('Using Global Fallback Treatment.') - return self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \ - self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \ - self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.config - - return label, treatment, None def _get_treatment(self, feature, bucketing, key, attrs, ctx, label, _treatment): if _treatment == CONTROL: diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 9a6848eb..ab790214 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -9,6 +9,8 @@ from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_treatment import FallbackTreatment from splitio.models.impressions import Impression, Label from splitio.models.events import Event, EventWrapper from splitio.storage import EventStorage, ImpressionStorage, SegmentStorage, SplitStorage, RuleBasedSegmentsStorage @@ -1375,6 +1377,287 @@ def synchronize_config(*_): assert client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': ('on', None)} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=RuntimeError()) + def test_fallback_treatment_eval_exception(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"})))) + + def get_feature_flag_names_by_flag_sets(*_): + return ["some", "some2"] + client._get_feature_flag_names_by_flag_sets = get_feature_flag_names_by_flag_sets + + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key_m", ["some", "some2"]) + assert(treatment == {"some": "on-global", "some2": "on-global"}) + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + assert(self.imps[1].treatment == "on-global") + assert(self.imps[1].label == "fallback - exception") + + assert(client.get_treatment_with_config("key", "some") == ("on-global", '{"prop": "val"}')) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-global", "some2": "on-global"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-global", "some2": "on-global"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key2_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "on-global"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "on-global") + assert(imp.label == "fallback - exception") + assert assert_both == 2 + + assert(client.get_treatment_with_config("key", "some") == ("on-local", None)) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "on-global"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "on-global"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local", {"prop":"val"})})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key3_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "control"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "control") + assert(imp.label == "exception") + assert assert_both == 2 + + assert(client.get_treatment_with_config("key", "some") == ("on-local", '{"prop": "val"}')) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "control"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "control"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "exception") + + try: + factory.destroy() + except: + pass + + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) + def test_fallback_treatment_exception_no_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps == None) + + try: + factory.destroy() + except: + pass + + @mock.patch('splitio.client.client.Client.ready', side_effect=None) + def test_fallback_treatment_not_ready_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client.ready = False + + treatment = client.get_treatment("key", "some") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "not ready") + + try: + factory.destroy() + except: + pass + class ClientAsyncTests(object): # pylint: disable=too-few-public-methods """Split client async test cases.""" @@ -2585,3 +2868,297 @@ async def synchronize_config(*_): assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': ('on', None)} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + @pytest.mark.asyncio + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=RuntimeError()) + async def test_fallback_treatment_eval_exception(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = await InMemoryTelemetryStorageAsync.create() + telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) + telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_evaluation_producer, telemetry_producer.get_telemetry_runtime_producer()) + + factory = SplitFactoryAsync(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock(), + mocker.Mock() + ) + + self.imps = None + async def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + async def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = ClientAsync(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"})))) + + async def get_feature_flag_names_by_flag_sets(*_): + return ["some", "some2"] + client._get_feature_flag_names_by_flag_sets = get_feature_flag_names_by_flag_sets + + async def fetch_many(*_): + return {"some": from_raw(splits_json['splitChange1_1']['ff']['d'][0])} + split_storage.fetch_many = fetch_many + + async def fetch_many_rbs(*_): + return {} + rb_segment_storage.fetch_many = fetch_many_rbs + + treatment = await client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key_m", ["some", "some2"]) + assert(treatment == {"some": "on-global", "some2": "on-global"}) + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + assert(self.imps[1].treatment == "on-global") + assert(self.imps[1].label == "fallback - exception") + + assert(await client.get_treatment_with_config("key", "some") == ("on-global", '{"prop": "val"}')) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-global", "some2": "on-global"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-global", "some2": "on-global"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")})) + treatment = await client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key2_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "on-global"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "on-global") + assert(imp.label == "fallback - exception") + assert assert_both == 2 + + assert(await client.get_treatment_with_config("key", "some") == ("on-local", None)) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "on-global"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "on-global"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local", {"prop":"val"})})) + treatment = await client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key3_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "control"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "control") + assert(imp.label == "exception") + assert assert_both == 2 + + assert(await client.get_treatment_with_config("key", "some") == ("on-local", '{"prop": "val"}')) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "control"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "control"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = await client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "exception") + + try: + await factory.destroy() + except: + pass + + @pytest.mark.asyncio + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) + def test_fallback_treatment_exception_no_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps == None) + + try: + factory.destroy() + except: + pass + + @pytest.mark.asyncio + @mock.patch('splitio.client.client.Client.ready', side_effect=None) + def test_fallback_treatment_not_ready_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client.ready = False + + treatment = client.get_treatment("key", "some") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "not ready") + + try: + factory.destroy() + except: + pass \ No newline at end of file From b7391b43434bfb06511099faae298a50e6c21bc3 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 28 Aug 2025 21:07:00 -0700 Subject: [PATCH 2/4] Update factory and tests --- splitio/client/client.py | 10 +-- splitio/client/factory.py | 35 +++++--- splitio/storage/adapters/redis.py | 4 +- tests/client/test_factory.py | 15 +++- tests/client/test_input_validator.py | 2 + tests/integration/test_client_e2e.py | 94 ++++++++++++++++++-- tests/push/test_manager.py | 10 ++- tests/storage/adapters/test_redis_adapter.py | 6 +- 8 files changed, 140 insertions(+), 36 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 947da98c..ec3c9260 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -63,7 +63,7 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_c self._feature_flag_storage = factory._get_storage('splits') # pylint: disable=protected-access self._segment_storage = factory._get_storage('segments') # pylint: disable=protected-access self._events_storage = factory._get_storage('events') # pylint: disable=protected-access - self._evaluator = Evaluator(self._splitter) + self._evaluator = Evaluator(self._splitter, fallback_treatments_configuration) self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer self._fallback_treatments_configuration = fallback_treatments_configuration @@ -344,7 +344,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1: + if result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) self._record_stats([(impression_decorated, attributes)], start, method) @@ -647,7 +647,7 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) - if i.Impression.label.find(Label.SPLIT_NOT_FOUND) == -1 + if i.Impression.label == None or (i.Impression.label != None and i.Impression.label.find(Label.SPLIT_NOT_FOUND)) == -1 ] self._record_stats(imp_decorated_attrs, start, method) @@ -844,7 +844,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1: + if result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] @@ -1060,7 +1060,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) - if i.Impression.label != Label.SPLIT_NOT_FOUND + if i.Impression.label == None or (i.Impression.label != None and i.Impression.label.find(Label.SPLIT_NOT_FOUND)) == -1 ] await self._record_stats(imp_decorated_attrs, start, method) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index f6070243..57d194ab 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -170,7 +170,8 @@ def __init__( # pylint: disable=too-many-arguments telemetry_producer=None, telemetry_init_producer=None, telemetry_submitter=None, - preforked_initialization=False + preforked_initialization=False, + fallback_treatments_configuration=None ): """ Class constructor. @@ -201,6 +202,7 @@ def __init__( # pylint: disable=too-many-arguments self._ready_time = get_current_epoch_time_ms() _LOGGER.debug("Running in threading mode") self._sdk_internal_ready_flag = sdk_ready_flag + self._fallback_treatments_configuration = fallback_treatments_configuration self._start_status_updater() def _start_status_updater(self): @@ -242,7 +244,7 @@ def client(self): This client is only a set of references to structures hold by the factory. Creating one a fast operation and safe to be used anywhere. """ - return Client(self, self._recorder, self._labels_enabled) + return Client(self, self._recorder, self._labels_enabled, self._fallback_treatments_configuration) def manager(self): """ @@ -338,7 +340,8 @@ def __init__( # pylint: disable=too-many-arguments telemetry_init_producer=None, telemetry_submitter=None, manager_start_task=None, - api_client=None + api_client=None, + fallback_treatments_configuration=None ): """ Class constructor. @@ -372,6 +375,7 @@ def __init__( # pylint: disable=too-many-arguments self._sdk_ready_flag = asyncio.Event() self._ready_task = asyncio.get_running_loop().create_task(self._update_status_when_ready_async()) self._api_client = api_client + self._fallback_treatments_configuration = fallback_treatments_configuration async def _update_status_when_ready_async(self): """Wait until the sdk is ready and update the status for async mode.""" @@ -460,7 +464,7 @@ def client(self): This client is only a set of references to structures hold by the factory. Creating one a fast operation and safe to be used anywhere. """ - return ClientAsync(self, self._recorder, self._labels_enabled) + return ClientAsync(self, self._recorder, self._labels_enabled, self._fallback_treatments_configuration) def _wrap_impression_listener(listener, metadata): """ @@ -623,7 +627,8 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl synchronizer._split_synchronizers._segment_sync.shutdown() return SplitFactory(api_key, storages, cfg['labelsEnabled'], - recorder, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization) + recorder, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() @@ -631,7 +636,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, sdk_ready_flag, telemetry_producer, telemetry_init_producer, - telemetry_submitter) + telemetry_submitter, fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-localsa auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None, @@ -750,7 +755,7 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= recorder, manager, telemetry_producer, telemetry_init_producer, telemetry_submitter, manager_start_task=manager_start_task, - api_client=http_client) + api_client=http_client, fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) def _build_redis_factory(api_key, cfg): """Build and return a split factory with redis-based storage.""" @@ -828,7 +833,8 @@ def _build_redis_factory(api_key, cfg): manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, - telemetry_init_producer=telemetry_init_producer + telemetry_init_producer=telemetry_init_producer, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -910,7 +916,8 @@ async def _build_redis_factory_async(api_key, cfg): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -992,7 +999,8 @@ def _build_pluggable_factory(api_key, cfg): manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, - telemetry_init_producer=telemetry_init_producer + telemetry_init_producer=telemetry_init_producer, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1072,7 +1080,8 @@ async def _build_pluggable_factory_async(api_key, cfg): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1150,6 +1159,7 @@ def _build_localhost_factory(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitter(), + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) async def _build_localhost_factory_async(cfg): @@ -1220,7 +1230,8 @@ async def _build_localhost_factory_async(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitterAsync(), - manager_start_task=manager_start_task + manager_start_task=manager_start_task, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) def get_factory(api_key, **kwargs): diff --git a/splitio/storage/adapters/redis.py b/splitio/storage/adapters/redis.py index 78d88487..4cf87b5e 100644 --- a/splitio/storage/adapters/redis.py +++ b/splitio/storage/adapters/redis.py @@ -715,7 +715,7 @@ def _build_default_client(config): # pylint: disable=too-many-locals unix_socket_path = config.get('redisUnixSocketPath', None) encoding = config.get('redisEncoding', 'utf-8') encoding_errors = config.get('redisEncodingErrors', 'strict') - errors = config.get('redisErrors', None) +# errors = config.get('redisErrors', None) decode_responses = config.get('redisDecodeResponses', True) retry_on_timeout = config.get('redisRetryOnTimeout', False) ssl = config.get('redisSsl', False) @@ -740,7 +740,7 @@ def _build_default_client(config): # pylint: disable=too-many-locals unix_socket_path=unix_socket_path, encoding=encoding, encoding_errors=encoding_errors, - errors=errors, +# errors=errors, Starting from redis 6.0.0 errors argument is removed decode_responses=decode_responses, retry_on_timeout=retry_on_timeout, ssl=ssl, diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index fbe499d6..5f5224e0 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -13,6 +13,8 @@ from splitio.storage import redis, inmemmory, pluggable from splitio.tasks.util import asynctask from splitio.engine.impressions.impressions import Manager as ImpressionsManager +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_treatment import FallbackTreatment from splitio.sync.manager import Manager, ManagerAsync from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitSynchronizers, SplitTasks from splitio.sync.split import SplitSynchronizer, SplitSynchronizerAsync @@ -94,7 +96,7 @@ def test_redis_client_creation(self, mocker): """Test that a client with redis storage is created correctly.""" strict_redis_mock = mocker.Mock() mocker.patch('splitio.storage.adapters.redis.StrictRedis', new=strict_redis_mock) - + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on"))) config = { 'labelsEnabled': False, 'impressionListener': 123, @@ -119,7 +121,8 @@ def test_redis_client_creation(self, mocker): 'redisSslCertReqs': 'some_cert_req', 'redisSslCaCerts': 'some_ca_cert', 'redisMaxConnections': 999, - 'flagSetsFilter': ['set_1'] + 'flagSetsFilter': ['set_1'], + 'fallbackTreatmentsConfiguration': fallback_treatments_configuration } factory = get_factory('some_api_key', config=config) class TelemetrySubmitterMock(): @@ -133,6 +136,7 @@ def synchronize_config(*_): assert isinstance(factory._get_storage('events'), redis.RedisEventsStorage) assert factory._get_storage('splits').flag_set_filter.flag_sets == set([]) + assert factory._fallback_treatments_configuration == fallback_treatments_configuration adapter = factory._get_storage('splits')._redis assert adapter == factory._get_storage('segments')._redis @@ -153,7 +157,7 @@ def synchronize_config(*_): unix_socket_path='/some_path', encoding='utf-8', encoding_errors='non-strict', - errors=True, +# errors=True, decode_responses=True, retry_on_timeout=True, ssl=True, @@ -705,10 +709,13 @@ class SplitFactoryAsyncTests(object): @pytest.mark.asyncio async def test_flag_sets_counts(self): + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on"))) factory = await get_factory_async("none", config={ 'flagSetsFilter': ['set1', 'set2', 'set3'], - 'streamEnabled': False + 'streamEnabled': False, + 'fallbackTreatmentsConfiguration': fallback_treatments_configuration }) + assert factory._fallback_treatments_configuration == fallback_treatments_configuration assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 0 await factory.destroy() diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 476db45e..3923ffbf 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1,5 +1,6 @@ """Unit tests for the input_validator module.""" import pytest +import logging from splitio.client.factory import SplitFactory, get_factory, SplitFactoryAsync, get_factory_async from splitio.client.client import CONTROL, Client, _LOGGER as _logger, ClientAsync @@ -9,6 +10,7 @@ InMemorySplitStorage, InMemorySplitStorageAsync, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync from splitio.models.splits import Split from splitio.client import input_validator +from splitio.client.manager import SplitManager, SplitManagerAsync from splitio.recorder.recorder import StandardRecorder, StandardRecorderAsync from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageProducerAsync from splitio.engine.impressions.impressions import Manager as ImpressionManager diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index f8625f6a..894bba8d 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -29,6 +29,8 @@ PluggableRuleBasedSegmentsStorage, PluggableRuleBasedSegmentsStorageAsync from splitio.storage.adapters.redis import build, RedisAdapter, RedisAdapterAsync, build_async from splitio.models import splits, segments, rule_based_segments +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_treatment import FallbackTreatment from splitio.engine.impressions.impressions import Manager as ImpressionsManager, ImpressionsMode from splitio.engine.impressions import set_classes, set_classes_async from splitio.engine.impressions.strategies import StrategyDebugMode, StrategyOptimizedMode, StrategyNoneMode @@ -196,6 +198,11 @@ def _get_treatment(factory, skip_rbs=False): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('prereq_feature', 'user1234', 'off_default')) + # test fallback treatment + assert client.get_treatment('user4321', 'fallback_feature') == 'on-local' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatment_with_config(factory): """Test client.get_treatment_with_config().""" try: @@ -229,6 +236,11 @@ def _get_treatment_with_config(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatment_with_config('user4321', 'fallback_feature') == ('on-local', '{"prop": "val"}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments(factory): """Test client.get_treatments().""" try: @@ -267,6 +279,11 @@ def _get_treatments(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatments('user4321', ['fallback_feature']) == {'fallback_feature': 'on-local'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments_with_config(factory): """Test client.get_treatments_with_config().""" try: @@ -306,6 +323,11 @@ def _get_treatments_with_config(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatments_with_config('user4321', ['fallback_feature']) == {'fallback_feature': ('on-local', '{"prop": "val"}')} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments_by_flag_set(factory): """Test client.get_treatments_by_flag_set().""" try: @@ -539,6 +561,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -697,6 +720,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -819,7 +843,11 @@ def setup_method(self): 'sdk_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'events_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'auth_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), - 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug'} + 'config': {'connectTimeout': 10000, + 'streamingEnabled': False, + 'impressionsMode': 'debug', + 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + } } self.factory = get_factory('some_apikey', **kwargs) @@ -989,6 +1017,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -1177,6 +1206,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init class LocalhostIntegrationTests(object): # pylint: disable=too-few-public-methods @@ -1400,6 +1430,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1595,6 +1626,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1789,6 +1821,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1942,6 +1975,7 @@ def test_optimized(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -1964,6 +1998,8 @@ def test_optimized(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' def test_debug(self): split_storage = InMemorySplitStorage() @@ -1997,6 +2033,7 @@ def test_debug(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2019,6 +2056,8 @@ def test_debug(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' def test_none(self): split_storage = InMemorySplitStorage() @@ -2052,6 +2091,7 @@ def test_none(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2076,6 +2116,8 @@ def test_none(self): assert imps_count[1].count == 1 assert imps_count[2].feature == 'SPLIT_3' assert imps_count[2].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' class RedisImpressionsToggleIntegrationTests(object): """Run impression toggle tests for Redis.""" @@ -2113,6 +2155,7 @@ def test_optimized(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2141,6 +2184,8 @@ def test_optimized(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2177,6 +2222,7 @@ def test_debug(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2205,6 +2251,8 @@ def test_debug(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2241,6 +2289,7 @@ def test_none(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2271,6 +2320,8 @@ def test_none(self): assert imps_count[1].count == 1 assert imps_count[2].feature == 'SPLIT_3' assert imps_count[2].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2342,6 +2393,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2513,6 +2565,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2653,7 +2706,11 @@ async def _setup_method(self): 'sdk_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'events_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'auth_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), - 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug'} + 'config': {'connectTimeout': 10000, + 'streamingEnabled': False, + 'impressionsMode': 'debug', + 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + } } self.factory = await get_factory_async('some_apikey', **kwargs) @@ -2861,7 +2918,8 @@ async def _setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3083,7 +3141,8 @@ async def _setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3317,7 +3376,8 @@ async def _setup_method(self): RedisManagerAsync(PluggableSynchronizerAsync()), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3546,7 +3606,8 @@ async def _setup_method(self): RedisManagerAsync(PluggableSynchronizerAsync()), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() @@ -3781,6 +3842,7 @@ async def _setup_method(self): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -4481,6 +4543,11 @@ async def _get_treatment_async(factory, skip_rbs=False): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('regex_test', 'abc4', 'on')) + # test fallback treatment + assert await client.get_treatment('user4321', 'fallback_feature') == 'on-local' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + if skip_rbs: return @@ -4537,6 +4604,11 @@ async def _get_treatment_with_config_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatment_with_config('user4321', 'fallback_feature') == ('on-local', '{"prop": "val"}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_async(factory): """Test client.get_treatments().""" try: @@ -4575,6 +4647,11 @@ async def _get_treatments_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatments('user4321', ['fallback_feature']) == {'fallback_feature': 'on-local'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_with_config_async(factory): """Test client.get_treatments_with_config().""" try: @@ -4614,6 +4691,11 @@ async def _get_treatments_with_config_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatments_with_config('user4321', ['fallback_feature']) == {'fallback_feature': ('on-local', '{"prop": "val"}')} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_by_flag_set_async(factory): """Test client.get_treatments_by_flag_set().""" try: diff --git a/tests/push/test_manager.py b/tests/push/test_manager.py index c85301d8..3525baf3 100644 --- a/tests/push/test_manager.py +++ b/tests/push/test_manager.py @@ -259,7 +259,6 @@ class PushManagerAsyncTests(object): async def test_connection_success(self, mocker): """Test the initial status is ok and reset() works as expected.""" api_mock = mocker.Mock() - async def authenticate(): return Token(True, 'abc', {}, 2000000, 1000000) api_mock.authenticate.side_effect = authenticate @@ -274,8 +273,8 @@ async def coro(): t = 0 try: while t < 3: - yield SSEEvent('1', EventType.MESSAGE, '', '{}') await asyncio.sleep(1) + yield SSEEvent('1', EventType.MESSAGE, '', '{}') t += 1 except Exception: pass @@ -295,7 +294,7 @@ async def stop(): manager._sse_client = sse_mock async def deferred_shutdown(): - await asyncio.sleep(1) + await asyncio.sleep(2) await manager.stop(True) manager.start() @@ -309,7 +308,10 @@ async def deferred_shutdown(): assert self.token.exp == 2000000 assert self.token.iat == 1000000 - await shutdown_task + try: + await shutdown_task + except: + pass assert not manager._running assert(telemetry_storage._streaming_events._streaming_events[0]._type == StreamingEventTypes.TOKEN_REFRESH.value) assert(telemetry_storage._streaming_events._streaming_events[1]._type == StreamingEventTypes.CONNECTION_ESTABLISHED.value) diff --git a/tests/storage/adapters/test_redis_adapter.py b/tests/storage/adapters/test_redis_adapter.py index a6bc72dc..78d28bbc 100644 --- a/tests/storage/adapters/test_redis_adapter.py +++ b/tests/storage/adapters/test_redis_adapter.py @@ -99,7 +99,7 @@ def test_adapter_building(self, mocker): 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', - 'redisErrors': 'abc', +# 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': True, @@ -126,7 +126,7 @@ def test_adapter_building(self, mocker): unix_socket_path='/tmp/socket', encoding='utf-8', encoding_errors='strict', - errors='abc', +# errors='abc', decode_responses=True, retry_on_timeout=True, ssl=True, @@ -151,7 +151,7 @@ def test_adapter_building(self, mocker): 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', - 'redisErrors': 'abc', +# 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': False, From 0cfb0ef7a7ac691d83d6ce127bb48ac5333d5222 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 2 Sep 2025 08:32:40 -0700 Subject: [PATCH 3/4] updated regex --- splitio/client/input_validator.py | 2 +- tests/client/test_input_validator.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 2367816d..d732ba21 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -16,7 +16,7 @@ EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$' MAX_PROPERTIES_LENGTH_BYTES = 32768 _FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$' -_FALLBACK_TREATMENT_REGEX = '^[a-zA-Z][a-zA-Z0-9-_;]+$' +_FALLBACK_TREATMENT_REGEX = '^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$' _FALLBACK_TREATMENT_SIZE = 100 def _check_not_null(value, name, operation): diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 3923ffbf..144f2160 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1645,19 +1645,19 @@ def test_fallback_treatments(self, mocker): _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("on/c")) assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("9on")) assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("on$as")) assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] assert input_validator.validate_fallback_treatment(FallbackTreatment("on_c")) From 44110b031e1623d6c45f79789d5280da6a3ded50 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 2 Sep 2025 14:40:24 -0700 Subject: [PATCH 4/4] polishing --- splitio/client/client.py | 7 ++-- splitio/client/config.py | 52 ++++++++++++---------------- splitio/client/factory.py | 18 +++++----- splitio/client/input_validator.py | 9 +++++ splitio/client/util.py | 20 +++++------ splitio/models/fallback_config.py | 27 +-------------- tests/client/test_client.py | 50 +++++++++++++------------- tests/client/test_config.py | 42 +++++++++++----------- tests/client/test_factory.py | 14 ++++---- tests/client/test_input_validator.py | 6 ---- tests/engine/test_evaluator.py | 12 +++---- tests/integration/test_client_e2e.py | 46 ++++++++++++------------ tests/models/test_fallback.py | 6 ++-- 13 files changed, 143 insertions(+), 166 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index ec3c9260..9a33e67c 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -219,6 +219,9 @@ def _get_fallback_eval_results(self, eval_result, feature): feature, result["treatment"], result["impression"]["label"], _LOGGER) return result + def _check_impression_label(self, result): + return result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1) + class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" @@ -344,7 +347,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1): + if self._check_impression_label(result): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) self._record_stats([(impression_decorated, attributes)], start, method) @@ -844,7 +847,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1): + if self._check_impression_label(result): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] diff --git a/splitio/client/config.py b/splitio/client/config.py index 0d77678e..316ac96f 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -5,7 +5,7 @@ from splitio.engine.impressions import ImpressionsMode from splitio.client.input_validator import validate_flag_sets, validate_fallback_treatment, validate_regex_name -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 @@ -71,7 +71,7 @@ class AuthenticateScheme(Enum): 'httpAuthenticateScheme': AuthenticateScheme.NONE, 'kerberosPrincipalUser': None, 'kerberosPrincipalPassword': None, - 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(None) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None) } def _parse_operation_mode(sdk_key, config): @@ -175,32 +175,26 @@ def sanitize(sdk_key, config): return processed def _sanitize_fallback_config(config, processed): - if config.get('fallbackTreatmentsConfiguration') is not None: - if not isinstance(config['fallbackTreatmentsConfiguration'], FallbackTreatmentsConfiguration): - _LOGGER.warning('Config: fallbackTreatmentsConfiguration parameter should be of `FallbackTreatmentsConfiguration` class.') - processed['fallbackTreatmentsConfiguration'] = FallbackTreatmentsConfiguration(None) + if config.get('fallbackTreatments') is not None: + if not isinstance(config['fallbackTreatments'], FallbackTreatmentsConfiguration): + _LOGGER.warning('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.') + processed['fallbackTreatments'] = None return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config != None: - if not isinstance(config['fallbackTreatmentsConfiguration'].fallback_config, FallbackConfig): - _LOGGER.warning('Config: fallback_config parameter should be of `FallbackConfig` class.') - processed['fallbackTreatmentsConfiguration'].fallback_config = FallbackConfig(None, None) - return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment): - _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') - processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment = None - return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment is not None: - sanitized_flag_fallback_treatments = {} - for feature_name in config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment.keys(): - if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment[feature_name]): - _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) - continue - - sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment[feature_name] - - processed['fallbackTreatmentsConfiguration'].fallback_config = FallbackConfig(config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment, sanitized_flag_fallback_treatments) - + + sanitized_global_fallback_treatment = config['fallbackTreatments'].global_fallback_treatment + if config['fallbackTreatments'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatments'].global_fallback_treatment): + _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') + sanitized_global_fallback_treatment = None + + sanitized_flag_fallback_treatments = {} + if config['fallbackTreatments'].by_flag_fallback_treatment is not None: + for feature_name in config['fallbackTreatments'].by_flag_fallback_treatment.keys(): + if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]): + _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) + continue + + sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatments'].by_flag_fallback_treatment[feature_name] + + processed['fallbackTreatments'] = FallbackTreatmentsConfiguration(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments) + return processed \ No newline at end of file diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 57d194ab..e06d6cf9 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -628,7 +628,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) + fallback_treatments_configuration=cfg['fallbackTreatments']) initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() @@ -636,7 +636,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, sdk_ready_flag, telemetry_producer, telemetry_init_producer, - telemetry_submitter, fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) + telemetry_submitter, fallback_treatments_configuration=cfg['fallbackTreatments']) async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-localsa auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None, @@ -755,7 +755,7 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= recorder, manager, telemetry_producer, telemetry_init_producer, telemetry_submitter, manager_start_task=manager_start_task, - api_client=http_client, fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) + api_client=http_client, fallback_treatments_configuration=cfg['fallbackTreatments']) def _build_redis_factory(api_key, cfg): """Build and return a split factory with redis-based storage.""" @@ -834,7 +834,7 @@ def _build_redis_factory(api_key, cfg): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -917,7 +917,7 @@ async def _build_redis_factory_async(api_key, cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1000,7 +1000,7 @@ def _build_pluggable_factory(api_key, cfg): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1081,7 +1081,7 @@ async def _build_pluggable_factory_async(api_key, cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1159,7 +1159,7 @@ def _build_localhost_factory(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitter(), - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) async def _build_localhost_factory_async(cfg): @@ -1231,7 +1231,7 @@ async def _build_localhost_factory_async(cfg): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitterAsync(), manager_start_task=manager_start_task, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) def get_factory(api_key, **kwargs): diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index d732ba21..aaaf8026 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -9,6 +9,7 @@ from splitio.client import client from splitio.client.util import get_fallback_treatment_and_label from splitio.engine.evaluator import CONTROL +from splitio.models.fallback_treatment import FallbackTreatment _LOGGER = logging.getLogger(__name__) @@ -722,6 +723,14 @@ def validate_flag_sets(flag_sets, method_name): return list(sanitized_flag_sets) def validate_fallback_treatment(fallback_treatment): + if not isinstance(fallback_treatment, FallbackTreatment): + _LOGGER.warning("Config: Fallback treatment instance should be FallbackTreatment, input is discarded") + return False + + if not isinstance(fallback_treatment.treatment, str): + _LOGGER.warning("Config: Fallback treatment value should be str type, input is discarded") + return False + if not validate_regex_name(fallback_treatment.treatment): _LOGGER.warning("Config: Fallback treatment should match regex %s", _FALLBACK_TREATMENT_REGEX) return False diff --git a/splitio/client/util.py b/splitio/client/util.py index 6541b9df..1f01de3f 100644 --- a/splitio/client/util.py +++ b/splitio/client/util.py @@ -53,20 +53,20 @@ def get_metadata(config): return SdkMetadata(version, hostname, ip_address) def get_fallback_treatment_and_label(fallback_treatments_configuration, feature_name, treatment, label, _logger): - if fallback_treatments_configuration == None or fallback_treatments_configuration.fallback_config == None: + if fallback_treatments_configuration == None: return label, treatment, None - if fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \ - fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None: + if fallback_treatments_configuration.by_flag_fallback_treatment != None and \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name) != None: _logger.debug('Using Fallback Treatment for feature: %s', feature_name) - return fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ - fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \ - fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config + return fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).treatment, \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).config - if fallback_treatments_configuration.fallback_config.global_fallback_treatment != None: + if fallback_treatments_configuration.global_fallback_treatment != None: _logger.debug('Using Global Fallback Treatment.') - return fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \ - fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \ - fallback_treatments_configuration.fallback_config.global_fallback_treatment.config + return fallback_treatments_configuration.global_fallback_treatment.label_prefix + label, \ + fallback_treatments_configuration.global_fallback_treatment.treatment, \ + fallback_treatments_configuration.global_fallback_treatment.config return label, treatment, None diff --git a/splitio/models/fallback_config.py b/splitio/models/fallback_config.py index 6e84d62f..14b00dda 100644 --- a/splitio/models/fallback_config.py +++ b/splitio/models/fallback_config.py @@ -1,32 +1,7 @@ """Segment module.""" class FallbackTreatmentsConfiguration(object): - """FallbackConfiguration object class.""" - - def __init__(self, fallback_config): - """ - Class constructor. - - :param fallback_config: fallback config object. - :type fallback_config: FallbackConfig - - :param by_flag_fallback_treatment: Dict of flags and their fallback treatment - :type by_flag_fallback_treatment: {str: FallbackTreatment} - """ - self._fallback_config = fallback_config - - @property - def fallback_config(self): - """Return fallback config.""" - return self._fallback_config - - @fallback_config.setter - def fallback_config(self, new_value): - """Set fallback config.""" - self._fallback_config = new_value - -class FallbackConfig(object): - """FallbackConfig object class.""" + """FallbackTreatmentsConfiguration object class.""" def __init__(self, global_fallback_treatment=None, by_flag_fallback_treatment=None): """ diff --git a/tests/client/test_client.py b/tests/client/test_client.py index ab790214..75f46464 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -9,7 +9,7 @@ from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.models.fallback_treatment import FallbackTreatment from splitio.models.impressions import Impression, Label from splitio.models.events import Event, EventWrapper @@ -1419,7 +1419,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"})))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}))) def get_feature_flag_names_by_flag_sets(*_): return ["some", "some2"] @@ -1446,7 +1446,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -1475,7 +1475,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local", {"prop":"val"})})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", {"prop":"val"})}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -1504,7 +1504,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -1557,25 +1557,25 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) treatment = client.get_treatment("key", "some") assert(treatment == "on-global") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps == None) @@ -1625,7 +1625,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) client.ready = False treatment = client.get_treatment("key", "some") @@ -1633,21 +1633,21 @@ def synchronize_config(*_): assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -2914,7 +2914,7 @@ class TelemetrySubmitterMock(): async def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = ClientAsync(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"})))) + client = ClientAsync(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}))) async def get_feature_flag_names_by_flag_sets(*_): return ["some", "some2"] @@ -2949,7 +2949,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")}) treatment = await client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -2978,7 +2978,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local", {"prop":"val"})})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", {"prop":"val"})}) treatment = await client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -3007,7 +3007,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = await client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -3061,25 +3061,25 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) treatment = client.get_treatment("key", "some") assert(treatment == "on-global") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps == None) @@ -3130,7 +3130,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) client.ready = False treatment = client.get_treatment("key", "some") @@ -3138,21 +3138,21 @@ def synchronize_config(*_): assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") diff --git a/tests/client/test_config.py b/tests/client/test_config.py index cbe8ffcd..0017938c 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -4,7 +4,7 @@ from splitio.client import config from splitio.engine.impressions.impressions import ImpressionsMode from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration class ConfigSanitizationTests(object): """Inmemory storage-based integration tests.""" @@ -92,32 +92,34 @@ def test_sanitize(self, mocker): assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': 'NONE'}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config == None - assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatmentsConfiguration parameter should be of `FallbackTreatmentsConfiguration` class.") + processed = config.sanitize('some', {'fallbackTreatments': 'NONE'}) + assert processed['fallbackTreatments'] == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.") _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(123)}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment == None - assert _logger.warning.mock_calls[1] == mocker.call("Config: fallback_config parameter should be of `FallbackConfig` class.") + processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(123)}) + assert processed['fallbackTreatments'].global_fallback_treatment == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("123")))}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment == None + processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(FallbackTreatment(123))}) + assert processed['fallbackTreatments'].global_fallback_treatment == None assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") - fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'))) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment + fb = FallbackTreatmentsConfiguration(FallbackTreatment('on')) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + assert processed['fallbackTreatments'].global_fallback_treatment.label_prefix == "fallback - " - fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'), {"flag": FallbackTreatment("off")})) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment - assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment["flag"] == fb.fallback_config.by_flag_fallback_treatment["flag"] + fb = FallbackTreatmentsConfiguration(FallbackTreatment('on'), {"flag": FallbackTreatment("off")}) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"] == fb.by_flag_fallback_treatment["flag"] + assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"].label_prefix == "fallback - " _logger.reset_mock() - fb = FallbackTreatmentsConfiguration(FallbackConfig(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")})) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert len(processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment) == 1 - assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment.get("flag2") == fb.fallback_config.by_flag_fallback_treatment["flag2"] + fb = FallbackTreatmentsConfiguration(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")}) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert len(processed['fallbackTreatments'].by_flag_fallback_treatment) == 1 + assert processed['fallbackTreatments'].by_flag_fallback_treatment.get("flag2") == fb.by_flag_fallback_treatment["flag2"] assert _logger.warning.mock_calls[1] == mocker.call('Config: fallback treatment parameter for feature flag %s is discarded.', 'flag#%') \ No newline at end of file diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 5f5224e0..86e13088 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -13,7 +13,7 @@ from splitio.storage import redis, inmemmory, pluggable from splitio.tasks.util import asynctask from splitio.engine.impressions.impressions import Manager as ImpressionsManager -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.models.fallback_treatment import FallbackTreatment from splitio.sync.manager import Manager, ManagerAsync from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitSynchronizers, SplitTasks @@ -96,7 +96,7 @@ def test_redis_client_creation(self, mocker): """Test that a client with redis storage is created correctly.""" strict_redis_mock = mocker.Mock() mocker.patch('splitio.storage.adapters.redis.StrictRedis', new=strict_redis_mock) - fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on"))) + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on")) config = { 'labelsEnabled': False, 'impressionListener': 123, @@ -122,7 +122,7 @@ def test_redis_client_creation(self, mocker): 'redisSslCaCerts': 'some_ca_cert', 'redisMaxConnections': 999, 'flagSetsFilter': ['set_1'], - 'fallbackTreatmentsConfiguration': fallback_treatments_configuration + 'fallbackTreatments': fallback_treatments_configuration } factory = get_factory('some_api_key', config=config) class TelemetrySubmitterMock(): @@ -136,7 +136,7 @@ def synchronize_config(*_): assert isinstance(factory._get_storage('events'), redis.RedisEventsStorage) assert factory._get_storage('splits').flag_set_filter.flag_sets == set([]) - assert factory._fallback_treatments_configuration == fallback_treatments_configuration + assert factory._fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment adapter = factory._get_storage('splits')._redis assert adapter == factory._get_storage('segments')._redis @@ -709,13 +709,13 @@ class SplitFactoryAsyncTests(object): @pytest.mark.asyncio async def test_flag_sets_counts(self): - fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on"))) + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on")) factory = await get_factory_async("none", config={ 'flagSetsFilter': ['set1', 'set2', 'set3'], 'streamEnabled': False, - 'fallbackTreatmentsConfiguration': fallback_treatments_configuration + 'fallbackTreatments': fallback_treatments_configuration }) - assert factory._fallback_treatments_configuration == fallback_treatments_configuration + assert factory._fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 0 await factory.destroy() diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 144f2160..85afb248 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1648,12 +1648,6 @@ def test_fallback_treatments(self, mocker): mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] - _logger.reset_mock() - assert not input_validator.validate_fallback_treatment(FallbackTreatment("9on")) - assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") - ] - _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("on$as")) assert _logger.warning.mock_calls == [ diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index e95a8710..ba51f901 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -12,7 +12,7 @@ from splitio.models.grammar import condition from splitio.models import rule_based_segments from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.engine import evaluator, splitters from splitio.engine.evaluator import EvaluationContext from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, InMemoryRuleBasedSegmentStorage, \ @@ -384,7 +384,7 @@ def test_evaluate_treatment_with_fallback(self, mocker): # should use global fallback logger_mock.reset_mock() - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"})))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}))) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-global' assert result['configurations'] == '{"prop": "val"}' @@ -394,7 +394,7 @@ def test_evaluate_treatment_with_fallback(self, mocker): # should use by flag fallback logger_mock.reset_mock() - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-some2' assert result['configurations'] == '{"prop2": "val2"}' @@ -402,21 +402,21 @@ def test_evaluate_treatment_with_fallback(self, mocker): assert logger_mock.debug.mock_calls[0] == mocker.call("Using Fallback Treatment for feature: %s", "some2") # should not use any fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) assert result['treatment'] == 'control' assert result['configurations'] == None assert result['impression']['label'] == Label.SPLIT_NOT_FOUND # should use by flag fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-some2' assert result['configurations'] == '{"prop2": "val2"}' assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND # should global flag fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) assert result['treatment'] == 'off-global' assert result['configurations'] == '{"prop": "val"}' diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 894bba8d..257d9099 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -29,7 +29,7 @@ PluggableRuleBasedSegmentsStorage, PluggableRuleBasedSegmentsStorageAsync from splitio.storage.adapters.redis import build, RedisAdapter, RedisAdapterAsync, build_async from splitio.models import splits, segments, rule_based_segments -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.models.fallback_treatment import FallbackTreatment from splitio.engine.impressions.impressions import Manager as ImpressionsManager, ImpressionsMode from splitio.engine.impressions import set_classes, set_classes_async @@ -561,7 +561,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -720,7 +720,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -846,7 +846,7 @@ def setup_method(self): 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug', - 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) } } @@ -1017,7 +1017,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -1206,7 +1206,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init class LocalhostIntegrationTests(object): # pylint: disable=too-few-public-methods @@ -1430,7 +1430,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1626,7 +1626,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1821,7 +1821,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1975,7 +1975,7 @@ def test_optimized(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2033,7 +2033,7 @@ def test_debug(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2091,7 +2091,7 @@ def test_none(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2155,7 +2155,7 @@ def test_optimized(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2222,7 +2222,7 @@ def test_debug(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2289,7 +2289,7 @@ def test_none(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2393,7 +2393,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2565,7 +2565,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2709,7 +2709,7 @@ async def _setup_method(self): 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug', - 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) } } @@ -2919,7 +2919,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3142,7 +3142,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3377,7 +3377,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3607,7 +3607,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() @@ -3842,7 +3842,7 @@ async def _setup_method(self): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage diff --git a/tests/models/test_fallback.py b/tests/models/test_fallback.py index b326fd6f..a3111277 100644 --- a/tests/models/test_fallback.py +++ b/tests/models/test_fallback.py @@ -1,5 +1,5 @@ from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig +from splitio.models.fallback_config import FallbackTreatmentsConfiguration class FallbackTreatmentModelTests(object): """Fallback treatment model tests.""" @@ -13,13 +13,13 @@ def test_working(self): assert fallback_treatment.config == None assert fallback_treatment.treatment == 'off' -class FallbackConfigModelTests(object): +class FallbackTreatmentsConfigModelTests(object): """Fallback treatment model tests.""" def test_working(self): global_fb = FallbackTreatment("on") flag_fb = FallbackTreatment("off") - fallback_config = FallbackConfig(global_fb, {"flag1": flag_fb}) + fallback_config = FallbackTreatmentsConfiguration(global_fb, {"flag1": flag_fb}) assert fallback_config.global_fallback_treatment == global_fb assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb}