diff --git a/CHANGES.txt b/CHANGES.txt index 6ea03dfc..eee840fd 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,13 @@ +9.6.0 (Nov 3, 2023) +- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. + - get_treatments_by_flag_set and get_treatments_by_flag_sets + - get_treatments_with_config_by_flag_set and get_treatments_with_config_by_flag_sets +- Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. + - Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init. +- Updated the following SDK manager methods to expose flag sets on flag views. +- Removed raising an exception when Telemetry post config data fails, SDK will only log the error. + 9.5.1 (Sep 5, 2023) - Exclude tests from when building the package - Fixed exception when fetching telemetry stats if no SSE Feature flags update events are stored diff --git a/splitio/api/commons.py b/splitio/api/commons.py index 92004cb8..0766ae49 100644 --- a/splitio/api/commons.py +++ b/splitio/api/commons.py @@ -57,7 +57,7 @@ def record_telemetry(status_code, elapsed, metric_name, telemetry_runtime_produc class FetchOptions(object): """Fetch Options object.""" - def __init__(self, cache_control_headers=False, change_number=None): + def __init__(self, cache_control_headers=False, change_number=None, sets=None): """ Class constructor. @@ -66,9 +66,13 @@ def __init__(self, cache_control_headers=False, change_number=None): :param change_number: ChangeNumber to use for bypassing CDN in request. :type change_number: int + + :param sets: list of flag sets + :type sets: list """ self._cache_control_headers = cache_control_headers self._change_number = change_number + self._sets = sets @property def cache_control_headers(self): @@ -80,12 +84,19 @@ def change_number(self): """Return change number.""" return self._change_number + @property + def sets(self): + """Return sets.""" + return self._sets + def __eq__(self, other): """Match between other options.""" if self._cache_control_headers != other._cache_control_headers: return False if self._change_number != other._change_number: return False + if self._sets != other._sets: + return False return True @@ -113,4 +124,6 @@ def build_fetch(change_number, fetch_options, metadata): extra_headers[_CACHE_CONTROL] = _CACHE_CONTROL_NO_CACHE if fetch_options.change_number is not None: query['till'] = fetch_options.change_number + if fetch_options.sets is not None: + query['sets'] = fetch_options.sets return query, extra_headers \ No newline at end of file diff --git a/splitio/api/splits.py b/splitio/api/splits.py index b584111b..78e15ef2 100644 --- a/splitio/api/splits.py +++ b/splitio/api/splits.py @@ -59,6 +59,8 @@ def fetch_splits(self, change_number, fetch_options): if 200 <= response.status_code < 300: return json.loads(response.body) else: + if response.status_code == 414: + _LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.') raise APIException(response.body, response.status_code) except HttpClientException as exc: _LOGGER.error('Error fetching feature flags because an exception was raised by the HTTPClient') diff --git a/splitio/api/telemetry.py b/splitio/api/telemetry.py index 4c182a4e..722bb75d 100644 --- a/splitio/api/telemetry.py +++ b/splitio/api/telemetry.py @@ -76,7 +76,6 @@ def record_init(self, configs): 'Error posting init config because an exception was raised by the HTTPClient' ) _LOGGER.debug('Error: ', exc_info=True) - raise APIException('Init config data not flushed properly.') from exc def record_stats(self, stats): """ diff --git a/splitio/client/client.py b/splitio/client/client.py index 91e88447..35030595 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -6,7 +6,7 @@ from splitio.models.impressions import Impression, Label 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 import input_validator, config from splitio.util.time import get_current_epoch_time_ms, utctime_ms _LOGGER = logging.getLogger(__name__) @@ -59,8 +59,9 @@ def destroyed(self): """Return whether the factory holding this client has been destroyed.""" return self._factory.destroyed - def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=None): + def _evaluate_if_ready(self, matching_key, bucketing_key, feature, method, attributes=None): if not self.ready: + _LOGGER.warning("%s: The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", method, feature) self._telemetry_init_producer.record_not_ready_usage() return { 'treatment': CONTROL, @@ -102,7 +103,7 @@ def _make_evaluation(self, key, feature_flag, attributes, method_name, metric_na or not input_validator.validate_attributes(attributes, method_name): return CONTROL, None - result = self._evaluate_if_ready(matching_key, bucketing_key, feature_flag, attributes) + result = self._evaluate_if_ready(matching_key, bucketing_key, feature_flag, method_name, attributes) impression = self._build_impression( matching_key, @@ -167,7 +168,7 @@ def _make_evaluations(self, key, feature_flags, attributes, method_name, metric_ try: evaluations = self._evaluate_features_if_ready(matching_key, bucketing_key, - list(feature_flags), attributes) + list(feature_flags), method_name, attributes) for feature_flag in feature_flags: try: @@ -212,8 +213,9 @@ def _make_evaluations(self, key, feature_flags, attributes, method_name, metric_ _LOGGER.debug('Error: ', exc_info=True) return input_validator.generate_control_treatments(list(feature_flags), method_name) - def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, attributes=None): + def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, method, attributes=None): if not self.ready: + _LOGGER.warning("%s: The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", method, ', '.join([feature for feature in feature_flags])) self._telemetry_init_producer.record_not_ready_usage() return { feature_flag: { @@ -309,6 +311,132 @@ def get_treatments(self, key, feature_flags, attributes=None): MethodExceptionsAndLatencies.TREATMENTS) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} + def get_treatments_by_flag_set(self, key, flag_set, attributes=None): + """ + Get treatments for feature flags that contain given flag set. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_set: flag set + :type flag_sets: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: Dictionary with the result of all the feature flags provided + :rtype: dict + """ + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes) + + def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): + """ + Get treatments for feature flags that contain given flag sets. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_sets: list of flag sets + :type flag_sets: list + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: Dictionary with the result of all the feature flags provided + :rtype: dict + """ + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes) + + def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None): + """ + Get treatments for feature flags that contain given flag set. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_set: flag set + :type flag_sets: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: Dictionary with the result of all the feature flags provided + :rtype: dict + """ + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes) + + def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None): + """ + Get treatments for feature flags that contain given flag set. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_set: flag set + :type flag_sets: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: Dictionary with the result of all the feature flags provided + :rtype: dict + """ + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes) + + def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): + """ + Get treatments for feature flags that contain given flag sets. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_sets: list of flag sets + :type flag_sets: list + :param method: Treatment by flag set method flavor + :type method: splitio.models.telemetry.MethodExceptionsAndLatencies + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: Dictionary with the result of all the feature flags provided + :rtype: dict + """ + feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets, method.value) + if feature_flags_names == []: + _LOGGER.warning("%s: No valid Flag set or no feature flags found for evaluating treatments" % (method.value)) + return {} + + if 'config' in method.value: + return self._make_evaluations(key, feature_flags_names, attributes, method.value, + method) + + with_config = self._make_evaluations(key, feature_flags_names, attributes, method.value, + method) + return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} + + + def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): + """ + Sanitize given flag sets and return list of feature flag names associated with them + + :param flag_sets: list of flag sets + :type flag_sets: list + + :return: list of feature flag names + :rtype: list + """ + sanitized_flag_sets = input_validator.validate_flag_sets(flag_sets, method_name) + feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets) + if feature_flags_by_set is None: + _LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_sets)) + return [] + return feature_flags_by_set + def _build_impression( # pylint: disable=too-many-arguments self, matching_key, diff --git a/splitio/client/config.py b/splitio/client/config.py index 4531e40a..92388edf 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -3,12 +3,12 @@ import logging from splitio.engine.impressions import ImpressionsMode +from splitio.client.input_validator import validate_flag_sets _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 - DEFAULT_CONFIG = { 'operationMode': 'standalone', 'connectionTimeout': 1500, @@ -58,10 +58,10 @@ 'dataSampling': DEFAULT_DATA_SAMPLING, 'storageWrapper': None, 'storagePrefix': None, - 'storageType': None + 'storageType': None, + 'flagSetsFilter': None } - def _parse_operation_mode(sdk_key, config): """ Process incoming config to determine operation mode and storage type @@ -118,7 +118,6 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None): return mode, refresh_rate - def sanitize(sdk_key, config): """ Look for inconsistencies or ill-formed configs and tune it accordingly. @@ -143,4 +142,10 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 + if config['operationMode'] == 'consumer' and config.get('flagSetsFilter') is not None: + processed['flagSetsFilter'] = None + _LOGGER.warning('config: FlagSets filter is not applicable for Consumer modes where the SDK does keep rollout data in sync. FlagSet filter was discarded.') + else: + processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None + return processed diff --git a/splitio/client/factory.py b/splitio/client/factory.py index fede6ad0..67c57e68 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -312,7 +312,8 @@ def _wrap_impression_listener(listener, metadata): def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-locals - auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None): + auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None, + total_flag_sets=0, invalid_flag_sets=0): """Build and return a split factory tailored to the supplied config.""" if not input_validator.validate_factory_instantiation(api_key): return None @@ -350,7 +351,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl } storages = { - 'splits': InMemorySplitStorage(), + 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(), 'impressions': InMemoryImpressionStorage(cfg['impressionsQueueSize'], telemetry_runtime_producer), 'events': InMemoryEventStorage(cfg['eventsQueueSize'], telemetry_runtime_producer), @@ -416,7 +417,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl telemetry_evaluation_producer ) - telemetry_init_producer.record_config(cfg, extra_cfg) + telemetry_init_producer.record_config(cfg, extra_cfg, total_flag_sets, invalid_flag_sets) if preforked_initialization: synchronizer.sync_all(max_retry_attempts=_MAX_RETRY_SYNC_ALL) @@ -440,7 +441,7 @@ def _build_redis_factory(api_key, cfg): cache_enabled = cfg.get('redisLocalCacheEnabled', False) cache_ttl = cfg.get('redisLocalCacheTTL', 5) storages = { - 'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl), + 'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': RedisSegmentStorage(redis_adapter), 'impressions': RedisImpressionsStorage(redis_adapter, sdk_metadata), 'events': RedisEventsStorage(redis_adapter, sdk_metadata), @@ -495,7 +496,7 @@ def _build_redis_factory(api_key, cfg): initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() - telemetry_init_producer.record_config(cfg, {}) + telemetry_init_producer.record_config(cfg, {}, 0, 0) split_factory = SplitFactory( api_key, @@ -523,7 +524,7 @@ def _build_pluggable_factory(api_key, cfg): pluggable_adapter = cfg.get('storageWrapper') storage_prefix = cfg.get('storagePrefix') storages = { - 'splits': PluggableSplitStorage(pluggable_adapter, storage_prefix), + 'splits': PluggableSplitStorage(pluggable_adapter, storage_prefix, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': PluggableSegmentStorage(pluggable_adapter, storage_prefix), 'impressions': PluggableImpressionsStorage(pluggable_adapter, sdk_metadata, storage_prefix), 'events': PluggableEventsStorage(pluggable_adapter, sdk_metadata, storage_prefix), @@ -573,7 +574,7 @@ def _build_pluggable_factory(api_key, cfg): initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() - telemetry_init_producer.record_config(cfg, {}) + telemetry_init_producer.record_config(cfg, {}, 0, 0) split_factory = SplitFactory( api_key, @@ -600,7 +601,7 @@ def _build_localhost_factory(cfg): telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() storages = { - 'splits': InMemorySplitStorage(), + 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(), # not used, just to avoid possible future errors. 'impressions': LocalhostImpressionsStorage(), 'events': LocalhostEventsStorage(), @@ -684,7 +685,14 @@ def get_factory(api_key, **kwargs): _INSTANTIATED_FACTORIES.update([api_key]) _INSTANTIATED_FACTORIES_LOCK.release() - config = sanitize_config(api_key, kwargs.get('config', {})) + config_raw = kwargs.get('config', {}) + total_flag_sets = 0 + invalid_flag_sets = 0 + if config_raw.get('flagSetsFilter') is not None and isinstance(config_raw.get('flagSetsFilter'), list): + total_flag_sets = len(config_raw.get('flagSetsFilter')) + invalid_flag_sets = total_flag_sets - len(input_validator.validate_flag_sets(config_raw.get('flagSetsFilter'), 'Telemetry Init')) + + config = sanitize_config(api_key, config_raw) if config['operationMode'] == 'localhost': split_factory = _build_localhost_factory(config) @@ -700,7 +708,9 @@ def get_factory(api_key, **kwargs): kwargs.get('events_api_base_url'), kwargs.get('auth_api_base_url'), kwargs.get('streaming_api_base_url'), - kwargs.get('telemetry_api_base_url')) + kwargs.get('telemetry_api_base_url'), + total_flag_sets, + invalid_flag_sets) return split_factory @@ -712,4 +722,4 @@ def _get_active_and_redundant_count(): redundant_factory_count += _INSTANTIATED_FACTORIES[item] - 1 active_factory_count += _INSTANTIATED_FACTORIES[item] _INSTANTIATED_FACTORIES_LOCK.release() - return redundant_factory_count, active_factory_count \ No newline at end of file + return redundant_factory_count, active_factory_count diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index a15caf91..fa6a0dbc 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -15,6 +15,7 @@ MAX_LENGTH = 250 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}$' def _check_not_null(value, name, operation): @@ -79,7 +80,8 @@ def _check_string_not_empty(value, name, operation): return True -def _check_string_matches(value, operation, pattern): + +def _check_string_matches(value, operation, pattern, name, length): """ Check if value is adhere to a regular expression passed. @@ -92,14 +94,14 @@ def _check_string_matches(value, operation, pattern): :return: The result of validation :rtype: True|False """ - if not re.match(pattern, value): + if re.search(pattern, value) is None or re.search(pattern, value).group() != value: _LOGGER.error( '%s: you passed %s, event_type must ' + 'adhere to the regular expression %s. ' + - 'This means an event name must be alphanumeric, cannot be more ' + - 'than 80 characters long, and can only include a dash, underscore, ' + + 'This means %s must be alphanumeric, cannot be more ' + + 'than %s characters long, and can only include a dash, underscore, ' + 'period, or colon as separators of alphanumeric characters.', - operation, value, pattern + operation, value, pattern, name, length ) return False return True @@ -165,10 +167,7 @@ def _check_valid_object_key(key, name, operation): :return: The result of validation :rtype: str|None """ - if key is None: - _LOGGER.error( - '%s: you passed a null %s, %s must be a non-empty string.', - operation, name, name) + if not _check_not_null(key, 'key', operation): return None if isinstance(key, str): if not _check_string_not_empty(key, name, operation): @@ -179,7 +178,7 @@ def _check_valid_object_key(key, name, operation): return key_str -def _remove_empty_spaces(value, operation): +def _remove_empty_spaces(value, name, operation): """ Check if an string has whitespaces. @@ -192,10 +191,17 @@ def _remove_empty_spaces(value, operation): """ strip_value = value.strip() if value != strip_value: - _LOGGER.warning("%s: feature flag name '%s' has extra whitespace, trimming.", operation, value) + _LOGGER.warning("%s: %s '%s' has extra whitespace, trimming.", operation, name, value) return strip_value +def _convert_str_to_lower(value, name, operation): + lower_value = value.lower() + if value != lower_value: + _LOGGER.warning("%s: %s '%s' should be all lowercase - converting string to lowercase" % (operation, name, value)) + return lower_value + + def validate_key(key, method_name): """ Validate Key parameter for get_treatment/s. @@ -211,8 +217,7 @@ def validate_key(key, method_name): """ matching_key_result = None bucketing_key_result = None - if key is None: - _LOGGER.error('%s: you passed a null key, key must be a non-empty string.', method_name) + if not _check_not_null(key, 'key', method_name): return None, None if isinstance(key, Key): @@ -255,7 +260,7 @@ def validate_feature_flag_name(feature_flag_name, should_validate_existance, fea ) return None - return _remove_empty_spaces(feature_flag_name, method_name) + return _remove_empty_spaces(feature_flag_name, 'feature flag name', method_name) def validate_track_key(key): @@ -294,10 +299,7 @@ def validate_traffic_type(traffic_type, should_validate_existance, feature_flag_ (not _check_is_string(traffic_type, 'traffic_type', 'track')) or \ (not _check_string_not_empty(traffic_type, 'traffic_type', 'track')): return None - if not traffic_type.islower(): - _LOGGER.warning('track: %s should be all lowercase - converting string to lowercase.', - traffic_type) - traffic_type = traffic_type.lower() + traffic_type = _convert_str_to_lower(traffic_type, 'traffic type', 'track') if should_validate_existance and not feature_flag_storage.is_valid_traffic_type(traffic_type): _LOGGER.warning( @@ -322,7 +324,7 @@ def validate_event_type(event_type): if (not _check_not_null(event_type, 'event_type', 'track')) or \ (not _check_is_string(event_type, 'event_type', 'track')) or \ (not _check_string_not_empty(event_type, 'event_type', 'track')) or \ - (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN)): + (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN, 'an event name', 80)): return None return event_type @@ -390,7 +392,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name _LOGGER.error("%s: feature flag names must be a non-empty array.", method_name) return None, None filtered_feature_flags = set( - _remove_empty_spaces(feature_flag, method_name) for feature_flag in feature_flags + _remove_empty_spaces(feature_flag, 'feature flag name', method_name) for feature_flag in feature_flags if feature_flag is not None and _check_is_string(feature_flag, 'feature flag name', method_name) and _check_string_not_empty(feature_flag, 'feature flag name', method_name) @@ -566,3 +568,33 @@ def validate_pluggable_adapter(config): _LOGGER.error("Pluggable adapter method %s has less than required arguments count: %s : " % (exp_method, len(get_method_args))) return False return True + +def validate_flag_sets(flag_sets, method_name): + """ + Validate flag sets list + + :param flag_set: list of flag sets + :type flag_set: list[str] + + :returns: Sanitized and sorted flag sets + :rtype: list[str] + """ + if not isinstance(flag_sets, list): + _LOGGER.warning("%s: flag sets parameter type should be list object, parameter is discarded" % (method_name)) + return [] + + sanitized_flag_sets = set() + for flag_set in flag_sets: + if not _check_not_null(flag_set, 'flag set', method_name): + continue + if not _check_is_string(flag_set, 'flag set', method_name): + continue + flag_set = _remove_empty_spaces(flag_set, 'flag set', method_name) + flag_set = _convert_str_to_lower(flag_set, 'flag set', method_name) + + if not _check_string_matches(flag_set, method_name, _FLAG_SETS_REGEX, 'a flag set', 50): + continue + + sanitized_flag_sets.add(flag_set) + + return list(sanitized_flag_sets) diff --git a/splitio/engine/telemetry.py b/splitio/engine/telemetry.py index f2ecf6f8..55afa320 100644 --- a/splitio/engine/telemetry.py +++ b/splitio/engine/telemetry.py @@ -36,9 +36,9 @@ def __init__(self, telemetry_storage): """Constructor.""" self._telemetry_storage = telemetry_storage - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets=0, invalid_flag_sets=0): """Record configurations.""" - self._telemetry_storage.record_config(config, extra_config) + self._telemetry_storage.record_config(config, extra_config, total_flag_sets, invalid_flag_sets) current_app, app_worker_id = self._get_app_worker_id() if current_app is not None: self.add_config_tag("initilization:" + current_app) @@ -48,6 +48,14 @@ def record_ready_time(self, ready_time): """Record ready time.""" self._telemetry_storage.record_ready_time(ready_time) + def record_flag_sets(self, flag_sets): + """Record flag sets.""" + self._telemetry_storage.record_flag_sets(flag_sets) + + def record_invalid_flag_sets(self, flag_sets): + """Record invalid flag sets.""" + self._telemetry_storage.record_invalid_flag_sets(flag_sets) + def record_bur_time_out(self): """Record block until ready timeout.""" self._telemetry_storage.record_bur_time_out() @@ -218,17 +226,27 @@ def pop_formatted_stats(self): exceptions = self.pop_exceptions()['methodExceptions'] latencies = self.pop_latencies()['methodLatencies'] return { - 'mE': {'t': exceptions['treatment'], - 'ts': exceptions['treatments'], - 'tc': exceptions['treatment_with_config'], - 'tcs': exceptions['treatments_with_config'], - 'tr': exceptions['track'] + 'mE': { + 't': exceptions['treatment'], + 'ts': exceptions['treatments'], + 'tc': exceptions['treatment_with_config'], + 'tcs': exceptions['treatments_with_config'], + 'tf': exceptions['treatments_by_flag_set'], + 'tfs': exceptions['treatments_by_flag_sets'], + 'tcf': exceptions['treatments_with_config_by_flag_set'], + 'tcfs': exceptions['treatments_with_config_by_flag_sets'], + 'tr': exceptions['track'] }, - 'mL': {'t': latencies['treatment'], - 'ts': latencies['treatments'], - 'tc': latencies['treatment_with_config'], - 'tcs': latencies['treatments_with_config'], - 'tr': latencies['track'] + 'mL': { + 't': latencies['treatment'], + 'ts': latencies['treatments'], + 'tc': latencies['treatment_with_config'], + 'tcs': latencies['treatments_with_config'], + 'tf': latencies['treatments_by_flag_set'], + 'tfs': latencies['treatments_by_flag_sets'], + 'tcf': latencies['treatments_with_config_by_flag_set'], + 'tcfs': latencies['treatments_with_config_by_flag_sets'], + 'tr': latencies['track'] }, } diff --git a/splitio/models/splits.py b/splitio/models/splits.py index 5e0ab394..0a10dd87 100644 --- a/splitio/models/splits.py +++ b/splitio/models/splits.py @@ -7,7 +7,7 @@ SplitView = namedtuple( 'SplitView', - ['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs'] + ['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'default_treatment', 'sets'] ) @@ -41,7 +41,8 @@ def __init__( # pylint: disable=too-many-arguments algo=None, traffic_allocation=None, traffic_allocation_seed=None, - configurations=None + configurations=None, + sets=None ): """ Class constructor. @@ -62,6 +63,8 @@ def __init__( # pylint: disable=too-many-arguments :type traffic_allocation: int :pram traffic_allocation_seed: Seed used to hash traffic allocation. :type traffic_allocation_seed: int + :pram sets: list of flag sets + :type sets: list """ self._name = name self._seed = seed @@ -90,6 +93,7 @@ def __init__( # pylint: disable=too-many-arguments self._algo = HashAlgorithm.LEGACY self._configurations = configurations + self._sets = set(sets) if sets is not None else set() @property def name(self): @@ -146,6 +150,11 @@ def traffic_allocation_seed(self): """Return the traffic allocation seed of the split.""" return self._traffic_allocation_seed + @property + def sets(self): + """Return the flag sets of the split.""" + return self._sets + def get_configurations_for(self, treatment): """Return the mapping of treatments to configurations.""" return self._configurations.get(treatment) if self._configurations else None @@ -173,7 +182,8 @@ def to_json(self): 'defaultTreatment': self.default_treatment, 'algo': self.algo.value, 'conditions': [c.to_json() for c in self.conditions], - 'configurations': self._configurations + 'configurations': self._configurations, + 'sets': list(self._sets) } def to_split_view(self): @@ -189,7 +199,9 @@ def to_split_view(self): self.killed, list(set(part.treatment for cond in self.conditions for part in cond.partitions)), self.change_number, - self._configurations if self._configurations is not None else {} + self._configurations if self._configurations is not None else {}, + self._default_treatment, + list(self._sets) if self._sets is not None else [] ) def local_kill(self, default_treatment, change_number): @@ -238,5 +250,6 @@ def from_raw(raw_split): raw_split.get('algo'), traffic_allocation=raw_split.get('trafficAllocation'), traffic_allocation_seed=raw_split.get('trafficAllocationSeed'), - configurations=raw_split.get('configurations') + configurations=raw_split.get('configurations'), + sets=set(raw_split.get('sets')) if raw_split.get('sets') is not None else [] ) diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index 3ac87316..e1685b3d 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -27,7 +27,7 @@ class CounterConstants(Enum): EVENTS_QUEUED = 'eventsQueued' EVENTS_DROPPED = 'eventsDropped' -class ConfigParams(Enum): +class _ConfigParams(Enum): """Config parameters constants""" SPLITS_REFRESH_RATE = 'featuresRefreshRate' SEGMENTS_REFRESH_RATE = 'segmentsRefreshRate' @@ -42,7 +42,7 @@ class ConfigParams(Enum): IMPRESSIONS_MODE = 'impressionsMode' IMPRESSIONS_LISTENER = 'impressionListener' -class ExtraConfig(Enum): +class _ExtraConfig(Enum): """Extra config constants""" ACTIVE_FACTORY_COUNT = 'activeFactoryCount' REDUNDANT_FACTORY_COUNT = 'redundantFactoryCount' @@ -53,7 +53,7 @@ class ExtraConfig(Enum): HTTP_PROXY = 'httpProxy' HTTPS_PROXY_ENV = 'HTTPS_PROXY' -class ApiURLs(Enum): +class _ApiURLs(Enum): """Api URL constants""" SDK_URL = 'sdk_url' EVENTS_URL = 'events_url' @@ -82,9 +82,13 @@ class MethodExceptionsAndLatencies(Enum): TREATMENTS = 'treatments' TREATMENT_WITH_CONFIG = 'treatment_with_config' TREATMENTS_WITH_CONFIG = 'treatments_with_config' + TREATMENTS_BY_FLAG_SET = 'treatments_by_flag_set' + TREATMENTS_BY_FLAG_SETS = 'treatments_by_flag_sets' + TREATMENTS_WITH_CONFIG_BY_FLAG_SET = 'treatments_with_config_by_flag_set' + TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 'treatments_with_config_by_flag_sets' TRACK = 'track' -class LastSynchronizationConstants(Enum): +class _LastSynchronizationConstants(Enum): """Last sync constants""" LAST_SYNCHRONIZATIONS = 'lastSynchronizations' @@ -104,7 +108,7 @@ class SSESyncMode(Enum): STREAMING = 0 POLLING = 1 -class StreamingEventsConstant(Enum): +class _StreamingEventsConstant(Enum): """Storage types constant""" STREAMING_EVENTS = 'streamingEvents' @@ -166,6 +170,10 @@ def _reset_all(self): self._treatments = [0] * MAX_LATENCY_BUCKET_COUNT self._treatment_with_config = [0] * MAX_LATENCY_BUCKET_COUNT self._treatments_with_config = [0] * MAX_LATENCY_BUCKET_COUNT + self._treatments_by_flag_set = [0] * MAX_LATENCY_BUCKET_COUNT + self._treatments_by_flag_sets = [0] * MAX_LATENCY_BUCKET_COUNT + self._treatments_with_config_by_flag_set = [0] * MAX_LATENCY_BUCKET_COUNT + self._treatments_with_config_by_flag_sets = [0] * MAX_LATENCY_BUCKET_COUNT self._track = [0] * MAX_LATENCY_BUCKET_COUNT def add_latency(self, method, latency): @@ -187,6 +195,14 @@ def add_latency(self, method, latency): self._treatment_with_config[latency_bucket] += 1 elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG: self._treatments_with_config[latency_bucket] += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET: + self._treatments_by_flag_set[latency_bucket] += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS: + self._treatments_by_flag_sets[latency_bucket] += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET: + self._treatments_with_config_by_flag_set[latency_bucket] += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS: + self._treatments_with_config_by_flag_sets[latency_bucket] += 1 elif method == MethodExceptionsAndLatencies.TRACK: self._track[latency_bucket] += 1 else: @@ -200,10 +216,18 @@ def pop_all(self): :rtype: dict """ with self._lock: - latencies = {MethodExceptionsAndLatencies.METHOD_LATENCIES.value: {MethodExceptionsAndLatencies.TREATMENT.value: self._treatment, MethodExceptionsAndLatencies.TREATMENTS.value: self._treatments, - MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG.value: self._treatment_with_config, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG.value: self._treatments_with_config, - MethodExceptionsAndLatencies.TRACK.value: self._track} + latencies = {MethodExceptionsAndLatencies.METHOD_LATENCIES.value: { + MethodExceptionsAndLatencies.TREATMENT.value: self._treatment, + MethodExceptionsAndLatencies.TREATMENTS.value: self._treatments, + MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG.value: self._treatment_with_config, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG.value: self._treatments_with_config, + MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET.value: self._treatments_by_flag_set, + MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS.value: self._treatments_by_flag_sets, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET.value: self._treatments_with_config_by_flag_set, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS.value: self._treatments_with_config_by_flag_sets, + MethodExceptionsAndLatencies.TRACK.value: self._track } + } self._reset_all() return latencies @@ -288,6 +312,10 @@ def _reset_all(self): self._treatments = 0 self._treatment_with_config = 0 self._treatments_with_config = 0 + self._treatments_by_flag_set = 0 + self._treatments_by_flag_sets = 0 + self._treatments_with_config_by_flag_set = 0 + self._treatments_with_config_by_flag_sets = 0 self._track = 0 def add_exception(self, method): @@ -306,6 +334,14 @@ def add_exception(self, method): self._treatment_with_config += 1 elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG: self._treatments_with_config += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET: + self._treatments_by_flag_set += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS: + self._treatments_by_flag_sets += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET: + self._treatments_with_config_by_flag_set += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS: + self._treatments_with_config_by_flag_sets += 1 elif method == MethodExceptionsAndLatencies.TRACK: self._track += 1 else: @@ -319,10 +355,18 @@ def pop_all(self): :rtype: dict """ with self._lock: - exceptions = {MethodExceptionsAndLatencies.METHOD_EXCEPTIONS.value: {MethodExceptionsAndLatencies.TREATMENT.value: self._treatment, MethodExceptionsAndLatencies.TREATMENTS.value: self._treatments, - MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG.value: self._treatment_with_config, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG.value: self._treatments_with_config, - MethodExceptionsAndLatencies.TRACK.value: self._track} + exceptions = {MethodExceptionsAndLatencies.METHOD_EXCEPTIONS.value: { + MethodExceptionsAndLatencies.TREATMENT.value: self._treatment, + MethodExceptionsAndLatencies.TREATMENTS.value: self._treatments, + MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG.value: self._treatment_with_config, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG.value: self._treatments_with_config, + MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET.value: self._treatments_by_flag_set, + MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS.value: self._treatments_by_flag_sets, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET.value: self._treatments_with_config_by_flag_set, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS.value: self._treatments_with_config_by_flag_sets, + MethodExceptionsAndLatencies.TRACK.value: self._track } + } self._reset_all() return exceptions @@ -382,7 +426,7 @@ def get_all(self): :rtype: dict """ with self._lock: - return {LastSynchronizationConstants.LAST_SYNCHRONIZATIONS.value: {HTTPExceptionsAndLatencies.SPLIT.value: self._split, HTTPExceptionsAndLatencies.SEGMENT.value: self._segment, HTTPExceptionsAndLatencies.IMPRESSION.value: self._impression, + return {_LastSynchronizationConstants.LAST_SYNCHRONIZATIONS.value: {HTTPExceptionsAndLatencies.SPLIT.value: self._split, HTTPExceptionsAndLatencies.SEGMENT.value: self._segment, HTTPExceptionsAndLatencies.IMPRESSION.value: self._impression, HTTPExceptionsAndLatencies.IMPRESSION_COUNT.value: self._impression_count, HTTPExceptionsAndLatencies.EVENT.value: self._event, HTTPExceptionsAndLatencies.TELEMETRY.value: self._telemetry, HTTPExceptionsAndLatencies.TOKEN.value: self._token} } @@ -716,7 +760,7 @@ def pop_streaming_events(self): with self._lock: streaming_events = self._streaming_events self._streaming_events = [] - return {StreamingEventsConstant.STREAMING_EVENTS.value: [{'e': streaming_event.type, 'd': streaming_event.data, + return {_StreamingEventsConstant.STREAMING_EVENTS.value: [{'e': streaming_event.type, 'd': streaming_event.data, 't': streaming_event.time} for streaming_event in streaming_events]} class TelemetryConfig(object): @@ -738,10 +782,10 @@ def _reset_all(self): self._operation_mode = None self._storage_type = None self._streaming_enabled = None - self._refresh_rate = {ConfigParams.SPLITS_REFRESH_RATE.value: 0, ConfigParams.SEGMENTS_REFRESH_RATE.value: 0, - ConfigParams.IMPRESSIONS_REFRESH_RATE.value: 0, ConfigParams.EVENTS_REFRESH_RATE.value: 0, ConfigParams.TELEMETRY_REFRESH_RATE.value: 0} - self._url_override = {ApiURLs.SDK_URL.value: False, ApiURLs.EVENTS_URL.value: False, ApiURLs.AUTH_URL.value: False, - ApiURLs.STREAMING_URL.value: False, ApiURLs.TELEMETRY_URL.value: False} + self._refresh_rate = {_ConfigParams.SPLITS_REFRESH_RATE.value: 0, _ConfigParams.SEGMENTS_REFRESH_RATE.value: 0, + _ConfigParams.IMPRESSIONS_REFRESH_RATE.value: 0, _ConfigParams.EVENTS_REFRESH_RATE.value: 0, _ConfigParams.TELEMETRY_REFRESH_RATE.value: 0} + self._url_override = {_ApiURLs.SDK_URL.value: False, _ApiURLs.EVENTS_URL.value: False, _ApiURLs.AUTH_URL.value: False, + _ApiURLs.STREAMING_URL.value: False, _ApiURLs.TELEMETRY_URL.value: False} self._impressions_queue_size = 0 self._events_queue_size = 0 self._impressions_mode = None @@ -749,8 +793,10 @@ def _reset_all(self): self._http_proxy = None self._active_factory_count = 0 self._redundant_factory_count = 0 + self._flag_sets = 0 + self._flag_sets_invalid = 0 - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets, invalid_flag_sets): """ Record configurations. @@ -773,23 +819,24 @@ def record_config(self, config, extra_config): :type config: dict """ with self._lock: - self._operation_mode = self._get_operation_mode(config[ConfigParams.OPERATION_MODE.value]) - self._storage_type = self._get_storage_type(config[ConfigParams.OPERATION_MODE.value], config[ConfigParams.STORAGE_TYPE.value]) - self._streaming_enabled = config[ConfigParams.STREAMING_ENABLED.value] + self._operation_mode = self._get_operation_mode(config[_ConfigParams.OPERATION_MODE.value]) + self._storage_type = self._get_storage_type(config[_ConfigParams.OPERATION_MODE.value], config[_ConfigParams.STORAGE_TYPE.value]) + self._streaming_enabled = config[_ConfigParams.STREAMING_ENABLED.value] self._refresh_rate = self._get_refresh_rates(config) self._url_override = self._get_url_overrides(extra_config) - self._impressions_queue_size = config[ConfigParams.IMPRESSIONS_QUEUE_SIZE.value] - self._events_queue_size = config[ConfigParams.EVENTS_QUEUE_SIZE.value] - self._impressions_mode = self._get_impressions_mode(config[ConfigParams.IMPRESSIONS_MODE.value]) - self._impression_listener = True if config[ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False + self._impressions_queue_size = config[_ConfigParams.IMPRESSIONS_QUEUE_SIZE.value] + self._events_queue_size = config[_ConfigParams.EVENTS_QUEUE_SIZE.value] + self._impressions_mode = self._get_impressions_mode(config[_ConfigParams.IMPRESSIONS_MODE.value]) + self._impression_listener = True if config[_ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False self._http_proxy = self._check_if_proxy_detected() + self._flag_sets = total_flag_sets + self._flag_sets_invalid = invalid_flag_sets def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): with self._lock: self._active_factory_count = active_factory_count self._redundant_factory_count = redundant_factory_count - def record_ready_time(self, ready_time): """ Record ready time. @@ -851,23 +898,25 @@ def get_stats(self): 'oM': self._operation_mode, 'sT': self._storage_type, 'sE': self._streaming_enabled, - 'rR': {'sp': self._refresh_rate[ConfigParams.SPLITS_REFRESH_RATE.value], - 'se': self._refresh_rate[ConfigParams.SEGMENTS_REFRESH_RATE.value], - 'im': self._refresh_rate[ConfigParams.IMPRESSIONS_REFRESH_RATE.value], - 'ev': self._refresh_rate[ConfigParams.EVENTS_REFRESH_RATE.value], - 'te': self._refresh_rate[ConfigParams.TELEMETRY_REFRESH_RATE.value]}, - 'uO': {'s': self._url_override[ApiURLs.SDK_URL.value], - 'e': self._url_override[ApiURLs.EVENTS_URL.value], - 'a': self._url_override[ApiURLs.AUTH_URL.value], - 'st': self._url_override[ApiURLs.STREAMING_URL.value], - 't': self._url_override[ApiURLs.TELEMETRY_URL.value]}, + 'rR': {'sp': self._refresh_rate[_ConfigParams.SPLITS_REFRESH_RATE.value], + 'se': self._refresh_rate[_ConfigParams.SEGMENTS_REFRESH_RATE.value], + 'im': self._refresh_rate[_ConfigParams.IMPRESSIONS_REFRESH_RATE.value], + 'ev': self._refresh_rate[_ConfigParams.EVENTS_REFRESH_RATE.value], + 'te': self._refresh_rate[_ConfigParams.TELEMETRY_REFRESH_RATE.value]}, + 'uO': {'s': self._url_override[_ApiURLs.SDK_URL.value], + 'e': self._url_override[_ApiURLs.EVENTS_URL.value], + 'a': self._url_override[_ApiURLs.AUTH_URL.value], + 'st': self._url_override[_ApiURLs.STREAMING_URL.value], + 't': self._url_override[_ApiURLs.TELEMETRY_URL.value]}, 'iQ': self._impressions_queue_size, 'eQ': self._events_queue_size, 'iM': self._impressions_mode, 'iL': self._impression_listener, 'hp': self._http_proxy, 'aF': self._active_factory_count, - 'rF': self._redundant_factory_count + 'rF': self._redundant_factory_count, + 'fsT': self._flag_sets, + 'fsI': self._flag_sets_invalid } def _get_operation_mode(self, op_mode): @@ -918,11 +967,11 @@ def _get_refresh_rates(self, config): """ with self._lock: return { - ConfigParams.SPLITS_REFRESH_RATE.value: config[ConfigParams.SPLITS_REFRESH_RATE.value], - ConfigParams.SEGMENTS_REFRESH_RATE.value: config[ConfigParams.SEGMENTS_REFRESH_RATE.value], - ConfigParams.IMPRESSIONS_REFRESH_RATE.value: config[ConfigParams.IMPRESSIONS_REFRESH_RATE.value], - ConfigParams.EVENTS_REFRESH_RATE.value: config[ConfigParams.EVENTS_REFRESH_RATE.value], - ConfigParams.TELEMETRY_REFRESH_RATE.value: config[ConfigParams.TELEMETRY_REFRESH_RATE.value] + _ConfigParams.SPLITS_REFRESH_RATE.value: config[_ConfigParams.SPLITS_REFRESH_RATE.value], + _ConfigParams.SEGMENTS_REFRESH_RATE.value: config[_ConfigParams.SEGMENTS_REFRESH_RATE.value], + _ConfigParams.IMPRESSIONS_REFRESH_RATE.value: config[_ConfigParams.IMPRESSIONS_REFRESH_RATE.value], + _ConfigParams.EVENTS_REFRESH_RATE.value: config[_ConfigParams.EVENTS_REFRESH_RATE.value], + _ConfigParams.TELEMETRY_REFRESH_RATE.value: config[_ConfigParams.TELEMETRY_REFRESH_RATE.value] } def _get_url_overrides(self, config): @@ -937,11 +986,11 @@ def _get_url_overrides(self, config): """ with self._lock: return { - ApiURLs.SDK_URL.value: True if ApiURLs.SDK_URL.value in config else False, - ApiURLs.EVENTS_URL.value: True if ApiURLs.EVENTS_URL.value in config else False, - ApiURLs.AUTH_URL.value: True if ApiURLs.AUTH_URL.value in config else False, - ApiURLs.STREAMING_URL.value: True if ApiURLs.STREAMING_URL.value in config else False, - ApiURLs.TELEMETRY_URL.value: True if ApiURLs.TELEMETRY_URL.value in config else False + _ApiURLs.SDK_URL.value: True if _ApiURLs.SDK_URL.value in config else False, + _ApiURLs.EVENTS_URL.value: True if _ApiURLs.EVENTS_URL.value in config else False, + _ApiURLs.AUTH_URL.value: True if _ApiURLs.AUTH_URL.value in config else False, + _ApiURLs.STREAMING_URL.value: True if _ApiURLs.STREAMING_URL.value in config else False, + _ApiURLs.TELEMETRY_URL.value: True if _ApiURLs.TELEMETRY_URL.value in config else False } def _get_impressions_mode(self, imp_mode): @@ -971,6 +1020,6 @@ def _check_if_proxy_detected(self): """ with self._lock: for x in os.environ: - if x.upper() == ExtraConfig.HTTPS_PROXY_ENV.value: + if x.upper() == _ExtraConfig.HTTPS_PROXY_ENV.value: return True return False \ No newline at end of file diff --git a/splitio/push/manager.py b/splitio/push/manager.py index 1fec6ea1..51f44343 100644 --- a/splitio/push/manager.py +++ b/splitio/push/manager.py @@ -143,7 +143,8 @@ def _trigger_connection_flow(self): self._feedback_loop.put(Status.PUSH_RETRYABLE_ERROR) return - if not token.push_enabled: + + if token is None or not token.push_enabled: self._feedback_loop.put(Status.PUSH_NONRETRYABLE_ERROR) return self._telemetry_runtime_producer.record_token_refreshes() diff --git a/splitio/push/splitworker.py b/splitio/push/splitworker.py index 96654040..00329c44 100644 --- a/splitio/push/splitworker.py +++ b/splitio/push/splitworker.py @@ -10,7 +10,7 @@ from splitio.models.splits import from_raw, Status from splitio.models.telemetry import UpdateFromSSE from splitio.push.parser import UpdateType - +from splitio.util.storage_helper import update_feature_flag_storage _LOGGER = logging.getLogger(__name__) @@ -88,17 +88,12 @@ def _run(self): try: if self._check_instant_ff_update(event): try: - new_split = from_raw(json.loads(self._get_feature_flag_definition(event))) - if new_split.status == Status.ACTIVE: - self._feature_flag_storage.put(new_split) - _LOGGER.debug('Feature flag %s is updated', new_split.name) - for segment_name in new_split.get_segment_names(): - if self._segment_storage.get(segment_name) is None: - _LOGGER.debug('Fetching new segment %s', segment_name) - self._segment_handler(segment_name, event.change_number) - else: - self._feature_flag_storage.remove(new_split.name) - self._feature_flag_storage.set_change_number(event.change_number) + new_feature_flag = from_raw(json.loads(self._get_feature_flag_definition(event))) + segment_list = update_feature_flag_storage(self._feature_flag_storage, [new_feature_flag], event.change_number) + for segment_name in segment_list: + if self._segment_storage.get(segment_name) is None: + _LOGGER.debug('Fetching new segment %s', segment_name) + self._segment_handler(segment_name, event.change_number) self._telemetry_runtime_producer.record_update_from_sse(UpdateFromSSE.SPLIT_UPDATE) continue except Exception as e: diff --git a/splitio/storage/__init__.py b/splitio/storage/__init__.py index 5467bc14..76b63070 100644 --- a/splitio/storage/__init__.py +++ b/splitio/storage/__init__.py @@ -1,7 +1,6 @@ """Base storage interfaces.""" import abc - class SplitStorage(object, metaclass=abc.ABCMeta): """Split storage interface implemented as an abstract class.""" @@ -30,25 +29,16 @@ def fetch_many(self, split_names): pass @abc.abstractmethod - def put(self, split): - """ - Store a split. - - :param split: Split object to store - :type split_name: splitio.models.splits.Split - """ - pass - - @abc.abstractmethod - def remove(self, split_name): + def update(self, to_add, to_delete, new_change_number): """ - Remove a split from storage. - - :param split_name: Name of the feature to remove. - :type split_name: str + Update feature flag storage. - :return: True if the split was found and removed. False otherwise. - :rtype: bool + :param to_add: List of feature flags to add + :type to_add: list[splitio.models.splits.Split] + :param to_delete: List of feature flags to delete + :type to_delete: list[splitio.models.splits.Split] + :param new_change_number: New change number. + :type new_change_number: int """ pass @@ -61,16 +51,6 @@ def get_change_number(self): """ pass - @abc.abstractmethod - def set_change_number(self, new_change_number): - """ - Set the latest change number. - - :param new_change_number: New change number. - :type new_change_number: int - """ - pass - @abc.abstractmethod def get_split_names(self): """ @@ -334,3 +314,43 @@ def record_bur_time_out(self): """ pass + +class FlagSetsFilter(object): + """Config Flagsets Filter storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self.flag_sets = set(flag_sets) + self.should_filter = any(flag_sets) + self.sorted_flag_sets = sorted(flag_sets) + + def set_exist(self, flag_set): + """ + Check if a flagset exist in flagset filter + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + if not self.should_filter: + return True + if not isinstance(flag_set, str) or flag_set == '': + return False + + return any(self.flag_sets.intersection(set([flag_set]))) + + def intersect(self, flag_sets): + """ + Check if a set exist in config flagset filter + + :param flag_set: set of flagsets + :type flag_set: set + + :rtype: bool + """ + if not self.should_filter: + return True + if not isinstance(flag_sets, set) or len(flag_sets) == 0: + return False + return any(self.flag_sets.intersection(flag_sets)) diff --git a/splitio/storage/adapters/redis.py b/splitio/storage/adapters/redis.py index de3026b3..8657b317 100644 --- a/splitio/storage/adapters/redis.py +++ b/splitio/storage/adapters/redis.py @@ -339,6 +339,10 @@ def execute(self): except RedisError as exc: raise RedisAdapterException('Error executing pipeline operation') from exc + def smembers(self, name): + """Mimic original redis function but using user custom prefix.""" + self._pipe.smembers(self._prefix_helper.add_prefix(name)) + def _build_default_client(config): # pylint: disable=too-many-locals """ diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 00dbb16b..6d74bdad 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -6,6 +6,7 @@ from splitio.models.segments import Segment from splitio.models.telemetry import HTTPErrors, HTTPLatencies, MethodExceptions, MethodLatencies, LastSynchronization, StreamingEvents, TelemetryConfig, TelemetryCounters, CounterConstants +from splitio.storage import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage MAX_SIZE_BYTES = 5 * 1024 * 1024 @@ -13,16 +14,100 @@ _LOGGER = logging.getLogger(__name__) +class FlagSets(object): + """InMemory Flagsets storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self._lock = threading.RLock() + self.sets_feature_flag_map = {} + for flag_set in flag_sets: + self.sets_feature_flag_map[flag_set] = set() + + def flag_set_exist(self, flag_set): + """ + Check if a flagset exist in stored flagset + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + with self._lock: + return flag_set in self.sets_feature_flag_map.keys() + + def get_flag_set(self, flag_set): + """ + fetch feature flags stored in a flag set + + :param flag_set: set name + :type flag_set: str + + :rtype: list(str) + """ + with self._lock: + return self.sets_feature_flag_map.get(flag_set) + + def add_flag_set(self, flag_set): + """ + Add new flag set to storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if not self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set] = set() + + def remove_flag_set(self, flag_set): + """ + Remove existing flag set from storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + del self.sets_feature_flag_map[flag_set] + + def add_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Add a feature flag to existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].add(feature_flag) + + def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Remove a feature flag from existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].remove(feature_flag) + class InMemorySplitStorage(SplitStorage): - """InMemory implementation of a split storage.""" + """InMemory implementation of a feature flag storage.""" - def __init__(self): + def __init__(self, flag_sets=[]): """Constructor.""" self._lock = threading.RLock() self._splits = {} self._change_number = -1 self._traffic_types = Counter() + self.flag_set = FlagSets(flag_sets) + self.flag_set_filter = FlagSetsFilter(flag_sets) def get(self, split_name): """ @@ -48,7 +133,22 @@ def fetch_many(self, split_names): """ return {split_name: self.get(split_name) for split_name in split_names} - def put(self, split): + def update(self, to_add, to_delete, new_change_number): + """ + Update feature flag storage. + + :param to_add: List of feature flags to add + :type to_add: list[splitio.models.splits.Split] + :param to_delete: List of feature flags to delete + :type to_delete: list[str] + :param new_change_number: New change number. + :type new_change_number: int + """ + [self._put(add_split) for add_split in to_add] + [self._remove(delete_split) for delete_split in to_delete] + self._set_change_number(new_change_number) + + def _put(self, split): """ Store a split. @@ -57,11 +157,19 @@ def put(self, split): """ with self._lock: if split.name in self._splits: + self._remove_from_flag_sets(self._splits[split.name]) self._decrease_traffic_type_count(self._splits[split.name].traffic_type_name) self._splits[split.name] = split self._increase_traffic_type_count(split.traffic_type_name) + if split.sets is not None: + for flag_set in split.sets: + if not self.flag_set.flag_set_exist(flag_set): + if self.flag_set_filter.should_filter: + continue + self.flag_set.add_flag_set(flag_set) + self.flag_set.add_feature_flag_to_flag_set(flag_set, split.name) - def remove(self, split_name): + def _remove(self, split_name): """ Remove a split from storage. @@ -79,18 +187,54 @@ def remove(self, split_name): self._splits.pop(split_name) self._decrease_traffic_type_count(split.traffic_type_name) + self._remove_from_flag_sets(split) return True + def _remove_from_flag_sets(self, feature_flag): + """ + Remove flag sets associated to a split + + :param feature_flag: feature flag object + :type feature_flag: splitio.models.splits.Split + """ + if feature_flag.sets is not None: + for flag_set in feature_flag.sets: + self.flag_set.remove_feature_flag_to_flag_set(flag_set, feature_flag.name) + if self.is_flag_set_exist(flag_set) and len(self.flag_set.get_flag_set(flag_set)) == 0 and not self.flag_set_filter.should_filter: + self.flag_set.remove_flag_set(flag_set) + + def get_feature_flags_by_sets(self, sets): + """ + Get list of feature flag names associated to a set, if it does not exist will return empty list + + :param set: flag set + :type set: str + + :return: list of feature flag names + :rtype: list + """ + with self._lock: + sets_to_fetch = [] + for flag_set in sets: + if not self.flag_set.flag_set_exist(flag_set): + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring it." % (flag_set)) + continue + sets_to_fetch.append(flag_set) + + to_return = set() + [to_return.update(self.flag_set.get_flag_set(flag_set)) for flag_set in sets_to_fetch] + return list(to_return) + def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ with self._lock: return self._change_number - def set_change_number(self, new_change_number): + def _set_change_number(self, new_change_number): """ Set the latest change number. @@ -102,9 +246,9 @@ def set_change_number(self, new_change_number): def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ with self._lock: @@ -112,9 +256,9 @@ def get_split_names(self): def get_all_splits(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ with self._lock: @@ -122,7 +266,7 @@ def get_all_splits(self): def get_splits_count(self): """ - Return splits count. + Return feature flags count. :rtype: int """ @@ -131,7 +275,7 @@ def get_splits_count(self): def is_valid_traffic_type(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -142,12 +286,12 @@ def is_valid_traffic_type(self, traffic_type_name): with self._lock: return traffic_type_name in self._traffic_types - def kill_locally(self, split_name, default_treatment, change_number): + def kill_locally(self, feature_flag_name, default_treatment, change_number): """ - Local kill for split + Local kill for feature flag - :param split_name: name of the split to perform kill - :type split_name: str + :param feature_flag_name: name of the feature flag to perform kill + :type feature_flag_name: str :param default_treatment: name of the default treatment to return :type default_treatment: str :param change_number: change_number @@ -156,11 +300,11 @@ def kill_locally(self, split_name, default_treatment, change_number): with self._lock: if self.get_change_number() > change_number: return - split = self._splits.get(split_name) + split = self._splits.get(feature_flag_name) if not split: return split.local_kill(default_treatment, change_number) - self.put(split) + self._put(split) def _increase_traffic_type_count(self, traffic_type_name): """ @@ -181,6 +325,17 @@ def _decrease_traffic_type_count(self, traffic_type_name): self._traffic_types.subtract([traffic_type_name]) self._traffic_types += Counter() + def is_flag_set_exist(self, flag_set): + """ + Return whether a flag set exists in at least one feature flag in cache. + + :param flag_set: Flag set to validate. + :type flag_set: str + + :return: True if the flag_set exist. False otherwise. + :rtype: bool + """ + return self.flag_set.flag_set_exist(flag_set) class InMemorySegmentStorage(SegmentStorage): """In-memory implementation of a segment storage.""" @@ -487,9 +642,9 @@ def _reset_config_tags(self): with self._lock: self._config_tags = [] - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets, invalid_flag_sets): """Record configurations.""" - self._tel_config.record_config(config, extra_config) + self._tel_config.record_config(config, extra_config, total_flag_sets, invalid_flag_sets) def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): """Record active and redundant factories.""" diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index a15df284..d1503af3 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -7,16 +7,18 @@ from splitio.models import splits, segments from splitio.models.impressions import Impression from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MAX_TAGS, get_latency_bucket_index +from splitio.storage import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage +from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets _LOGGER = logging.getLogger(__name__) class PluggableSplitStorage(SplitStorage): - """InMemory implementation of a split storage.""" + """InMemory implementation of feature flag storage.""" - _SPLIT_NAME_LENGTH = 12 + _FEATURE_FLAG_NAME_LENGTH = 19 - def __init__(self, pluggable_adapter, prefix=None): + def __init__(self, pluggable_adapter, prefix=None, config_flag_sets=[]): """ Class constructor. @@ -26,48 +28,99 @@ def __init__(self, pluggable_adapter, prefix=None): :type prefix: str """ self._pluggable_adapter = pluggable_adapter - self._prefix = "SPLITIO.split.{split_name}" + self._prefix = "SPLITIO.split.{feature_flag_name}" self._traffic_type_prefix = "SPLITIO.trafficType.{traffic_type_name}" - self._split_till_prefix = "SPLITIO.splits.till" + self._feature_flag_till_prefix = "SPLITIO.splits.till" + self._flag_set_prefix = 'SPLITIO.flagSet.{flag_set}' + self.flag_set_filter = FlagSetsFilter(config_flag_sets) if prefix is not None: self._prefix = prefix + "." + self._prefix self._traffic_type_prefix = prefix + "." + self._traffic_type_prefix - self._split_till_prefix = prefix + "." + self._split_till_prefix + self._feature_flag_till_prefix = prefix + "." + self._feature_flag_till_prefix + self._flag_set_prefix = prefix + "." + self._flag_set_prefix - def get(self, split_name): + def get(self, feature_flag_name): """ - Retrieve a split. + Retrieve a feature flag. - :param split_name: Name of the feature to fetch. - :type split_name: str + :param feature_flag_name: Name of the feature to fetch. + :type feature_flag_name: str :rtype: splitio.models.splits.Split """ try: - split = self._pluggable_adapter.get(self._prefix.format(split_name=split_name)) - if not split: + feature_flag = self._pluggable_adapter.get(self._prefix.format(feature_flag_name=feature_flag_name)) + if not feature_flag: return None - return splits.from_raw(split) + return splits.from_raw(feature_flag) except Exception: - _LOGGER.error('Error getting split from storage') + _LOGGER.error('Error getting feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def fetch_many(self, split_names): + def fetch_many(self, feature_flag_names): """ - Retrieve splits. + Retrieve feature flags. - :param split_names: Names of the features to fetch. - :type split_name: list(str) + :param feature_flag_names: Names of the features to fetch. + :type feature_flag_name: list(str) :return: A dict with split objects parsed from queue. :rtype: dict(split_name, splitio.models.splits.Split) """ try: - prefix_added = [self._prefix.format(split_name=split_name) for split_name in split_names] - return {split['name']: splits.from_raw(split) for split in self._pluggable_adapter.get_many(prefix_added)} + prefix_added = [self._prefix.format(feature_flag_name=feature_flag_name) for feature_flag_name in feature_flag_names] + return {feature_flag['name']: splits.from_raw(feature_flag) for feature_flag in self._pluggable_adapter.get_many(prefix_added)} + except Exception: + _LOGGER.error('Error getting feature flag from storage') + _LOGGER.debug('Error: ', exc_info=True) + return None + + def get_feature_flags_by_sets(self, flag_sets): + """ + Retrieve feature flags by flag set. + + :param flag_sets: List of flag sets to fetch. + :type flag_sets: list(str) + + :return: Feature flag names that are tagged with the flag set + :rtype: listt(str) + """ + try: + sets_to_fetch = get_valid_flag_sets(flag_sets, self.flag_set_filter) + if sets_to_fetch == []: + return [] + + keys = [self._flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] + result_sets = [] + [result_sets.append(set(key)) for key in self._pluggable_adapter.get_many(keys)] + return list(combine_valid_flag_sets(result_sets)) + except Exception: + _LOGGER.error('Error fetching feature flag from storage') + _LOGGER.debug('Error: ', exc_info=True) + return None + + def get_feature_flags_by_sets(self, flag_sets): + """ + Retrieve feature flags by flag set. + + :param flag_set: Names of the flag set to fetch. + :type flag_set: str + + :return: Feature flag names that are tagged with the flag set + :rtype: listt(str) + """ + try: + sets_to_fetch = get_valid_flag_sets(flag_sets, self.flag_set_filter) + if sets_to_fetch == []: + return [] + + keys = [self._flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] + result_sets = [] + [result_sets.append(set(key)) for key in self._pluggable_adapter.get_many(keys)] + return list(combine_valid_flag_sets(result_sets)) except Exception: - _LOGGER.error('Error getting split from storage') + _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -87,7 +140,21 @@ def fetch_many(self, split_names): # _LOGGER.error('Error storing splits in storage') # _LOGGER.debug('Error: ', exc_info=True) - def remove(self, split_name): + def update(self, to_add, to_delete, new_change_number): + """ + Update feature flag storage. + + :param to_add: List of feature flags to add + :type to_add: list[splitio.models.splits.Split] + :param to_delete: List of feature flags to delete + :type to_delete: list[splitio.models.splits.Split] + :param new_change_number: New change number. + :type new_change_number: int + """ + pass + + # TODO: To be added when producer mode is aupported +# def _remove(self, split_name): """ Remove a split from storage. @@ -97,8 +164,7 @@ def remove(self, split_name): :return: True if the split was found and removed. False otherwise. :rtype: bool """ - pass - # TODO: To be added when producer mode is aupported +# pass # try: # split = self.get(split_name) # if not split: @@ -114,26 +180,26 @@ def remove(self, split_name): def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ try: - return self._pluggable_adapter.get(self._split_till_prefix) + return self._pluggable_adapter.get(self._feature_flag_till_prefix) except Exception: - _LOGGER.error('Error getting change number in split storage') + _LOGGER.error('Error getting change number in feature flag storage') _LOGGER.debug('Error: ', exc_info=True) return None - def set_change_number(self, new_change_number): + # TODO: To be added when producer mode is aupported +# def _set_change_number(self, new_change_number): """ Set the latest change number. :param new_change_number: New change number. :type new_change_number: int """ - pass - # TODO: To be added when producer mode is aupported +# pass # try: # self._pluggable_adapter.set(self._split_till_prefix, new_change_number) # except Exception: @@ -143,35 +209,35 @@ def set_change_number(self, new_change_number): def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ try: - return [split.name for split in self.get_all()] + return [feature_flag.name for feature_flag in self.get_all()] except Exception: - _LOGGER.error('Error getting split names from storage') + _LOGGER.error('Error getting feature flag names from storage') _LOGGER.debug('Error: ', exc_info=True) return None def get_all(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ try: - return [splits.from_raw(self._pluggable_adapter.get(key)) for key in self._pluggable_adapter.get_keys_by_prefix(self._prefix[:-self._SPLIT_NAME_LENGTH])] + return [splits.from_raw(self._pluggable_adapter.get(key)) for key in self._pluggable_adapter.get_keys_by_prefix(self._prefix[:-self._FEATURE_FLAG_NAME_LENGTH])] except Exception: - _LOGGER.error('Error getting split keys from storage') + _LOGGER.error('Error getting feature flag keys from storage') _LOGGER.debug('Error: ', exc_info=True) return None def traffic_type_exists(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -182,7 +248,7 @@ def traffic_type_exists(self, traffic_type_name): try: return self._pluggable_adapter.get(self._traffic_type_prefix.format(traffic_type_name=traffic_type_name)) != None except Exception: - _LOGGER.error('Error getting split info from storage') + _LOGGER.error('Error getting traffic type info from storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -251,21 +317,21 @@ def kill_locally(self, split_name, default_treatment, change_number): def get_all_splits(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ try: return self.get_all() except Exception: - _LOGGER.error('Error fetching splits from storage') + _LOGGER.error('Error fetching feature flags from storage') _LOGGER.debug('Error: ', exc_info=True) return None def is_valid_traffic_type(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -276,19 +342,19 @@ def is_valid_traffic_type(self, traffic_type_name): try: return self.traffic_type_exists(traffic_type_name) except Exception: - _LOGGER.error('Error getting split info from storage') + _LOGGER.error('Error getting traffic type info from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def put(self, split): + # TODO: To be added when producer mode is aupported +# def _put(self, split): """ Store a split. :param split: Split object. :type split: splitio.models.split.Split """ - pass - # TODO: To be added when producer mode is aupported +# pass # try: # existing_split = self.get(split.name) # self._pluggable_adapter.set(self._prefix.format(split_name=split.name), split.to_json()) @@ -729,7 +795,7 @@ def add_config_tag(self, tag): if len(self._config_tags) < MAX_TAGS: self._config_tags.append(tag) - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets, invalid_flag_sets): """ initilize telemetry objects @@ -738,7 +804,7 @@ def record_config(self, config, extra_config): :param extra_config: any extra configs :type extra_config: Dict """ - self._tel_config.record_config(config, extra_config) + self._tel_config.record_config(config, extra_config, total_flag_sets, invalid_flag_sets) def pop_config_tags(self): """Get and reset configs.""" diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index d2aa2788..4e50f643 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -6,23 +6,25 @@ from splitio.models.impressions import Impression from splitio.models import splits, segments from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, get_latency_bucket_index +from splitio.storage import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, \ ImpressionPipelinedStorage, TelemetryStorage from splitio.storage.adapters.redis import RedisAdapterException from splitio.storage.adapters.cache_trait import decorate as add_cache, DEFAULT_MAX_AGE - +from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets _LOGGER = logging.getLogger(__name__) MAX_TAGS = 10 class RedisSplitStorage(SplitStorage): - """Redis-based storage for splits.""" + """Redis-based storage for feature flags.""" - _SPLIT_KEY = 'SPLITIO.split.{split_name}' - _SPLIT_TILL_KEY = 'SPLITIO.splits.till' + _FEATURE_FLAG_KEY = 'SPLITIO.split.{feature_flag_name}' + _FEATURE_FLAG_TILL_KEY = 'SPLITIO.splits.till' _TRAFFIC_TYPE_KEY = 'SPLITIO.trafficType.{traffic_type_name}' + _FLAG_SET_KEY = 'SPLITIO.flagSet.{flag_set}' - def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE): + def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, config_flag_sets=[]): """ Class constructor. @@ -30,87 +32,128 @@ def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE): :type redis_client: splitio.storage.adapters.redis.RedisAdapter """ self._redis = redis_client + self.flag_set_filter = FlagSetsFilter(config_flag_sets) + self._pipe = self._redis.pipeline if enable_caching: self.get = add_cache(lambda *p, **_: p[0], max_age)(self.get) self.is_valid_traffic_type = add_cache(lambda *p, **_: p[0], max_age)(self.is_valid_traffic_type) # pylint: disable=line-too-long self.fetch_many = add_cache(lambda *p, **_: frozenset(p[0]), max_age)(self.fetch_many) - def _get_key(self, split_name): + def _get_key(self, feature_flag_name): """ - Use the provided split_name to build the appropriate redis key. + Use the provided feature_flag_name to build the appropriate redis key. - :param split_name: Name of the split to interact with in redis. - :type split_name: str + :param feature_flag_name: Name of the feature flag to interact with in redis. + :type feature_flag_name: str :return: Redis key. :rtype: str. """ - return self._SPLIT_KEY.format(split_name=split_name) + return self._FEATURE_FLAG_KEY.format(feature_flag_name=feature_flag_name) def _get_traffic_type_key(self, traffic_type_name): """ - Use the provided split_name to build the appropriate redis key. + Use the provided traffic_type_name to build the appropriate redis key. - :param split_name: Name of the split to interact with in redis. - :type split_name: str + :param trafic_type_name: Name of the traffic type to interact with in redis. + :type traffic_type_name: str :return: Redis key. :rtype: str. """ return self._TRAFFIC_TYPE_KEY.format(traffic_type_name=traffic_type_name) - def get(self, split_name): # pylint: disable=method-hidden + def _get_flag_set_key(self, flag_set): + """ + Use the provided flag set to build the appropriate redis key. + + :param flag_set: Name of the flag set to interact with in redis. + :type flag_set: str + + :return: Redis key. + :rtype: str. + """ + return self._FLAG_SET_KEY.format(flag_set=flag_set) + + def get(self, feature_flag_name): # pylint: disable=method-hidden """ - Retrieve a split. + Retrieve a feature flag. - :param split_name: Name of the feature to fetch. - :type split_name: str + :param feature_flag_name: Name of the feature to fetch. + :type feature_flag_name: str :return: A split object parsed from redis if the key exists. None otherwise :rtype: splitio.models.splits.Split """ try: - raw = self._redis.get(self._get_key(split_name)) - _LOGGER.debug("Fetchting Split [%s] from redis" % split_name) + raw = self._redis.get(self._get_key(feature_flag_name)) + _LOGGER.debug("Fetchting Feature flag [%s] from redis" % feature_flag_name) _LOGGER.debug(raw) return splits.from_raw(json.loads(raw)) if raw is not None else None except RedisAdapterException: - _LOGGER.error('Error fetching split from storage') + _LOGGER.error('Error fetching feature flag from storage') + _LOGGER.debug('Error: ', exc_info=True) + return None + + def get_feature_flags_by_sets(self, flag_sets): + """ + Retrieve feature flags by flag set. + + :param flag_set: Names of the flag set to fetch. + :type flag_set: str + + :return: Feature flag names that are tagged with the flag set + :rtype: listt(str) + """ + try: + sets_to_fetch = get_valid_flag_sets(flag_sets, self.flag_set_filter) + if sets_to_fetch == []: + return [] + + keys = [self._get_flag_set_key(flag_set) for flag_set in sets_to_fetch] + pipe = self._pipe() + [pipe.smembers(key) for key in keys] + result_sets = pipe.execute() + _LOGGER.debug("Fetchting Feature flags by set [%s] from redis" % (keys)) + _LOGGER.debug(result_sets) + return list(combine_valid_flag_sets(result_sets)) + except RedisAdapterException: + _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def fetch_many(self, split_names): + def fetch_many(self, feature_flag_names): """ - Retrieve splits. + Retrieve feature flags. - :param split_names: Names of the features to fetch. - :type split_name: list(str) + :param feature_flag_names: Names of the features to fetch. + :type feature_flag_name: list(str) :return: A dict with split objects parsed from redis. :rtype: dict(split_name, splitio.models.splits.Split) """ to_return = dict() try: - keys = [self._get_key(split_name) for split_name in split_names] - raw_splits = self._redis.mget(keys) - _LOGGER.debug("Fetchting Splits [%s] from redis" % split_names) - _LOGGER.debug(raw_splits) - for i in range(len(split_names)): - split = None + keys = [self._get_key(feature_flag_name) for feature_flag_name in feature_flag_names] + raw_feature_flags = self._redis.mget(keys) + _LOGGER.debug("Fetchting feature flags [%s] from redis" % feature_flag_names) + _LOGGER.debug(raw_feature_flags) + for i in range(len(feature_flag_names)): + feature_flag = None try: - split = splits.from_raw(json.loads(raw_splits[i])) + feature_flag = splits.from_raw(json.loads(raw_feature_flags[i])) except (ValueError, TypeError): - _LOGGER.error('Could not parse split.') - _LOGGER.debug("Raw split that failed parsing attempt: %s", raw_splits[i]) - to_return[split_names[i]] = split + _LOGGER.error('Could not parse feature flag.') + _LOGGER.debug("Raw feature flag that failed parsing attempt: %s", raw_feature_flags[i]) + to_return[feature_flag_names[i]] = feature_flag except RedisAdapterException: - _LOGGER.error('Error fetching splits from storage') + _LOGGER.error('Error fetching feature flags from storage') _LOGGER.debug('Error: ', exc_info=True) return to_return def is_valid_traffic_type(self, traffic_type_name): # pylint: disable=method-hidden """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -124,68 +167,51 @@ def is_valid_traffic_type(self, traffic_type_name): # pylint: disable=method-hi _LOGGER.debug("Fetching TrafficType [%s] count in redis: %s" % (traffic_type_name, count)) return count > 0 except RedisAdapterException: - _LOGGER.error('Error fetching split from storage') + _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return False - def put(self, split): + def update(self, to_add, to_delete, new_change_number): """ - Store a split. + Update feature flag storage. - :param split: Split object to store - :type split_name: splitio.models.splits.Split - """ - raise NotImplementedError('Only redis-consumer mode is supported.') - - def remove(self, split_name): - """ - Remove a split from storage. - - :param split_name: Name of the feature to remove. - :type split_name: str - - :return: True if the split was found and removed. False otherwise. - :rtype: bool + :param to_add: List of feature flags to add + :type to_add: list[splitio.models.splits.Split] + :param to_delete: List of feature flags to delete + :type to_delete: list[splitio.models.splits.Split] + :param new_change_number: New change number. + :type new_change_number: int """ raise NotImplementedError('Only redis-consumer mode is supported.') def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ try: - stored_value = self._redis.get(self._SPLIT_TILL_KEY) - _LOGGER.debug("Fetching Split Change Number from redis: %s" % stored_value) + stored_value = self._redis.get(self._FEATURE_FLAG_TILL_KEY) + _LOGGER.debug("Fetching feature flag Change Number from redis: %s" % stored_value) return json.loads(stored_value) if stored_value is not None else None except RedisAdapterException: - _LOGGER.error('Error fetching split change number from storage') + _LOGGER.error('Error fetching feature flag change number from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def set_change_number(self, new_change_number): - """ - Set the latest change number. - - :param new_change_number: New change number. - :type new_change_number: int - """ - raise NotImplementedError('Only redis-consumer mode is supported.') - def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ try: keys = self._redis.keys(self._get_key('*')) - _LOGGER.debug("Fetchting Split names from redis: %s" % keys) + _LOGGER.debug("Fetchting feature flag names from redis: %s" % keys) return [key.replace(self._get_key(''), '') for key in keys] except RedisAdapterException: - _LOGGER.error('Error fetching split names from storage') + _LOGGER.error('Error fetching feature flag names from storage') _LOGGER.debug('Error: ', exc_info=True) return [] @@ -199,33 +225,33 @@ def get_splits_count(self): def get_all_splits(self): """ - Return all the splits in cache. - :return: List of all splits in cache. + Return all the feature flags in cache. + :return: List of all feature flags in cache. :rtype: list(splitio.models.splits.Split) """ keys = self._redis.keys(self._get_key('*')) to_return = [] try: - _LOGGER.debug("Fetchting all Splits from redis: %s" % keys) - raw_splits = self._redis.mget(keys) - _LOGGER.debug(raw_splits) - for raw in raw_splits: + _LOGGER.debug("Fetchting all feature flags from redis: %s" % keys) + raw_feature_flags = self._redis.mget(keys) + _LOGGER.debug(raw_feature_flags) + for raw in raw_feature_flags: try: to_return.append(splits.from_raw(json.loads(raw))) except (ValueError, TypeError): - _LOGGER.error('Could not parse split. Skipping') - _LOGGER.debug("Raw split that failed parsing attempt: %s", raw) + _LOGGER.error('Could not parse feature flag. Skipping') + _LOGGER.debug("Raw feature flag that failed parsing attempt: %s", raw) except RedisAdapterException: _LOGGER.error('Error fetching all splits from storage') _LOGGER.debug('Error: ', exc_info=True) return to_return - def kill_locally(self, split_name, default_treatment, change_number): + def kill_locally(self, feature_flag_name, default_treatment, change_number): """ - Local kill for split + Local kill for feature flag - :param split_name: name of the split to perform kill - :type split_name: str + :param feature_flag_name: name of the feature flag to perform kill + :type feature_flag_name: str :param default_treatment: name of the default treatment to return :type default_treatment: str :param change_number: change_number @@ -636,14 +662,14 @@ def add_config_tag(self, tag): if len(self._config_tags) < MAX_TAGS: self._config_tags.append(tag) - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets, invalid_flag_sets): """ initilize telemetry objects :param congif: factory configuration parameters :type config: splitio.client.config """ - self._tel_config.record_config(config, extra_config) + self._tel_config.record_config(config, extra_config, total_flag_sets, invalid_flag_sets) def pop_config_tags(self): """Get and reset tags.""" diff --git a/splitio/sync/split.py b/splitio/sync/split.py index a39f42d1..91143e53 100644 --- a/splitio/sync/split.py +++ b/splitio/sync/split.py @@ -10,9 +10,11 @@ from splitio.api import APIException from splitio.api.commons import FetchOptions +from splitio.client.input_validator import validate_flag_sets from splitio.models import splits from splitio.util.backoff import Backoff from splitio.util.time import get_current_epoch_time_ms +from splitio.util.storage_helper import update_feature_flag_storage from splitio.sync import util _LEGACY_COMMENT_LINE_RE = re.compile(r'^#.*$') @@ -79,15 +81,9 @@ def _fetch_until(self, fetch_options, till=None): _LOGGER.error('Exception raised while fetching feature flags') _LOGGER.debug('Exception information: ', exc_info=True) raise exc - - for feature_flag in feature_flag_changes.get('splits', []): - if feature_flag['status'] == splits.Status.ACTIVE.value: - parsed = splits.from_raw(feature_flag) - self._feature_flag_storage.put(parsed) - segment_list.update(set(parsed.get_segment_names())) - else: - self._feature_flag_storage.remove(feature_flag['name']) - self._feature_flag_storage.set_change_number(feature_flag_changes['till']) + fetched_feature_flags = [] + [fetched_feature_flags.append(splits.from_raw(feature_flag)) for feature_flag in feature_flag_changes.get('splits', [])] + segment_list = update_feature_flag_storage(self._feature_flag_storage, fetched_feature_flags, feature_flag_changes['till']) if feature_flag_changes['till'] == feature_flag_changes['since']: return feature_flag_changes['till'], segment_list @@ -118,6 +114,17 @@ def _attempt_feature_flag_sync(self, fetch_options, till=None): how_long = self._backoff.get() time.sleep(how_long) + def _get_config_sets(self): + """ + Get all filter flag sets cnverrted to string, if no filter flagsets exist return None + + :return: string with flagsets + :rtype: str + """ + if self._feature_flag_storage.flag_set_filter.flag_sets == set({}): + return None + return ','.join(self._feature_flag_storage.flag_set_filter.sorted_flag_sets) + def synchronize_splits(self, till=None): """ Hit endpoint, update storage and return True if sync is complete. @@ -126,7 +133,7 @@ def synchronize_splits(self, till=None): :type till: int """ final_segment_list = set() - fetch_options = FetchOptions(True) # Set Cache-Control to no-cache + fetch_options = FetchOptions(True, sets=self._get_config_sets()) # Set Cache-Control to no-cache successful_sync, remaining_attempts, change_number, segment_list = self._attempt_feature_flag_sync(fetch_options, till) final_segment_list.update(segment_list) @@ -134,7 +141,7 @@ def synchronize_splits(self, till=None): if successful_sync: # succedeed sync _LOGGER.debug('Refresh completed in %d attempts.', attempts) return final_segment_list - with_cdn_bypass = FetchOptions(True, change_number) # Set flag for bypassing CDN + with_cdn_bypass = FetchOptions(True, change_number, sets=self._get_config_sets()) # Set flag for bypassing CDN without_cdn_successful_sync, remaining_attempts, change_number, segment_list = self._attempt_feature_flag_sync(with_cdn_bypass, till) final_segment_list.update(segment_list) without_cdn_attempts = _ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES - remaining_attempts @@ -347,11 +354,10 @@ def _synchronize_legacy(self): fetched = self._read_feature_flags_from_legacy_file(self._filename) to_delete = [name for name in self._feature_flag_storage.get_split_names() if name not in fetched.keys()] - for feature_flag in fetched.values(): - self._feature_flag_storage.put(feature_flag) + to_add = [] + [to_add.append(feature_flag) for feature_flag in fetched.values()] - for feature_flag in to_delete: - self._feature_flag_storage.remove(feature_flag) + self._feature_flag_storage.update(to_add, to_delete, 0) return [] @@ -371,18 +377,12 @@ def _synchronize_json(self): self._current_json_sha = fecthed_sha if self._feature_flag_storage.get_change_number() > till and till != self._DEFAULT_FEATURE_FLAG_TILL: return [] - for feature_flag in fetched: - if feature_flag['status'] == splits.Status.ACTIVE.value: - parsed = splits.from_raw(feature_flag) - self._feature_flag_storage.put(parsed) - _LOGGER.debug("feature flag %s is updated", parsed.name) - segment_list.update(set(parsed.get_segment_names())) - else: - self._feature_flag_storage.remove(feature_flag['name']) - - self._feature_flag_storage.set_change_number(till) + fetched_feature_flags = [] + [fetched_feature_flags.append(splits.from_raw(feature_flag)) for feature_flag in fetched] + segment_list = update_feature_flag_storage(self._feature_flag_storage, fetched_feature_flags, till) return segment_list except Exception as exc: + _LOGGER.debug(exc) raise ValueError("Error reading feature flags from json.") from exc def _read_feature_flags_from_json_file(self, filename): @@ -434,7 +434,7 @@ def _sanitize_json_elements(self, parsed): if 'till' not in parsed or parsed['till'] is None or parsed['till'] < -1: parsed['till'] = -1 if 'since' not in parsed or parsed['since'] is None or parsed['since'] < -1 or parsed['since'] > parsed['till']: - parsed['since'] = parsed['till'] + parsed['since'] = parsed['till'] return parsed @@ -464,6 +464,11 @@ def _sanitize_feature_flag_elements(self, parsed_feature_flags): ('algo', 2, 2, 2, None, None)]: feature_flag = util._sanitize_object_element(feature_flag, 'split', element[0], element[1], lower_value=element[2], upper_value=element[3], in_list=element[4], not_in_list=element[5]) feature_flag = self._sanitize_condition(feature_flag) + + if 'sets' not in feature_flag: + feature_flag['sets'] = [] + feature_flag['sets'] = validate_flag_sets(feature_flag['sets'], 'Localhost Validator') + sanitized_feature_flags.append(feature_flag) return sanitized_feature_flags diff --git a/splitio/sync/synchronizer.py b/splitio/sync/synchronizer.py index 3b5a4251..59c57f01 100644 --- a/splitio/sync/synchronizer.py +++ b/splitio/sync/synchronizer.py @@ -251,6 +251,7 @@ def __init__(self, split_synchronizers, split_tasks): self._periodic_data_recording_tasks.append(self._split_tasks.unique_keys_task) if self._split_tasks.clear_filter_task: self._periodic_data_recording_tasks.append(self._split_tasks.clear_filter_task) + self._break_sync_all = False @property def split_sync(self): @@ -289,6 +290,7 @@ def synchronize_splits(self, till, sync_segments=True): :returns: whether the synchronization was successful or not. :rtype: bool """ + self._break_sync_all = False _LOGGER.debug('Starting feature flags synchronization') try: new_segments = [] @@ -304,7 +306,9 @@ def synchronize_splits(self, till, sync_segments=True): else: _LOGGER.debug('Segment sync scheduled.') return True - except APIException: + except APIException as exc: + if exc._status_code is not None and exc._status_code == 414: + self._break_sync_all = True _LOGGER.error('Failed syncing feature flags') _LOGGER.debug('Error: ', exc_info=True) return False @@ -334,7 +338,7 @@ def sync_all(self, max_retry_attempts=_SYNC_ALL_NO_RETRIES): _LOGGER.debug('Error: ', exc_info=True) if max_retry_attempts != _SYNC_ALL_NO_RETRIES: retry_attempts += 1 - if retry_attempts > max_retry_attempts: + if retry_attempts > max_retry_attempts or self._break_sync_all: break how_long = self._backoff.get() time.sleep(how_long) diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py new file mode 100644 index 00000000..d281c438 --- /dev/null +++ b/splitio/util/storage_helper.py @@ -0,0 +1,71 @@ +"""Storage Helper.""" +import logging + +from splitio.models import splits + +_LOGGER = logging.getLogger(__name__) + +def update_feature_flag_storage(feature_flag_storage, feature_flags, change_number): + """ + Update feature flag storage from given list of feature flags while checking the flag set logic + + :param feature_flag_storage: Feature flag storage instance + :type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage + :param feature_flag: Feature flag instance to validate. + :type feature_flag: splitio.models.splits.Split + :param: last change number + :type: int + + :return: segments list from feature flags list + :rtype: list(str) + """ + segment_list = set() + to_add = [] + to_delete = [] + for feature_flag in feature_flags: + if feature_flag_storage.flag_set_filter.intersect(feature_flag.sets) and feature_flag.status == splits.Status.ACTIVE: + to_add.append(feature_flag) + segment_list.update(set(feature_flag.get_segment_names())) + else: + if feature_flag_storage.get(feature_flag.name) is not None: + to_delete.append(feature_flag.name) + + feature_flag_storage.update(to_add, to_delete, change_number) + return segment_list + +def get_valid_flag_sets(flag_sets, flag_set_filter): + """ + Check each flag set in given array, return it if exist in a given config flag set array, if config array is empty return all + + :param flag_sets: Flag sets array + :type flag_sets: list(str) + :param config_flag_sets: Config flag sets array + :type config_flag_sets: list(str) + + :return: array of flag sets + :rtype: list(str) + """ + sets_to_fetch = [] + for flag_set in flag_sets: + if not flag_set_filter.set_exist(flag_set) and flag_set_filter.should_filter: + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) + continue + sets_to_fetch.append(flag_set) + + return sets_to_fetch + +def combine_valid_flag_sets(result_sets): + """ + Check each flag set in given array of sets, combine all flag sets in one unique set + + :param result_sets: Flag sets set + :type flag_sets: list(set) + + :return: flag sets set + :rtype: set + """ + to_return = set() + for result_set in result_sets: + if isinstance(result_set, set) and len(result_set) > 0: + to_return.update(result_set) + return to_return \ No newline at end of file diff --git a/splitio/version.py b/splitio/version.py index 8b98c7d1..17781f45 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '9.5.1' +__version__ = '9.6.0' diff --git a/tests/api/test_splits_api.py b/tests/api/test_splits_api.py index 3c37b199..e8d1784e 100644 --- a/tests/api/test_splits_api.py +++ b/tests/api/test_splits_api.py @@ -19,7 +19,7 @@ def test_fetch_split_changes(self, mocker): httpclient.get.return_value = client.HttpResponse(200, '{"prop1": "value1"}') split_api = splits.SplitsAPI(httpclient, 'some_api_key', SdkMetadata('1.0', 'some', '1.2.3.4'), mocker.Mock()) - response = split_api.fetch_splits(123, FetchOptions()) + response = split_api.fetch_splits(123, FetchOptions(False, None, 'set1,set2')) assert response['prop1'] == 'value1' assert httpclient.get.mock_calls == [mocker.call('sdk', '/splitChanges', 'some_api_key', extra_headers={ @@ -27,7 +27,7 @@ def test_fetch_split_changes(self, mocker): 'SplitSDKMachineIP': '1.2.3.4', 'SplitSDKMachineName': 'some' }, - query={'since': 123})] + query={'since': 123, 'sets': 'set1,set2'})] httpclient.reset_mock() response = split_api.fetch_splits(123, FetchOptions(True)) @@ -42,7 +42,7 @@ def test_fetch_split_changes(self, mocker): query={'since': 123})] httpclient.reset_mock() - response = split_api.fetch_splits(123, FetchOptions(True, 123)) + response = split_api.fetch_splits(123, FetchOptions(True, 123, 'set3')) assert response['prop1'] == 'value1' assert httpclient.get.mock_calls == [mocker.call('sdk', '/splitChanges', 'some_api_key', extra_headers={ @@ -51,7 +51,7 @@ def test_fetch_split_changes(self, mocker): 'SplitSDKMachineName': 'some', 'Cache-Control': 'no-cache' }, - query={'since': 123, 'till': 123})] + query={'since': 123, 'till': 123, 'sets': 'set3'})] httpclient.reset_mock() def raise_exception(*args, **kwargs): diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 207b302a..6341142c 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -41,7 +41,6 @@ def test_get_treatment(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) factory = SplitFactory(mocker.Mock(), {'splits': split_storage, @@ -84,6 +83,7 @@ def test_get_treatment(self, mocker): assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -98,7 +98,7 @@ def _raise(*_): ) in impmanager.process_impressions.mock_calls def test_get_treatment_with_config(self, mocker): - """Test get_treatment execution paths.""" + """Test get_treatment with config execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) @@ -110,7 +110,6 @@ def test_get_treatment_with_config(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) factory = SplitFactory(mocker.Mock(), {'splits': split_storage, @@ -161,6 +160,7 @@ def test_get_treatment_with_config(self, mocker): [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -175,7 +175,7 @@ def _raise(*_): ) in impmanager.process_impressions.mock_calls def test_get_treatments(self, mocker): - """Test get_treatment execution paths.""" + """Test get_treatments execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) @@ -238,6 +238,7 @@ def test_get_treatments(self, mocker): assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -249,7 +250,7 @@ def _raise(*_): assert client.get_treatments('key', ['f1', 'f2']) == {'f1': 'control', 'f2': 'control'} def test_get_treatments_with_config(self, mocker): - """Test get_treatment execution paths.""" + """Test get_treatments with config execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) @@ -260,7 +261,6 @@ def test_get_treatments_with_config(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) factory = SplitFactory(mocker.Mock(), {'splits': split_storage, @@ -313,6 +313,7 @@ def test_get_treatments_with_config(self, mocker): assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -326,6 +327,376 @@ def _raise(*_): 'f2': ('control', None) } + def test_get_treatments_by_flag_set(self, mocker): + """Test get_treatments by flagset execution paths.""" + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + impmanager = mocker.Mock(spec=ImpressionManager) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + def get_feature_flags_by_sets(flag_sets): + if flag_sets == ['set1']: + return ['f1', 'f2'] + if flag_sets == ['set2']: + return ['f3', 'f4'] + if flag_sets == ['set3']: + return ['some_feature'] + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets + + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': '{"color": "red"}', + 'impression': { + 'label': 'some_label', + 'change_number': 123 + } + } + def evaluate_features(feature_flag_names, matching_key, bucketing_key, attributes=None): + return {feature_flag_name: evaluation for feature_flag_name in feature_flag_names} + client._evaluator.evaluate_features = evaluate_features + + _logger = mocker.Mock() + client._send_impression_to_listener = mocker.Mock() + assert client.get_treatments_by_flag_set('key', 'set1') == {'f1': 'on', 'f2': 'on'} + + impressions_called = impmanager.process_impressions.mock_calls[0][1][0] + assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + assert client.get_treatments_by_flag_set('key', 'set2') == {'f3': 'on', 'f4': 'on'} + impressions_called = impmanager.process_impressions.mock_calls[1][1][0] + assert (Impression('key', 'f3', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f4', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + # Test with client not ready + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property + impmanager.process_impressions.reset_mock() + assert client.get_treatments_by_flag_set('some_key', 'set3', {'some_attribute': 1}) == {'some_feature': 'control'} + assert mocker.call( + [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] + ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) + + # Test with exception: + ready_property.return_value = True + split_storage.get_change_number.return_value = -1 + + def _raise(*_): + raise Exception('something') + client._evaluator.evaluate_features = _raise + assert client.get_treatments_by_flag_set('key', 'set1') == {'f1': 'control', 'f2': 'control'} + + def test_get_treatments_by_flag_sets(self, mocker): + """Test get_treatments by flagsets execution paths.""" + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + impmanager = mocker.Mock(spec=ImpressionManager) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + def get_feature_flags_by_sets(flag_sets): + if sorted(flag_sets) == ['set1', 'set2']: + return ['f1', 'f2'] + if sorted(flag_sets) == ['set3', 'set4']: + return ['f3', 'f4'] + if flag_sets == ['set5']: + return ['some_feature'] + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets + + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': '{"color": "red"}', + 'impression': { + 'label': 'some_label', + 'change_number': 123 + } + } + def evaluate_features(feature_flag_names, matching_key, bucketing_key, attributes=None): + return {feature_flag_name: evaluation for feature_flag_name in feature_flag_names} + + client._evaluator.evaluate_features = evaluate_features + _logger = mocker.Mock() + client._send_impression_to_listener = mocker.Mock() + assert client.get_treatments_by_flag_sets('key', ['set1', 'set2']) == {'f1': 'on', 'f2': 'on'} + + impressions_called = impmanager.process_impressions.mock_calls[0][1][0] + assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + assert client.get_treatments_by_flag_sets('key', ['set3', 'set4']) == {'f3': 'on', 'f4': 'on'} + impressions_called = impmanager.process_impressions.mock_calls[1][1][0] + assert (Impression('key', 'f3', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f4', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + # Test with client not ready + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property + impmanager.process_impressions.reset_mock() + assert client.get_treatments_by_flag_sets('some_key', ['set5'], {'some_attribute': 1}) == {'some_feature': 'control'} + assert mocker.call( + [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] + ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) + + # Test with exception: + ready_property.return_value = True + split_storage.get_change_number.return_value = -1 + + def _raise(*_): + raise Exception('something') + client._evaluator.evaluate_features = _raise + assert client.get_treatments_by_flag_sets('key', ['set1', 'set2']) == {'f1': 'control', 'f2': 'control'} + + def test_get_treatments_with_config_by_flag_set(self, mocker): + """Test get_treatments with config by flagset execution paths.""" + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + impmanager = mocker.Mock(spec=ImpressionManager) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + def get_feature_flags_by_sets(flag_sets): + if flag_sets == ['set1']: + return ['f1', 'f2'] + if flag_sets == ['set2']: + return ['f3', 'f4'] + if flag_sets == ['set3']: + return ['some_feature'] + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets + + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': '{"color": "red"}', + 'impression': { + 'label': 'some_label', + 'change_number': 123 + } + } + def evaluate_features(feature_flag_names, matching_key, bucketing_key, attributes=None): + return {feature_flag_name: evaluation for feature_flag_name in feature_flag_names} + client._evaluator.evaluate_features = evaluate_features + + _logger = mocker.Mock() + assert client.get_treatments_with_config_by_flag_set('key', 'set1') == { + 'f1': ('on', '{"color": "red"}'), + 'f2': ('on', '{"color": "red"}') + } + + impressions_called = impmanager.process_impressions.mock_calls[0][1][0] + assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + _logger = mocker.Mock() + assert client.get_treatments_with_config_by_flag_set('key', 'set2') == { + 'f3': ('on', '{"color": "red"}'), + 'f4': ('on', '{"color": "red"}') + } + + impressions_called = impmanager.process_impressions.mock_calls[1][1][0] + assert (Impression('key', 'f3', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f4', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + # Test with client not ready + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property + impmanager.process_impressions.reset_mock() + assert client.get_treatments_with_config_by_flag_set('some_key', 'set3', {'some_attribute': 1}) == {'some_feature': ('control', None)} + assert mocker.call( + [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] + ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) + + # Test with exception: + ready_property.return_value = True + split_storage.get_change_number.return_value = -1 + + def _raise(*_): + raise Exception('something') + client._evaluator.evaluate_features = _raise + assert client.get_treatments_with_config_by_flag_set('key', 'set1') == { + 'f1': ('control', None), + 'f2': ('control', None) + } + + def test_get_treatments_with_config_by_flag_sets(self, mocker): + """Test get_treatments with config by flagsets execution paths.""" + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + impmanager = mocker.Mock(spec=ImpressionManager) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + def get_feature_flags_by_sets(flag_sets): + if sorted(flag_sets) == ['set1', 'set2']: + return ['f1', 'f2'] + if sorted(flag_sets) == ['set3', 'set4']: + return ['f3', 'f4'] + if flag_sets == ['set5']: + return ['some_feature'] + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets + + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': '{"color": "red"}', + 'impression': { + 'label': 'some_label', + 'change_number': 123 + } + } + def evaluate_features(feature_flag_names, matching_key, bucketing_key, attributes=None): + return {feature_flag_name: evaluation for feature_flag_name in feature_flag_names} + client._evaluator.evaluate_features = evaluate_features + + _logger = mocker.Mock() + assert client.get_treatments_with_config_by_flag_sets('key', ['set1', 'set2']) == { + 'f1': ('on', '{"color": "red"}'), + 'f2': ('on', '{"color": "red"}') + } + + impressions_called = impmanager.process_impressions.mock_calls[0][1][0] + assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + _logger = mocker.Mock() + assert client.get_treatments_with_config_by_flag_sets('key', ['set3', 'set4']) == { + 'f3': ('on', '{"color": "red"}'), + 'f4': ('on', '{"color": "red"}') + } + + impressions_called = impmanager.process_impressions.mock_calls[1][1][0] + assert (Impression('key', 'f3', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f4', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + # Test with client not ready + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property + impmanager.process_impressions.reset_mock() + assert client.get_treatments_with_config_by_flag_sets('some_key', ['set5'], {'some_attribute': 1}) == {'some_feature': ('control', None)} + assert mocker.call( + [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] + ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) + + # Test with exception: + ready_property.return_value = True + split_storage.get_change_number.return_value = -1 + + def _raise(*_): + raise Exception('something') + client._evaluator.evaluate_features = _raise + assert client.get_treatments_with_config_by_flag_sets('key', ['set1', 'set2']) == { + 'f1': ('control', None), + 'f2': ('control', None) + } + @mock.patch('splitio.client.factory.SplitFactory.destroy') def test_destroy(self, mocker): """Test that destroy/destroyed calls are forwarded to the factory.""" @@ -384,6 +755,7 @@ def test_track(self, mocker): telemetry_producer.get_telemetry_init_producer(), mocker.Mock() ) + _logger = mocker.Mock() destroyed_mock = mocker.PropertyMock() destroyed_mock.return_value = False @@ -398,6 +770,7 @@ def test_track(self, mocker): size=1024 ) ]) in event_storage.put.mock_calls + assert _logger.call("track: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method") def test_evaluations_before_running_post_fork(self, mocker): destroyed_property = mocker.PropertyMock() @@ -473,7 +846,7 @@ def test_telemetry_not_ready(self, mocker): ) client = Client(factory, mocker.Mock()) client.ready = False - client._evaluate_if_ready('matching_key','matching_key', 'feature') + client._evaluate_if_ready('matching_key','matching_key', 'method', 'feature') assert(telemetry_storage._tel_config._not_ready == 1) client.track('key', 'tt', 'ev') assert(telemetry_storage._tel_config._not_ready == 2) @@ -481,7 +854,7 @@ def test_telemetry_not_ready(self, mocker): @mock.patch('splitio.client.client.Client._evaluate_if_ready', side_effect=Exception()) def test_telemetry_record_treatment_exception(self, mocker): split_storage = InMemorySplitStorage() - split_storage.put(Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)) + split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], 123) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) @@ -525,7 +898,7 @@ def test_telemetry_record_treatment_exception(self, mocker): @mock.patch('splitio.client.client.Client._evaluate_features_if_ready', side_effect=Exception()) def test_telemetry_record_treatments_exception(self, mocker): split_storage = InMemorySplitStorage() - split_storage.put(Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)) + split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], 123) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) @@ -568,7 +941,7 @@ def test_telemetry_record_treatments_exception(self, mocker): def test_telemetry_method_latency(self, mocker): split_storage = InMemorySplitStorage() - split_storage.put(Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)) + split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], 123) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 0d96b478..b4b9d9e9 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -1,5 +1,6 @@ """Configuration unit tests.""" # pylint: disable=protected-access,no-self-use,line-too-long +import pytest from splitio.client import config from splitio.engine.impressions.impressions import ImpressionsMode @@ -64,7 +65,12 @@ def test_sanitize_imp_mode(self): def test_sanitize(self): """Test sanitization.""" - configs = {} - processed = config.sanitize('some', configs) - + processed = config.sanitize('some', {}) assert processed['redisLocalCacheEnabled'] # check default is True + assert processed['flagSetsFilter'] is None + + processed = config.sanitize('some', {'redisHost': 'x', 'flagSetsFilter': ['set']}) + assert processed['flagSetsFilter'] is None + + processed = config.sanitize('some', {'storageType': 'pluggable', 'flagSetsFilter': ['set']}) + assert processed['flagSetsFilter'] is None diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index cc778f1b..644fe6fd 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -29,6 +29,26 @@ class SplitFactoryTests(object): """Split factory test cases.""" + def test_flag_sets_counts(self): + factory = get_factory("none", config={ + 'flagSetsFilter': ['set1', 'set2', 'set3'] + }) + + 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 + + factory = get_factory("none", config={ + 'flagSetsFilter': ['s#et1', 'set2', 'set3'] + }) + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 1 + + factory = get_factory("none", config={ + 'flagSetsFilter': ['s#et1', 22, 'set3'] + }) + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 2 + def test_inmemory_client_creation_streaming_false(self, mocker): """Test that a client with in-memory storage is created correctly.""" diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index bceb39b0..4bb1e417 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -55,7 +55,7 @@ def test_get_treatment(self, mocker): assert client.get_treatment(None, 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatment') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -139,7 +139,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment(Key(None, 'bucketing_key'), 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'matching_key', 'matching_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -188,7 +188,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment(Key('matching_key', None), 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'bucketing_key', 'bucketing_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -232,7 +232,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment('matching_key', ' some_feature ', None) == 'default_treatment' assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatment', ' some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatment', 'feature flag name', ' some_feature ') ] _logger.reset_mock() @@ -289,7 +289,7 @@ def _configs(treatment): assert client.get_treatment_with_config(None, 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatment_with_config') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -373,7 +373,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config(Key(None, 'bucketing_key'), 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'matching_key', 'matching_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -422,7 +422,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config(Key('matching_key', None), 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'bucketing_key', 'bucketing_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -466,7 +466,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config('matching_key', ' some_feature ', None) == ('default_treatment', '{"some": "property"}') assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatment_with_config', ' some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatment_with_config', 'feature flag name', ' some_feature ') ] _logger.reset_mock() @@ -629,7 +629,7 @@ def test_track(self, mocker): _logger.reset_mock() assert client.track("some_key", "TRAFFIC_type", "event_type", 1) is True assert _logger.warning.mock_calls == [ - mocker.call("track: %s should be all lowercase - converting string to lowercase.", 'TRAFFIC_type') + mocker.call("track: traffic type 'TRAFFIC_type' should be all lowercase - converting string to lowercase") ] assert client.track("some_key", "traffic_type", None, 1) is False @@ -666,10 +666,10 @@ def test_track(self, mocker): assert _logger.error.mock_calls == [ mocker.call("%s: you passed %s, event_type must adhere to the regular " "expression %s. This means " - "an event name must be alphanumeric, cannot be more than 80 " + "%s must be alphanumeric, cannot be more than %s " "characters long, and can only include a dash, underscore, " "period, or colon as separators of alphanumeric characters.", - 'track', '@@', '^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$') + 'track', '@@', '^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$', 'an event name', 80) ] _logger.reset_mock() @@ -837,7 +837,7 @@ def test_get_treatments(self, mocker): assert client.get_treatments(None, ['some_feature']) == {'some_feature': CONTROL} assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatments') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatments', 'key', 'key') ] _logger.reset_mock() @@ -912,7 +912,7 @@ def test_get_treatments(self, mocker): _logger.reset_mock() assert client.get_treatments('some_key', ['some ']) == {'some': 'default_treatment'} assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatments', 'some ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatments', 'feature flag name', 'some ') ] _logger.reset_mock() @@ -978,7 +978,7 @@ def _configs(treatment): assert client.get_treatments_with_config(None, ['some_feature']) == {'some_feature': (CONTROL, None)} assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatments_with_config') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatments_with_config', 'key', 'key') ] _logger.reset_mock() @@ -1053,7 +1053,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatments_with_config('some_key', ['some_feature ']) == {'some_feature': ('default_treatment', '{"some": "property"}')} assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatments_with_config', 'some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatments_with_config', 'feature flag name', 'some_feature ') ] _logger.reset_mock() @@ -1074,6 +1074,33 @@ def _configs(treatment): ) ] + def test_flag_sets_validation(self): + """Test sanitization for flag sets.""" + flag_sets = input_validator.validate_flag_sets([' set1', 'set2 ', 'set3'], 'method') + assert sorted(flag_sets) == ['set1', 'set2', 'set3'] + + flag_sets = input_validator.validate_flag_sets(['1set', '_set2'], 'method') + assert flag_sets == ['1set'] + + flag_sets = input_validator.validate_flag_sets(['Set1', 'SET2'], 'method') + assert sorted(flag_sets) == ['set1', 'set2'] + + flag_sets = input_validator.validate_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5'], 'method') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets(['set4', 'set1', 'set3', 'set1'], 'method') + assert sorted(flag_sets) == ['set1', 'set3', 'set4'] + + flag_sets = input_validator.validate_flag_sets(['w' * 50, 's' * 51], 'method') + assert flag_sets == ['w' * 50] + + flag_sets = input_validator.validate_flag_sets('set1', 'method') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets([12, 33], 'method') + assert flag_sets == [] + + class ManagerInputValidationTests(object): #pylint: disable=too-few-public-methods """Manager input validation test cases.""" @@ -1265,3 +1292,30 @@ def test_validate_pluggable_adapter(self): # using non-string type prefix should not pass assert(not input_validator.validate_pluggable_adapter({'storageType': 'pluggable', 'storagePrefix': 'myprefix', 123: self.mock_adapter4()})) + + def test_sanitize_flag_sets(self): + """Test sanitization for flag sets.""" + flag_sets = input_validator.validate_flag_sets([' set1', 'set2 ', 'set3'], 'm') + assert sorted(flag_sets) == ['set1', 'set2', 'set3'] + + flag_sets = input_validator.validate_flag_sets(['1set', '_set2'], 'm') + assert flag_sets == ['1set'] + + flag_sets = input_validator.validate_flag_sets(['Set1', 'SET2'], 'm') + assert sorted(flag_sets) == ['set1', 'set2'] + + flag_sets = input_validator.validate_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5'], 'm') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets(['set4', 'set1', 'set3', 'set1'], 'm') + assert sorted(flag_sets) == ['set1', 'set3', 'set4'] + + flag_sets = input_validator.validate_flag_sets(['w' * 50, 's' * 51], 'm') + assert flag_sets == ['w' * 50] + + flag_sets = input_validator.validate_flag_sets('set1', 'm') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets([12, 33], 'm') + + assert flag_sets == [] diff --git a/tests/client/test_manager.py b/tests/client/test_manager.py index 30916177..b461d2bb 100644 --- a/tests/client/test_manager.py +++ b/tests/client/test_manager.py @@ -1,13 +1,15 @@ """SDK main manager test module.""" +import pytest from splitio.client.factory import SplitFactory from splitio.client.manager import SplitManager, _LOGGER as _logger from splitio.storage import SplitStorage, EventStorage, ImpressionStorage, SegmentStorage -from splitio.storage.inmemmory import InMemoryTelemetryStorage +from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemorySplitStorage +from splitio.models import splits from splitio.engine.impressions.impressions import Manager as ImpressionManager from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageConsumer from splitio.recorder.recorder import StandardRecorder - +from tests.models.test_splits import SplitTests class ManagerTests(object): # pylint: disable=too-few-public-methods """Split manager test cases.""" @@ -19,7 +21,6 @@ def test_evaluations_before_running_post_fork(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) recorder = StandardRecorder(impmanager, mocker.Mock(), mocker.Mock(), telemetry_producer.get_telemetry_evaluation_producer()) factory = SplitFactory(mocker.Mock(), {'splits': mocker.Mock(), @@ -55,3 +56,44 @@ def test_evaluations_before_running_post_fork(self, mocker): assert manager.splits() == [] assert _logger.error.mock_calls == expected_msg _logger.reset_mock() + + def test_manager_calls(self, mocker): + split_storage = InMemorySplitStorage() + split = splits.from_raw(SplitTests.raw) + split_storage.update([split], [], 123) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': mocker.Mock(), + 'impressions': mocker.Mock(), + 'events': mocker.Mock()}, + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + False + ) + manager = SplitManager(factory) + splits_view = manager.splits() + self._verify_split(splits_view[0]) + assert manager.split_names() == ['some_name'] + split_view = manager.split('some_name') + self._verify_split(split_view) + split2 = SplitTests.raw.copy() + split2['sets'] = None + split2['name'] = 'no_sets_split' + split_storage.update([splits.from_raw(split2)], [], 123) + + split_view = manager.split('no_sets_split') + assert split_view.sets == [] + + def _verify_split(self, split): + assert split.name == 'some_name' + assert split.traffic_type == 'user' + assert split.killed == False + assert sorted(split.treatments) == ['off', 'on'] + assert split.change_number == 123 + assert split.configs == {'on': '{"color": "blue", "size": 13}'} + assert sorted(split.sets) == ['set1', 'set2'] diff --git a/tests/engine/test_telemetry.py b/tests/engine/test_telemetry.py index 78466e87..45b05551 100644 --- a/tests/engine/test_telemetry.py +++ b/tests/engine/test_telemetry.py @@ -20,15 +20,42 @@ def test_instances(self): assert(telemetry_producer._telemetry_runtime_producer == telemetry_producer.get_telemetry_runtime_producer()) def test_record_config(self, mocker): - telemetry_storage = mocker.Mock() + telemetry_storage = InMemoryTelemetryStorage() telemetry_init_producer = TelemetryInitProducer(telemetry_storage) - - def record_config(*args, **kwargs): - self.passed_config = args[0] - - telemetry_storage.record_config.side_effect = record_config - telemetry_init_producer.record_config({'bT':0, 'nR':0, 'uC': 0}, {}) - assert(self.passed_config == {'bT':0, 'nR':0, 'uC': 0}) + config = {'operationMode': 'standalone', + 'streamingEnabled': True, + 'impressionsQueueSize': 100, + 'eventsQueueSize': 200, + 'impressionsMode': 'DEBUG', + 'impressionListener': None, + 'featuresRefreshRate': 30, + 'segmentsRefreshRate': 30, + 'impressionsRefreshRate': 60, + 'eventsPushRate': 60, + 'metricsRefreshRate': 10, + 'storageType': None + } + telemetry_init_producer.record_config(config, {}, 5, 2) + telemetry_init_producer.record_active_and_redundant_factories(1, 0) + + assert(telemetry_storage._tel_config.get_stats() == {'oM': 0, + 'sT': telemetry_storage._tel_config._get_storage_type(config['operationMode'], config['storageType']), + 'sE': config['streamingEnabled'], + 'rR': {'sp': 30, 'se': 30, 'im': 60, 'ev': 60, 'te': 10}, + 'uO': {'s': False, 'e': False, 'a': False, 'st': False, 't': False}, + 'iQ': config['impressionsQueueSize'], + 'eQ': config['eventsQueueSize'], + 'iM': telemetry_storage._tel_config._get_impressions_mode(config['impressionsMode']), + 'iL': True if config['impressionListener'] is not None else False, + 'hp': telemetry_storage._tel_config._check_if_proxy_detected(), + 'bT': 0, + 'tR': 0, + 'nR': 0, + 'aF': 1, + 'rF': 0, + 'fsT': 5, + 'fsI': 2} + ) def test_record_ready_time(self, mocker): telemetry_storage = mocker.Mock() diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 6475e24d..aae9e014 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1,6 +1,6 @@ -split11 = {"splits": [{"trafficTypeName": "user", "name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779, "seed": -113875324, "status": "ACTIVE","killed": False, "defaultTreatment": "off", "changeNumber": 1675443569027,"algo": 2, "configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}]},{"trafficTypeName": "user", "name": "SPLIT_1", "trafficAllocation": 100, "trafficAllocationSeed": -1780071202,"seed": -1442762199, "status": "ACTIVE","killed": False, "defaultTreatment": "off", "changeNumber": 1675443537882,"algo": 2, "configurations": {},"conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 0 },{ "treatment": "off", "size": 100 }],"label": "default rule"}]}],"since": -1,"till": 1675443569027} -split12 = {"splits": [{"trafficTypeName": "user","name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779,"seed": -113875324,"status": "ACTIVE","killed": True,"defaultTreatment": "off","changeNumber": 1675443767288,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}]}],"since": 1675443569027,"till": 167544376728} -split13 = {"splits": [{"trafficTypeName": "user","name": "SPLIT_1","trafficAllocation": 100,"trafficAllocationSeed": -1780071202,"seed": -1442762199,"status": "ARCHIVED","killed": False,"defaultTreatment": "off","changeNumber": 1675443984594,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 0 },{ "treatment": "off", "size": 100 }],"label": "default rule"}]},{"trafficTypeName": "user","name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779,"seed": -113875324,"status": "ACTIVE","killed": False,"defaultTreatment": "off","changeNumber": 1675443954220,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}]}],"since": 1675443767288,"till": 1675443984594} +split11 = {"splits": [{"trafficTypeName": "user", "name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779, "seed": -113875324, "status": "ACTIVE","killed": False, "defaultTreatment": "off", "changeNumber": 1675443569027,"algo": 2, "configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}], "sets": ["set2"]},{"trafficTypeName": "user", "name": "SPLIT_1", "trafficAllocation": 100, "trafficAllocationSeed": -1780071202,"seed": -1442762199, "status": "ACTIVE","killed": False, "defaultTreatment": "off", "changeNumber": 1675443537882,"algo": 2, "configurations": {},"conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 0 },{ "treatment": "off", "size": 100 }],"label": "default rule"}], "sets": ["set1"]}],"since": -1,"till": 1675443569027} +split12 = {"splits": [{"trafficTypeName": "user","name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779,"seed": -113875324,"status": "ACTIVE","killed": True,"defaultTreatment": "off","changeNumber": 1675443767288,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}], "sets": ["set3"]}],"since": 1675443569027,"till": 167544376728} +split13 = {"splits": [{"trafficTypeName": "user","name": "SPLIT_1","trafficAllocation": 100,"trafficAllocationSeed": -1780071202,"seed": -1442762199,"status": "ARCHIVED","killed": False,"defaultTreatment": "off","changeNumber": 1675443984594,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 0 },{ "treatment": "off", "size": 100 }],"label": "default rule"}]},{"trafficTypeName": "user","name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779,"seed": -113875324,"status": "ACTIVE","killed": False,"defaultTreatment": "off","changeNumber": 1675443954220,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}], "sets": ["set1", "set2"]}],"since": 1675443767288,"till": 1675443984594} split41 = split11 split42 = split12 diff --git a/tests/integration/files/splitChanges.json b/tests/integration/files/splitChanges.json index d5401c93..f77ce97e 100644 --- a/tests/integration/files/splitChanges.json +++ b/tests/integration/files/splitChanges.json @@ -58,7 +58,8 @@ } ] } - ] + ], + "sets": ["set1", "set2"] }, { "orgId": null, @@ -95,7 +96,8 @@ } ] } - ] + ], + "sets": ["set4"] }, { "orgId": null, @@ -136,7 +138,8 @@ } ] } - ] + ], + "sets": ["set3"] }, { "orgId": null, @@ -199,7 +202,8 @@ } ] } - ] + ], + "sets": ["set1"] }, { "orgId": null, diff --git a/tests/integration/files/split_changes.json b/tests/integration/files/split_changes.json index f536346d..2d21c0da 100644 --- a/tests/integration/files/split_changes.json +++ b/tests/integration/files/split_changes.json @@ -58,7 +58,8 @@ } ] } - ] + ], + "sets": ["set1", "set2"] }, { "orgId": null, @@ -95,7 +96,8 @@ } ] } - ] + ], + "sets": ["set4"] }, { "orgId": null, @@ -136,7 +138,8 @@ } ] } - ] + ], + "sets": ["set3"] }, { "orgId": null, @@ -199,7 +202,8 @@ } ] } - ] + ], + "sets": ["set1"] }, { "orgId": null, diff --git a/tests/integration/files/split_changes_temp.json b/tests/integration/files/split_changes_temp.json index 162c0b17..c8ad59e1 100644 --- a/tests/integration/files/split_changes_temp.json +++ b/tests/integration/files/split_changes_temp.json @@ -1 +1 @@ -{"splits": [{"trafficTypeName": "user", "name": "SPLIT_1", "trafficAllocation": 100, "trafficAllocationSeed": -1780071202, "seed": -1442762199, "status": "ARCHIVED", "killed": false, "defaultTreatment": "off", "changeNumber": 1675443984594, "algo": 2, "configurations": {}, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": null}, "matcherType": "ALL_KEYS", "negate": false, "userDefinedSegmentMatcherData": null, "whitelistMatcherData": null, "unaryNumericMatcherData": null, "betweenMatcherData": null, "booleanMatcherData": null, "dependencyMatcherData": null, "stringMatcherData": null}]}, "partitions": [{"treatment": "on", "size": 0}, {"treatment": "off", "size": 100}], "label": "default rule"}]}, {"trafficTypeName": "user", "name": "SPLIT_2", "trafficAllocation": 100, "trafficAllocationSeed": 1057590779, "seed": -113875324, "status": "ACTIVE", "killed": false, "defaultTreatment": "off", "changeNumber": 1675443954220, "algo": 2, "configurations": {}, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": null}, "matcherType": "ALL_KEYS", "negate": false, "userDefinedSegmentMatcherData": null, "whitelistMatcherData": null, "unaryNumericMatcherData": null, "betweenMatcherData": null, "booleanMatcherData": null, "dependencyMatcherData": null, "stringMatcherData": null}]}, "partitions": [{"treatment": "on", "size": 100}, {"treatment": "off", "size": 0}], "label": "default rule"}]}], "since": -1, "till": -1} \ No newline at end of file +{"splits": [{"trafficTypeName": "user", "name": "SPLIT_1", "trafficAllocation": 100, "trafficAllocationSeed": -1780071202, "seed": -1442762199, "status": "ARCHIVED", "killed": false, "defaultTreatment": "off", "changeNumber": 1675443984594, "algo": 2, "configurations": {}, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": null}, "matcherType": "ALL_KEYS", "negate": false, "userDefinedSegmentMatcherData": null, "whitelistMatcherData": null, "unaryNumericMatcherData": null, "betweenMatcherData": null, "booleanMatcherData": null, "dependencyMatcherData": null, "stringMatcherData": null}]}, "partitions": [{"treatment": "on", "size": 0}, {"treatment": "off", "size": 100}], "label": "default rule"}]}, {"trafficTypeName": "user", "name": "SPLIT_2", "trafficAllocation": 100, "trafficAllocationSeed": 1057590779, "seed": -113875324, "status": "ACTIVE", "killed": false, "defaultTreatment": "off", "changeNumber": 1675443954220, "algo": 2, "configurations": {}, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": null}, "matcherType": "ALL_KEYS", "negate": false, "userDefinedSegmentMatcherData": null, "whitelistMatcherData": null, "unaryNumericMatcherData": null, "betweenMatcherData": null, "booleanMatcherData": null, "dependencyMatcherData": null, "stringMatcherData": null}]}, "partitions": [{"treatment": "on", "size": 100}, {"treatment": "off", "size": 0}], "label": "default rule"}], "sets": ["set1", "set2"]}], "since": -1, "till": -1} \ No newline at end of file diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 02e61051..b1babada 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -34,6 +34,404 @@ from tests.integration import splits_json from tests.storage.test_pluggable import StorageMockAdapter +def _validate_last_impressions(client, *to_validate): + """Validate the last N impressions are present disregarding the order.""" + imp_storage = client._factory._get_storage('impressions') + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage) or isinstance(client._factory._get_storage('splits'), PluggableSplitStorage): + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage): + redis_client = imp_storage._redis + impressions_raw = [ + json.loads(redis_client.lpop(imp_storage.IMPRESSIONS_QUEUE_KEY)) + for _ in to_validate + ] + else: + pluggable_adapter = imp_storage._pluggable_adapter + results = pluggable_adapter.pop_items(imp_storage._impressions_queue_key) + results = [] if results == None else results + impressions_raw = [ + json.loads(i) + for i in results + ] + as_tup_set = set( + (i['i']['f'], i['i']['k'], i['i']['t']) + for i in impressions_raw + ) + assert as_tup_set == set(to_validate) + time.sleep(0.2) # delay for redis to sync + else: + impressions = imp_storage.pop_many(len(to_validate)) + as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) + assert as_tup_set == set(to_validate) + +def _validate_last_events(client, *to_validate): + """Validate the last N impressions are present disregarding the order.""" + event_storage = client._factory._get_storage('events') + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage) or isinstance(client._factory._get_storage('splits'), PluggableSplitStorage): + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage): + redis_client = event_storage._redis + events_raw = [ + json.loads(redis_client.lpop(event_storage._EVENTS_KEY_TEMPLATE)) + for _ in to_validate + ] + else: + pluggable_adapter = event_storage._pluggable_adapter + events_raw = [ + json.loads(i) + for i in pluggable_adapter.pop_items(event_storage._events_queue_key) + ] + as_tup_set = set( + (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) + for i in events_raw + ) + assert as_tup_set == set(to_validate) + else: + events = event_storage.pop_many(len(to_validate)) + as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) + assert as_tup_set == set(to_validate) + +def _get_treatment(factory): + """Test client.get_treatment().""" + try: + client = factory.client() + except: + pass + + assert client.get_treatment('user1', 'sample_feature') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + assert client.get_treatment('invalidKey', 'sample_feature') == 'off' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + + # testing a killed feature. No matter what the key, must return default treatment + assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + assert client.get_treatment('invalidKey', 'all_feature') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + + # testing WHITELIST matcher + assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) + assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) + + # testing INVALID matcher + assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + + # testing Dependency matcher + assert client.get_treatment('somekey', 'dependency_test') == 'off' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) + + # testing boolean matcher + assert client.get_treatment('True', 'boolean_test') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('boolean_test', 'True', 'on')) + + # testing regex matcher + assert client.get_treatment('abc4', 'regex_test') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + +def _get_treatment_with_config(factory): + """Test client.get_treatment_with_config().""" + try: + client = factory.client() + except: + pass + result = client.get_treatment_with_config('user1', 'sample_feature') + assert result == ('on', '{"size":15,"test":20}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatment_with_config('invalidKey', 'sample_feature') + assert result == ('off', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatment_with_config('invalidKey', 'invalid_feature') + assert result == ('control', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatment_with_config('invalidKey', 'killed_feature') + assert ('defTreatment', '{"size":15,"defTreatment":true}') == result + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatment_with_config('invalidKey', 'all_feature') + assert result == ('on', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments(factory): + """Test client.get_treatments().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments('user1', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatments('invalidKey', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == 'off' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatments('invalidKey', ['invalid_feature']) + assert len(result) == 1 + assert result['invalid_feature'] == 'control' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments('invalidKey', ['killed_feature']) + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments('invalidKey', ['all_feature']) + assert len(result) == 1 + assert result['all_feature'] == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_with_config(factory): + """Test client.get_treatments_with_config().""" + try: + client = factory.client() + except: + pass + + result = client.get_treatments_with_config('user1', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == ('on', '{"size":15,"test":20}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatments_with_config('invalidKey', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == ('off', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) + assert len(result) == 1 + assert result['invalid_feature'] == ('control', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config('invalidKey', ['killed_feature']) + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config('invalidKey', ['all_feature']) + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_by_flag_set(factory): + """Test client.get_treatments_by_flag_set().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_by_flag_set('user1', 'set1') + assert len(result) == 2 + assert result == {'sample_feature': 'on', 'whitelist_feature': 'off'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_by_flag_set('invalidKey', 'invalid_set') + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_by_flag_set('invalidKey', 'set3') + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_by_flag_set('invalidKey', 'set4') + assert len(result) == 1 + assert result['all_feature'] == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_by_flag_sets(factory): + """Test client.get_treatments_by_flag_sets().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_by_flag_sets('user1', ['set1']) + assert len(result) == 2 + assert result == {'sample_feature': 'on', 'whitelist_feature': 'off'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_by_flag_sets('invalidKey', ['invalid_set']) + assert len(result) == 0 + assert result == {} + + result = client.get_treatments_by_flag_sets('invalidKey', []) + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_by_flag_sets('invalidKey', ['set3']) + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_by_flag_sets('user1', ['set4']) + assert len(result) == 1 + assert result['all_feature'] == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'user1', 'on')) + +def _get_treatments_with_config_by_flag_set(factory): + """Test client.get_treatments_with_config_by_flag_set().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_with_config_by_flag_set('user1', 'set1') + assert len(result) == 2 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), 'whitelist_feature': ('off', None)} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'invalid_set') + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'set3') + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'set4') + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_with_config_by_flag_sets(factory): + """Test client.get_treatments_with_config_by_flag_sets().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1']) + assert len(result) == 2 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), 'whitelist_feature': ('off', None)} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_with_config_by_flag_sets('invalidKey', ['invalid_set']) + assert len(result) == 0 + assert result == {} + + result = client.get_treatments_with_config_by_flag_sets('invalidKey', []) + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config_by_flag_sets('invalidKey', ['set3']) + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config_by_flag_sets('user1', ['set4']) + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'user1', 'on')) + +def _track(factory): + """Test client.track().""" + try: + client = factory.client() + except: + pass + assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) + assert(not client.track(None, 'user', 'conversion')) + assert(not client.track('user1', None, 'conversion')) + assert(not client.track('user1', 'user', None)) + _validate_last_events( + client, + ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") + ) + +def _manager_methods(factory): + """Test manager.split/splits.""" + try: + manager = factory.manager() + except: + pass + result = manager.split('all_feature') + assert result.name == 'all_feature' + assert result.traffic_type is None + assert result.killed is False + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs == {} + + result = manager.split('killed_feature') + assert result.name == 'killed_feature' + assert result.traffic_type is None + assert result.killed is True + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' + assert result.configs['off'] == '{"size":15,"test":20}' + + result = manager.split('sample_feature') + assert result.name == 'sample_feature' + assert result.traffic_type is None + assert result.killed is False + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs['on'] == '{"size":15,"test":20}' + + assert len(manager.split_names()) == 7 + assert len(manager.splits()) == 7 class InMemoryIntegrationTests(object): """Inmemory storage-based integration tests.""" @@ -47,7 +445,7 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - split_storage.put(splits.from_raw(split)) + split_storage.update([splits.from_raw(split)], [], 0) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -61,7 +459,6 @@ def setup_method(self): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) -# telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() @@ -92,128 +489,18 @@ def teardown_method(self): self.factory.destroy(event) event.wait() - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions = imp_storage.pop_many(len(to_validate)) - as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) - assert as_tup_set == set(to_validate) - - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events = event_storage.pop_many(len(to_validate)) - as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - try: - client = self.factory.client() - except: - pass - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - try: - client = self.factory.client() - except: - pass - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): - """Test client.get_treatments().""" - try: - client = self.factory.client() - except: - pass - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing multiple splitNames + _get_treatments(self.factory) + # testing multiple splitNames + client = self.factory.client() result = client.get_treatments('invalidKey', [ 'all_feature', 'killed_feature', @@ -225,7 +512,7 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), @@ -234,39 +521,9 @@ def test_get_treatments(self): def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - try: - client = self.factory.client() - except: - pass - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments_with_config(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments_with_config('invalidKey', [ 'all_feature', 'killed_feature', @@ -278,61 +535,58 @@ def test_get_treatments_with_config(self): assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off'), ) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + def test_track(self): """Test client.track().""" - try: - client = self.factory.client() - except: - pass - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) class InMemoryOptimizedIntegrationTests(object): @@ -347,7 +601,7 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - split_storage.put(splits.from_raw(split)) + split_storage.update([splits.from_raw(split)], [], 0) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -361,7 +615,6 @@ def setup_method(self): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() @@ -382,101 +635,15 @@ def setup_method(self): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), ) # pylint:disable=attribute-defined-outside-init - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions = imp_storage.pop_many(len(to_validate)) - as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) - assert as_tup_set == set(to_validate) - - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events = event_storage.pop_many(len(to_validate)) - as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - client.get_treatment('user1', 'sample_feature') - client.get_treatment('user1', 'sample_feature') - client.get_treatment('user1', 'sample_feature') - - # Only one impression was added, and popped when validating, the rest were ignored - assert self.factory._storages['impressions']._impressions.qsize() == 0 - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" - client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments('invalidKey', [ 'all_feature', 'killed_feature', @@ -492,36 +659,9 @@ def test_get_treatments(self): def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments_with_config(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments_with_config('invalidKey', [ 'all_feature', 'killed_feature', @@ -529,55 +669,52 @@ def test_get_treatments_with_config(self): 'sample_feature' ]) assert len(result) == 4 - assert result['all_feature'] == ('on', None) assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) + _validate_last_impressions(client,) + + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ) assert self.factory._storages['impressions']._impressions.qsize() == 0 + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ) + def test_manager_methods(self): """Test manager.split/splits.""" - manager = self.factory.manager() - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) class RedisIntegrationTests(object): """Redis storage-based integration tests.""" @@ -594,7 +731,10 @@ def setup_method(self): data = json.loads(flo.read()) for split in data['splits']: redis_client.set(split_storage._get_key(split['name']), json.dumps(split)) - redis_client.set(split_storage._SPLIT_TILL_KEY, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + redis_client.sadd(split_storage._get_flag_set_key(flag_set), split['name']) + redis_client.set(split_storage._FEATURE_FLAG_TILL_KEY, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -610,7 +750,6 @@ def setup_method(self): telemetry_redis_storage = RedisTelemetryStorage(redis_client, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_redis_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_redis_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { @@ -630,135 +769,18 @@ def setup_method(self): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), ) # pylint:disable=attribute-defined-outside-init - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - redis_client = event_storage._redis - events_raw = [ - json.loads(redis_client.lpop(event_storage._EVENTS_KEY_TEMPLATE)) - for _ in to_validate - ] - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - redis_client = imp_storage._redis - impressions_raw = [ - json.loads(redis_client.lpop(imp_storage.IMPRESSIONS_QUEUE_KEY)) - for _ in to_validate - ] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - client = self.factory.client() - - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" + _get_treatments(self.factory) client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments('invalidKey', [ 'all_feature', @@ -771,44 +793,21 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off') ) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) + def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" + _get_treatments_with_config(self.factory) client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments_with_config('invalidKey', [ 'all_feature', @@ -821,58 +820,58 @@ def test_get_treatments_with_config(self): assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off'), ) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def teardown_method(self): """Clear redis cache.""" @@ -888,14 +887,17 @@ def teardown_method(self): "SPLITIO.split.regex_test", "SPLITIO.segment.human_beigns.till", "SPLITIO.split.boolean_test", - "SPLITIO.split.dependency_test" + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" ] redis_client = RedisAdapter(StrictRedis()) for key in keys_to_delete: redis_client.delete(key) - class RedisWithCacheIntegrationTests(RedisIntegrationTests): """Run the same tests as RedisIntegratioTests but with LRU/Expirable cache overlay.""" @@ -911,7 +913,7 @@ def setup_method(self): data = json.loads(flo.read()) for split in data['splits']: redis_client.set(split_storage._get_key(split['name']), json.dumps(split)) - redis_client.set(split_storage._SPLIT_TILL_KEY, data['till']) + redis_client.set(split_storage._FEATURE_FLAG_TILL_KEY, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -927,7 +929,6 @@ def setup_method(self): telemetry_redis_storage = RedisTelemetryStorage(redis_client, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_redis_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_redis_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -964,8 +965,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_1") == 'off' # Tests 1 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange1_1']) self._synchronize_now() @@ -988,8 +989,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 3 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange3_1']) self._synchronize_now() @@ -1003,8 +1004,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'off' # Tests 4 - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange4_1']) self._synchronize_now() @@ -1027,9 +1028,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 5 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1', 'SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange5_1']) self._synchronize_now() @@ -1043,8 +1043,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 6 - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange6_1']) self._synchronize_now() @@ -1141,12 +1141,11 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { @@ -1176,8 +1175,11 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1191,134 +1193,18 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added'])) self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions_raw = [] - stored_impressions = self.pluggable_storage_adapter.pop_items(imp_storage._impressions_queue_key) - if stored_impressions is not None: - impressions_raw = [json.loads(im) for im in stored_impressions] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - client = self.factory.client() - - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" + _get_treatments(self.factory) client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments('invalidKey', [ 'all_feature', @@ -1331,44 +1217,21 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off') ) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) + def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" + _get_treatments_with_config(self.factory) client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments_with_config('invalidKey', [ 'all_feature', @@ -1381,58 +1244,58 @@ def test_get_treatments_with_config(self): assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off'), ) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def teardown_method(self): """Clear pluggable cache.""" @@ -1448,9 +1311,12 @@ def teardown_method(self): "SPLITIO.split.regex_test", "SPLITIO.segment.human_beigns.till", "SPLITIO.split.boolean_test", - "SPLITIO.split.dependency_test" + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" ] - for key in keys_to_delete: self.pluggable_storage_adapter.delete(key) @@ -1461,19 +1327,18 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { 'splits': split_storage, 'segments': segment_storage, - 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), - 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), + 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata), + 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata), 'telemetry': telemetry_pluggable_storage } @@ -1496,8 +1361,11 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1511,161 +1379,34 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added'])) self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions_raw = [] - stored_impressions = self.pluggable_storage_adapter.pop_items(imp_storage._impressions_queue_key) - if stored_impressions is not None: - impressions_raw = [json.loads(im) for im in stored_impressions] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" + _get_treatment(self.factory) client = self.factory.client() assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) client.get_treatment('user1', 'sample_feature') client.get_treatment('user1', 'sample_feature') client.get_treatment('user1', 'sample_feature') + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] - # Only one impression was added, and popped when validating, the rest were ignored -# pytest.set_trace() - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" - client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatments(self.factory) - # testing multiple splitNames - result = client.get_treatments('invalidKey', [ - 'all_feature', - 'killed_feature', - 'invalid_feature', - 'sample_feature' - ]) - assert len(result) == 4 - assert result['all_feature'] == 'on' - assert result['killed_feature'] == 'defTreatment' - assert result['invalid_feature'] == 'control' - assert result['sample_feature'] == 'off' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments_with_config(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments_with_config('invalidKey', [ 'all_feature', 'killed_feature', @@ -1673,55 +1414,74 @@ def test_get_treatments_with_config(self): 'sample_feature' ]) assert len(result) == 4 - assert result['all_feature'] == ('on', None) assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + _validate_last_impressions(client,) - def test_manager_methods(self): - """Test manager.split/splits.""" - manager = self.factory.manager() - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ) def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) + + def test_manager_methods(self): + """Test manager.split/splits.""" + _manager_methods(self.factory) + + def teardown_method(self): + """Clear pluggable cache.""" + keys_to_delete = [ + "SPLITIO.segment.human_beigns", + "SPLITIO.segment.employees.till", + "SPLITIO.split.sample_feature", + "SPLITIO.splits.till", + "SPLITIO.split.killed_feature", + "SPLITIO.split.all_feature", + "SPLITIO.split.whitelist_feature", + "SPLITIO.segment.employees", + "SPLITIO.split.regex_test", + "SPLITIO.segment.human_beigns.till", + "SPLITIO.split.boolean_test", + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" + ] + for key in keys_to_delete: + self.pluggable_storage_adapter.delete(key) class PluggableNoneIntegrationTests(object): """Pluggable storage-based integration tests.""" @@ -1730,25 +1490,24 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { 'splits': split_storage, 'segments': segment_storage, - 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), - 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), + 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata), + 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata), 'telemetry': telemetry_pluggable_storage } unique_keys_synchronizer, clear_filter_sync, unique_keys_task, \ clear_filter_task, impressions_count_sync, impressions_count_task, \ - imp_strategy = set_classes('PLUGGABLE', ImpressionsMode.NONE, self.pluggable_storage_adapter, 'myprefix') + imp_strategy = set_classes('PLUGGABLE', ImpressionsMode.NONE, self.pluggable_storage_adapter) impmanager = ImpressionsManager(imp_strategy, telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], @@ -1787,8 +1546,11 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1803,67 +1565,79 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) self.client = self.factory.client() - - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - assert self.client.get_treatment('user1', 'sample_feature') == 'on' - assert self.client.get_treatment('invalidKey', 'sample_feature') == 'off' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + _get_treatment(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_get_treatments(self): """Test client.get_treatments().""" - result = self.client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - - result = self.client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 + _get_treatments(self.factory) + result = self.client.get_treatments('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == 'on' + assert result['killed_feature'] == 'defTreatment' + assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - result = self.client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - result = self.client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - - result = self.client.get_treatments_with_config('invalidKey2', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - - result = self.client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 + _get_treatments_with_config(self.factory) + result = self.client.get_treatments_with_config('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == ('on', None) + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + assert result['sample_feature'] == ('off', None) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + result = self.client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + result = self.client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_track(self): """Test client.track().""" - assert(self.client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not self.client.track(None, 'user', 'conversion')) - assert(not self.client.track('user1', None, 'conversion')) - assert(not self.client.track('user1', 'user', None)) - self._validate_last_events( - self.client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_mtk(self): self.client.get_treatment('user1', 'sample_feature') @@ -1873,6 +1647,6 @@ def test_mtk(self): event = threading.Event() self.factory.destroy(event) event.wait() - assert(json.loads(self.pluggable_storage_adapter._keys['myprefix.SPLITIO.uniquekeys'][0])["f"] =="sample_feature") - assert(json.loads(self.pluggable_storage_adapter._keys['myprefix.SPLITIO.uniquekeys'][0])["ks"].sort() == + assert(json.loads(self.pluggable_storage_adapter._keys['SPLITIO.uniquekeys'][0])["f"] =="sample_feature") + assert(json.loads(self.pluggable_storage_adapter._keys['SPLITIO.uniquekeys'][0])["ks"].sort() == ["invalidKey2", "invalidKey", "user1"].sort()) \ No newline at end of file diff --git a/tests/integration/test_pluggable_integration.py b/tests/integration/test_pluggable_integration.py index f7e23f9f..024f1688 100644 --- a/tests/integration/test_pluggable_integration.py +++ b/tests/integration/test_pluggable_integration.py @@ -23,9 +23,9 @@ def test_put_fetch(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - adapter.set(storage._prefix.format(split_name=split['name']), split) + adapter.set(storage._prefix.format(feature_flag_name=split['name']), split) adapter.increment(storage._traffic_type_prefix.format(traffic_type_name=split['trafficTypeName']), 1) - adapter.set(storage._split_till_prefix, data['till']) + adapter.set(storage._feature_flag_till_prefix, data['till']) split_objects = [splits.from_raw(raw) for raw in data['splits']] for split_object in split_objects: @@ -52,7 +52,7 @@ def test_put_fetch(self): assert len(original_condition.matchers) == len(fetched_condition.matchers) assert len(original_condition.partitions) == len(fetched_condition.partitions) - adapter.set(storage._split_till_prefix, data['till']) + adapter.set(storage._feature_flag_till_prefix, data['till']) assert storage.get_change_number() == data['till'] assert storage.is_valid_traffic_type('user') is True @@ -89,9 +89,9 @@ def test_get_all(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - adapter.set(storage._prefix.format(split_name=split['name']), split) + adapter.set(storage._prefix.format(feature_flag_name=split['name']), split) adapter.increment(storage._traffic_type_prefix.format(traffic_type_name=split['trafficTypeName']), 1) - adapter.set(storage._split_till_prefix, data['till']) + adapter.set(storage._feature_flag_till_prefix, data['till']) split_objects = [splits.from_raw(raw) for raw in data['splits']] original_splits = {split.name: split for split in split_objects} diff --git a/tests/integration/test_redis_integration.py b/tests/integration/test_redis_integration.py index 685f72c5..279b45a5 100644 --- a/tests/integration/test_redis_integration.py +++ b/tests/integration/test_redis_integration.py @@ -26,7 +26,7 @@ def test_put_fetch(self): split_objects = [splits.from_raw(raw) for raw in split_changes['splits']] for split_object in split_objects: raw = split_object.to_json() - adapter.set(RedisSplitStorage._SPLIT_KEY.format(split_name=split_object.name), json.dumps(raw)) + adapter.set(RedisSplitStorage._FEATURE_FLAG_KEY.format(feature_flag_name=split_object.name), json.dumps(raw)) adapter.incr(RedisSplitStorage._TRAFFIC_TYPE_KEY.format(traffic_type_name=split_object.traffic_type_name)) original_splits = {split.name: split for split in split_objects} @@ -50,7 +50,7 @@ def test_put_fetch(self): assert len(original_condition.matchers) == len(fetched_condition.matchers) assert len(original_condition.partitions) == len(fetched_condition.partitions) - adapter.set(RedisSplitStorage._SPLIT_TILL_KEY, split_changes['till']) + adapter.set(RedisSplitStorage._FEATURE_FLAG_TILL_KEY, split_changes['till']) assert storage.get_change_number() == split_changes['till'] assert storage.is_valid_traffic_type('user') is True @@ -89,7 +89,7 @@ def test_get_all(self): split_objects = [splits.from_raw(raw) for raw in split_changes['splits']] for split_object in split_objects: raw = split_object.to_json() - adapter.set(RedisSplitStorage._SPLIT_KEY.format(split_name=split_object.name), json.dumps(raw)) + adapter.set(RedisSplitStorage._FEATURE_FLAG_KEY.format(feature_flag_name=split_object.name), json.dumps(raw)) original_splits = {split.name: split for split in split_objects} fetched_names = storage.get_split_names() diff --git a/tests/models/test_splits.py b/tests/models/test_splits.py index 847448b0..23688d9e 100644 --- a/tests/models/test_splits.py +++ b/tests/models/test_splits.py @@ -60,6 +60,7 @@ class SplitTests(object): 'configurations': { 'on': '{"color": "blue", "size": 13}' }, + 'sets': ['set1', 'set2'] } def test_from_raw(self): @@ -79,6 +80,7 @@ def test_from_raw(self): assert len(parsed.conditions) == 2 assert parsed.get_configurations_for('on') == '{"color": "blue", "size": 13}' assert parsed._configurations == {'on': '{"color": "blue", "size": 13}'} + assert parsed.sets == {'set1', 'set2'} def test_get_segment_names(self, mocker): """Test fetching segment names.""" @@ -89,7 +91,6 @@ def test_get_segment_names(self, mocker): split1 = splits.Split( 'some_split', 123, False, 'off', 'user', 'ACTIVE', 123, [cond1, cond2]) assert split1.get_segment_names() == ['segment%d' % i for i in range(1, 5)] - def test_to_json(self): """Test json serialization.""" as_json = splits.from_raw(self.raw).to_json() @@ -105,6 +106,7 @@ def test_to_json(self): assert as_json['defaultTreatment'] == 'off' assert as_json['algo'] == 2 assert len(as_json['conditions']) == 2 + assert sorted(as_json['sets']) == ['set1', 'set2'] def test_to_split_view(self): """Test SplitView creation.""" @@ -115,3 +117,5 @@ def test_to_split_view(self): assert as_split_view.killed == self.raw['killed'] assert as_split_view.traffic_type == self.raw['trafficTypeName'] assert set(as_split_view.treatments) == set(['on', 'off']) + assert as_split_view.default_treatment == self.raw['defaultTreatment'] + assert sorted(as_split_view.sets) == sorted(list(self.raw['sets'])) diff --git a/tests/models/test_telemetry_model.py b/tests/models/test_telemetry_model.py index e34ee466..5ff98d72 100644 --- a/tests/models/test_telemetry_model.py +++ b/tests/models/test_telemetry_model.py @@ -6,7 +6,6 @@ from splitio.models.telemetry import StorageType, OperationMode, MethodLatencies, MethodExceptions, \ HTTPLatencies, HTTPErrors, LastSynchronization, TelemetryCounters, TelemetryConfig, \ StreamingEvent, StreamingEvents, UpdateFromSSE - import splitio.models.telemetry as ModelTelemetry class TelemetryModelTests(object): @@ -56,6 +55,14 @@ def test_method_latencies(self, mocker): assert(method_latencies._treatment_with_config[ModelTelemetry.get_latency_bucket_index(50)] == 1) elif method.value == 'treatments_with_config': assert(method_latencies._treatments_with_config[ModelTelemetry.get_latency_bucket_index(50)] == 1) + elif method.value == 'treatments_by_flag_set': + assert(method_latencies._treatments_by_flag_set[ModelTelemetry.get_latency_bucket_index(50)] == 1) + elif method.value == 'treatments_by_flag_sets': + assert(method_latencies._treatments_by_flag_sets[ModelTelemetry.get_latency_bucket_index(50)] == 1) + elif method.value == 'treatments_with_config_by_flag_set': + assert(method_latencies._treatments_with_config_by_flag_set[ModelTelemetry.get_latency_bucket_index(50)] == 1) + elif method.value == 'treatments_with_config_by_flag_sets': + assert(method_latencies._treatments_with_config_by_flag_sets[ModelTelemetry.get_latency_bucket_index(50)] == 1) elif method.value == 'track': assert(method_latencies._track[ModelTelemetry.get_latency_bucket_index(50)] == 1) method_latencies.add_latency(method, 50000000) @@ -67,6 +74,14 @@ def test_method_latencies(self, mocker): assert(method_latencies._treatment_with_config[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) if method.value == 'treatments_with_config': assert(method_latencies._treatments_with_config[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) + elif method.value == 'treatments_by_flag_set': + assert(method_latencies._treatments_by_flag_set[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) + elif method.value == 'treatments_by_flag_sets': + assert(method_latencies._treatments_by_flag_sets[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) + elif method.value == 'treatments_with_config_by_flag_set': + assert(method_latencies._treatments_with_config_by_flag_set[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) + elif method.value == 'treatments_with_config_by_flag_sets': + assert(method_latencies._treatments_with_config_by_flag_sets[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) if method.value == 'track': assert(method_latencies._track[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) @@ -81,9 +96,23 @@ def test_method_latencies(self, mocker): [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS, 20) for i in range(2)] method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, 50) method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, 20) + [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, 20) for i in range(3)] + [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, 20) for i in range(4)] + [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, 20) for i in range(5)] + [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, 20) for i in range(6)] method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TRACK, 20) latencies = method_latencies.pop_all() - assert(latencies == {'methodLatencies': {'treatment': [1] + [0] * 22, 'treatments': [2] + [0] * 22, 'treatment_with_config': [1] + [0] * 22, 'treatments_with_config': [1] + [0] * 22, 'track': [1] + [0] * 22}}) + assert(latencies == {'methodLatencies': {'treatment': [1] + [0] * 22, + 'treatments': [2] + [0] * 22, + 'treatment_with_config': [1] + [0] * 22, + 'treatments_with_config': [1] + [0] * 22, + 'treatments_by_flag_set': [3] + [0] * 22, + 'treatments_by_flag_sets': [4] + [0] * 22, + 'treatments_with_config_by_flag_set': [5] + [0] * 22, + 'treatments_with_config_by_flag_sets': [6] + [0] * 22, + 'track': [1] + [0] * 22} + } + ) def test_http_latencies(self, mocker): http_latencies = HTTPLatencies() @@ -148,6 +177,10 @@ def test_method_exceptions(self, mocker): method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG) [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG) for i in range(5)] [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TRACK) for i in range(3)] + [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET) for i in range(6)] + [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS) for i in range(7)] + [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET) for i in range(8)] + [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS) for i in range(9)] exceptions = method_exception.pop_all() assert(method_exception._treatment == 0) @@ -155,7 +188,18 @@ def test_method_exceptions(self, mocker): assert(method_exception._treatment_with_config == 0) assert(method_exception._treatments_with_config == 0) assert(method_exception._track == 0) - assert(exceptions == {'methodExceptions': {'treatment': 2, 'treatments': 1, 'treatment_with_config': 1, 'treatments_with_config': 5, 'track': 3}}) + assert(exceptions == {'methodExceptions': {'treatment': 2, + 'treatments': 1, + 'treatment_with_config': 1, + 'treatments_with_config': 5, + 'treatments_by_flag_set': 6, + 'treatments_by_flag_sets': 7, + 'treatments_with_config_by_flag_set': 8, + 'treatments_with_config_by_flag_sets': 9, + 'track': 3 + } + } + ) def test_http_errors(self, mocker): http_error = HTTPErrors() @@ -269,9 +313,10 @@ def test_telemetry_config(self): 'impressionsRefreshRate': 60, 'eventsPushRate': 60, 'metricsRefreshRate': 10, - 'storageType': None + 'storageType': None, + 'flagSetsFilter': None } - telemetry_config.record_config(config, {}) + telemetry_config.record_config(config, {}, 5, 2) assert(telemetry_config.get_stats() == {'oM': 0, 'sT': telemetry_config._get_storage_type(config['operationMode'], config['storageType']), 'sE': config['streamingEnabled'], @@ -286,7 +331,9 @@ def test_telemetry_config(self): 'nR': 0, 'bT': 0, 'aF': 0, - 'rF': 0} + 'rF': 0, + 'fsT': 5, + 'fsI': 2} ) telemetry_config.record_ready_time(10) diff --git a/tests/push/test_split_worker.py b/tests/push/test_split_worker.py index 09ede0bb..23831bc5 100644 --- a/tests/push/test_split_worker.py +++ b/tests/push/test_split_worker.py @@ -55,18 +55,13 @@ def test_handler(self, mocker): def get_change_number(): return 2345 - - self._feature_flag = None - def put(feature_flag): - self._feature_flag = feature_flag + split_worker._feature_flag_storage.get_change_number = get_change_number self.new_change_number = 0 - def set_change_number(new_change_number): - self.new_change_number = new_change_number - - split_worker._feature_flag_storage.get_change_number = get_change_number - split_worker._feature_flag_storage.set_change_number = set_change_number - split_worker._feature_flag_storage.put = put + def update(to_add, to_delete, change_number): + self.new_change_number = change_number + split_worker._feature_flag_storage.update = update + split_worker._feature_flag_storage.config_flag_sets_used = 0 # should call the handler q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 12345, "{}", 1)) @@ -98,45 +93,45 @@ def test_compression(self, mocker): split_worker.start() def get_change_number(): return 2345 - - def put(feature_flag): - self._feature_flag = feature_flag - - def remove(feature_flag): - self._feature_flag_delete = feature_flag - split_worker._feature_flag_storage.get_change_number = get_change_number - split_worker._feature_flag_storage.put = put - split_worker._feature_flag_storage.remove = remove + + self._feature_flag_added = None + self._feature_flag_deleted = None + def update(feature_flag_add, feature_flag_delete, change_number): + self._feature_flag_added = feature_flag_add + self._feature_flag_deleted = feature_flag_delete + split_worker._feature_flag_storage.update = update + split_worker._feature_flag_storage.config_flag_sets_used = 0 # compression 0 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiIzM2VhZmE1MC0xYTY1LTExZWQtOTBkZi1mYTMwZDk2OTA0NDUiLCJuYW1lIjoiYmlsYWxfc3BsaXQiLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOi0xMzY0MTE5MjgyLCJzZWVkIjotNjA1OTM4ODQzLCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib2ZmIiwiY2hhbmdlTnVtYmVyIjoxNjg0MzQwOTA4NDc1LCJhbGdvIjoyLCJjb25maWd1cmF0aW9ucyI6e30sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoidXNlciJ9LCJtYXRjaGVyVHlwZSI6IklOX1NFR01FTlQiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6eyJzZWdtZW50TmFtZSI6ImJpbGFsX3NlZ21lbnQifX1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjEwMH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgYmlsYWxfc2VnbWVudCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfV0sImxhYmVsIjoiZGVmYXVsdCBydWxlIn1dfQ==', 0)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' +# pytest.set_trace() + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 1 # compression 2 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eJzEUtFq20AQ/JUwz2c4WZZr3ZupTQh1FKjcQinGrKU95cjpZE6nh9To34ssJ3FNX0sfd3Zm53b2TgietDbF9vXIGdUMha5lDwFTQiGOmTQlchLRPJlEEZeTVJZ6oimWZTpP5WyWQMCNyoOxZPft0ZoA8TZ5aW1TUDCNg4qk/AueM5dQkyiez6IonS6mAu0IzWWSxovFLBZoA4WuhcLy8/bh+xoCL8bagaXJtixQsqbOhq1nCjW7AIVGawgUz+Qqzrr6wB4qmi9m00/JIk7TZCpAtmqgpgJF47SpOn9+UQt16s9YaS71z9NHOYQFha9Pm83Tty0EagrFM/t733RHqIFZH4wb7LDMVh+Ecc4Lv+ZsuQiNH8hXF3hLv39XXNCHbJ+v7x/X2eDmuKLA74sPihVr47jMuRpWfxy1Kwo0GLQjmv1xpBFD3+96gSP5cLVouM7QQaA1vxhK9uKmd853bEZS9jsBSwe2UDDu7mJxd2Mo/muQy81m/2X9I7+N8R/FcPmUd76zjH7X/w4AAP//90glTw==', 2)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 2 # compression 1 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'H4sIAAkVZWQC/8WST0+DQBDFv0qzZ0ig/BF6a2xjGismUk2MaZopzOKmy9Isy0EbvrtDwbY2Xo233Tdv5se85cCMBs5FtvrYYwIlsglratTMYiKns+chcAgc24UwsF0Xczt2cm5z8Jw8DmPH9wPyqr5zKyTITb2XwpA4TJ5KWWVgRKXYxHWcX/QUkVi264W+68bjaGyxupdCJ4i9KPI9UgyYpibI9Ha1eJnT/J2QsnNxkDVaLEcOjTQrjWBKVIasFefky95BFZg05Zb2mrhh5I9vgsiL44BAIIuKTeiQVYqLotHHLyLOoT1quRjub4fztQuLxj89LpePzytClGCyd9R3umr21ErOcitUh2PTZHY29HN2+JGixMxUujNfvMB3+u2pY1AXySad3z3Mk46msACDp8W7jhly4uUpFt3qD33vDAx0gLpXkx+P1GusbdcE24M2F4uaywwVEWvxSa1Oa13Vjvn2RXradm0xCVuUVBJqNCBGV0DrX4OcLpeb+/lreh3jH8Uw/JQj3UhkxPgCCurdEnADAAA=', 1)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 3 # should call delete split - self._feature_flag = None - self._feature_flag_delete = None + self._feature_flag_added = None + self._feature_flag_deleted = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eyJ0cmFmZmljVHlwZU5hbWUiOiAidXNlciIsICJpZCI6ICIzM2VhZmE1MC0xYTY1LTExZWQtOTBkZi1mYTMwZDk2OTA0NDUiLCAibmFtZSI6ICJiaWxhbF9zcGxpdCIsICJ0cmFmZmljQWxsb2NhdGlvbiI6IDEwMCwgInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6IC0xMzY0MTE5MjgyLCAic2VlZCI6IC02MDU5Mzg4NDMsICJzdGF0dXMiOiAiQVJDSElWRUQiLCAia2lsbGVkIjogZmFsc2UsICJkZWZhdWx0VHJlYXRtZW50IjogIm9mZiIsICJjaGFuZ2VOdW1iZXIiOiAxNjg0Mjc1ODM5OTUyLCAiYWxnbyI6IDIsICJjb25maWd1cmF0aW9ucyI6IHt9LCAiY29uZGl0aW9ucyI6IFt7ImNvbmRpdGlvblR5cGUiOiAiUk9MTE9VVCIsICJtYXRjaGVyR3JvdXAiOiB7ImNvbWJpbmVyIjogIkFORCIsICJtYXRjaGVycyI6IFt7ImtleVNlbGVjdG9yIjogeyJ0cmFmZmljVHlwZSI6ICJ1c2VyIn0sICJtYXRjaGVyVHlwZSI6ICJJTl9TRUdNRU5UIiwgIm5lZ2F0ZSI6IGZhbHNlLCAidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOiB7InNlZ21lbnROYW1lIjogImJpbGFsX3NlZ21lbnQifX1dfSwgInBhcnRpdGlvbnMiOiBbeyJ0cmVhdG1lbnQiOiAib24iLCAic2l6ZSI6IDB9LCB7InRyZWF0bWVudCI6ICJvZmYiLCAic2l6ZSI6IDEwMH1dLCAibGFiZWwiOiAiaW4gc2VnbWVudCBiaWxhbF9zZWdtZW50In0sIHsiY29uZGl0aW9uVHlwZSI6ICJST0xMT1VUIiwgIm1hdGNoZXJHcm91cCI6IHsiY29tYmluZXIiOiAiQU5EIiwgIm1hdGNoZXJzIjogW3sia2V5U2VsZWN0b3IiOiB7InRyYWZmaWNUeXBlIjogInVzZXIifSwgIm1hdGNoZXJUeXBlIjogIkFMTF9LRVlTIiwgIm5lZ2F0ZSI6IGZhbHNlfV19LCAicGFydGl0aW9ucyI6IFt7InRyZWF0bWVudCI6ICJvbiIsICJzaXplIjogMH0sIHsidHJlYXRtZW50IjogIm9mZiIsICJzaXplIjogMTAwfV0sICJsYWJlbCI6ICJkZWZhdWx0IHJ1bGUifV19', 0)) time.sleep(0.1) - assert self._feature_flag_delete == 'bilal_split' - assert self._feature_flag == None + assert self._feature_flag_deleted[0] == 'bilal_split' + self._feature_flag_added = None def test_edge_cases(self, mocker): q = queue.Queue() diff --git a/tests/storage/test_flag_sets.py b/tests/storage/test_flag_sets.py new file mode 100644 index 00000000..f4258bd5 --- /dev/null +++ b/tests/storage/test_flag_sets.py @@ -0,0 +1,63 @@ +from splitio.storage import FlagSetsFilter +from splitio.storage.inmemmory import FlagSets + +class FlagSetsFilterTests(object): + """Flag sets filter storage tests.""" + def test_without_initial_set(self): + flag_set = FlagSets() + assert flag_set.sets_feature_flag_map == {} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == False + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_with_initial_set(self): + flag_set = FlagSets(['set1', 'set2']) + assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == True + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_flag_set_filter(self): + flag_set_filter = FlagSetsFilter() + assert flag_set_filter.flag_sets == set() + assert not flag_set_filter.should_filter + + flag_set_filter = FlagSetsFilter(['set1', 'set2']) + assert flag_set_filter.flag_sets == set({'set1', 'set2'}) + assert flag_set_filter.should_filter + assert flag_set_filter.intersect(set({'set1', 'set2'})) + assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) + assert not flag_set_filter.intersect(set({'set4'})) + assert not flag_set_filter.set_exist('set4') + assert flag_set_filter.set_exist('set1') + + flag_set_filter = FlagSetsFilter(['set5', 'set2', 'set6', 'set1']) + assert flag_set_filter.sorted_flag_sets == ['set1', 'set2', 'set5', 'set6'] \ No newline at end of file diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 7319548d..2c44bd2d 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -8,10 +8,55 @@ from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper import splitio.models.telemetry as ModelTelemetry +from splitio.storage import FlagSetsFilter from splitio.engine.telemetry import TelemetryStorageProducer - from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ - InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage + InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, FlagSets + + +class FlagSetsFilterTests(object): + """Flag sets filter storage tests.""" + def test_without_initial_set(self): + flag_set = FlagSets() + assert flag_set.sets_feature_flag_map == {} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == False + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_with_initial_set(self): + flag_set = FlagSets(['set1', 'set2']) + assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == True + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False class InMemorySplitStorageTests(object): @@ -25,14 +70,18 @@ def test_storing_retrieving_splits(self, mocker): name_property = mocker.PropertyMock() name_property.return_value = 'some_split' type(split).name = name_property + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split).sets = sets_property + + storage.update([split], [], 0) - storage.put(split) assert storage.get('some_split') == split assert storage.get_split_names() == ['some_split'] assert storage.get_all_splits() == [split] assert storage.get('nonexistant_split') is None - storage.remove('some_split') + storage.update([], ['some_split'], 0) assert storage.get('some_split') is None def test_get_splits(self, mocker): @@ -45,10 +94,13 @@ def test_get_splits(self, mocker): name2_prop = mocker.PropertyMock() name2_prop.return_value = 'split2' type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property storage = InMemorySplitStorage() - storage.put(split1) - storage.put(split2) + storage.update([split1, split2], [], 0) splits = storage.fetch_many(['split1', 'split2', 'split3']) assert len(splits) == 3 @@ -60,7 +112,7 @@ def test_store_get_changenumber(self): """Test that storing and retrieving change numbers works.""" storage = InMemorySplitStorage() assert storage.get_change_number() == -1 - storage.set_change_number(5) + storage.update([], [], 5) assert storage.get_change_number() == 5 def test_get_split_names(self, mocker): @@ -73,10 +125,13 @@ def test_get_split_names(self, mocker): name2_prop = mocker.PropertyMock() name2_prop.return_value = 'split2' type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property storage = InMemorySplitStorage() - storage.put(split1) - storage.put(split2) + storage.update([split1, split2], [], 0) assert set(storage.get_split_names()) == set(['split1', 'split2']) @@ -90,10 +145,13 @@ def test_get_all_splits(self, mocker): name2_prop = mocker.PropertyMock() name2_prop.return_value = 'split2' type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property storage = InMemorySplitStorage() - storage.put(split1) - storage.put(split2) + storage.update([split1, split2], [], 0) all_splits = storage.get_all_splits() assert next(s for s in all_splits if s.name == 'split1') @@ -120,30 +178,31 @@ def test_is_valid_traffic_type(self, mocker): type(split1).traffic_type_name = tt_user type(split2).traffic_type_name = tt_account type(split3).traffic_type_name = tt_user + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property + type(split3).sets = sets_property storage = InMemorySplitStorage() - storage.put(split1) + storage.update([split1], [], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is False - storage.put(split2) - assert storage.is_valid_traffic_type('user') is True - assert storage.is_valid_traffic_type('account') is True - - storage.put(split3) + storage.update([split2, split3], [], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is True - storage.remove('split1') + storage.update([], ['split1'], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is True - storage.remove('split2') + storage.update([], ['split2'], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is False - storage.remove('split3') + storage.update([], ['split3'], 0) assert storage.is_valid_traffic_type('user') is False assert storage.is_valid_traffic_type('account') is False @@ -155,11 +214,14 @@ def test_traffic_type_inc_dec_logic(self, mocker): name1_prop = mocker.PropertyMock() name1_prop.return_value = 'split1' type(split1).name = name1_prop - split2 = mocker.Mock() name2_prop = mocker.PropertyMock() name2_prop.return_value = 'split1' type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property tt_user = mocker.PropertyMock() tt_user.return_value = 'user' @@ -170,11 +232,11 @@ def test_traffic_type_inc_dec_logic(self, mocker): type(split1).traffic_type_name = tt_user type(split2).traffic_type_name = tt_account - storage.put(split1) + storage.update([split1], [], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is False - storage.put(split2) + storage.update([split2], [], 0) assert storage.is_valid_traffic_type('user') is False assert storage.is_valid_traffic_type('account') is True @@ -184,8 +246,7 @@ def test_kill_locally(self): split = Split('some_split', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1) - storage.put(split) - storage.set_change_number(1) + storage.update([split], [], 1) storage.kill_locally('test', 'default_treatment', 2) assert storage.get('test') is None @@ -198,6 +259,94 @@ def test_kill_locally(self): storage.kill_locally('some_split', 'default_treatment', 3) assert storage.get('some_split').change_number == 3 + def test_flag_sets_with_config_sets(self): + storage = InMemorySplitStorage(['set10', 'set02', 'set05']) + assert storage.flag_set_filter.flag_sets == {'set10', 'set02', 'set05'} + assert storage.flag_set_filter.should_filter + + assert storage.flag_set.sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} + + split1 = Split('split1', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set10', 'set02']) + split2 = Split('split2', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set05', 'set02']) + split3 = Split('split3', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set04', 'set05']) + storage.update([split1], [], 1) + assert storage.get_feature_flags_by_sets(['set10']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02', 'set10']) == ['split1'] + assert storage.is_flag_set_exist('set10') + assert storage.is_flag_set_exist('set02') + assert not storage.is_flag_set_exist('set03') + + storage.update([split2], [], 1) + assert storage.get_feature_flags_by_sets(['set05']) == ['split2'] + assert sorted(storage.get_feature_flags_by_sets(['set02', 'set05'])) == ['split1', 'split2'] + assert storage.is_flag_set_exist('set05') + + storage.update([], [split2.name], 1) + assert storage.is_flag_set_exist('set05') + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set05']) == [] + + split1 = Split('split1', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set02']) + storage.update([split1], [], 1) + assert storage.is_flag_set_exist('set10') + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + + storage.update([], [split1.name], 1) + assert storage.get_feature_flags_by_sets(['set02']) == [] + assert storage.flag_set.sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} + + storage.update([split3], [], 1) + assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] + assert not storage.is_flag_set_exist('set04') + + def test_flag_sets_withut_config_sets(self): + storage = InMemorySplitStorage() + assert storage.flag_set_filter.flag_sets == set({}) + assert not storage.flag_set_filter.should_filter + + assert storage.flag_set.sets_feature_flag_map == {} + + split1 = Split('split1', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set10', 'set02']) + split2 = Split('split2', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set05', 'set02']) + split3 = Split('split3', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set04', 'set05']) + storage.update([split1], [], 1) + assert storage.get_feature_flags_by_sets(['set10']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + assert storage.is_flag_set_exist('set10') + assert storage.is_flag_set_exist('set02') + assert not storage.is_flag_set_exist('set03') + + storage.update([split2], [], 1) + assert storage.get_feature_flags_by_sets(['set05']) == ['split2'] + assert sorted(storage.get_feature_flags_by_sets(['set02', 'set05'])) == ['split1', 'split2'] + assert storage.is_flag_set_exist('set05') + + storage.update([], [split2.name], 1) + assert not storage.is_flag_set_exist('set05') + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + + split1 = Split('split1', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set02']) + storage.update([split1], [], 1) + assert not storage.is_flag_set_exist('set10') + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + + storage.update([], [split1.name], 1) + assert storage.get_feature_flags_by_sets(['set02']) == [] + assert storage.flag_set.sets_feature_flag_map == {} + + storage.update([split3], [], 1) + assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] + assert storage.get_feature_flags_by_sets(['set04', 'set05']) == ['split3'] + class InMemorySegmentStorageTests(object): """In memory segment storage tests.""" @@ -449,7 +598,7 @@ def test_resets(self): assert(storage._counters._auth_rejections == 0) assert(storage._counters._token_refreshes == 0) - assert(storage._method_exceptions.pop_all() == {'methodExceptions': {'treatment': 0, 'treatments': 0, 'treatment_with_config': 0, 'treatments_with_config': 0, 'track': 0}}) + assert(storage._method_exceptions.pop_all() == {'methodExceptions': {'treatment': 0, 'treatments': 0, 'treatment_with_config': 0, 'treatments_with_config': 0, 'treatments_by_flag_set': 0, 'treatments_by_flag_sets': 0, 'treatments_with_config_by_flag_set': 0, 'treatments_with_config_by_flag_sets': 0, 'track': 0}}) assert(storage._last_synchronization.get_all() == {'lastSynchronizations': {'split': 0, 'segment': 0, 'impression': 0, 'impressionCount': 0, 'event': 0, 'telemetry': 0, 'token': 0}}) assert(storage._http_sync_errors.pop_all() == {'httpErrors': {'split': {}, 'segment': {}, 'impression': {}, 'impressionCount': {}, 'event': {}, 'telemetry': {}, 'token': {}}}) assert(storage._tel_config.get_stats() == { @@ -467,12 +616,14 @@ def test_resets(self): 'iL': False, 'hp': None, 'aF': 0, - 'rF': 0 + 'rF': 0, + 'fsT': 0, + 'fsI': 0 }) assert(storage._streaming_events.pop_streaming_events() == {'streamingEvents': []}) assert(storage._tags == []) - assert(storage._method_latencies.pop_all() == {'methodLatencies': {'treatment': [0] * 23, 'treatments': [0] * 23, 'treatment_with_config': [0] * 23, 'treatments_with_config': [0] * 23, 'track': [0] * 23}}) + assert(storage._method_latencies.pop_all() == {'methodLatencies': {'treatment': [0] * 23, 'treatments': [0] * 23, 'treatment_with_config': [0] * 23, 'treatments_with_config': [0] * 23, 'treatments_by_flag_set': [0] * 23, 'treatments_by_flag_sets': [0] * 23, 'treatments_with_config_by_flag_set': [0] * 23, 'treatments_with_config_by_flag_sets': [0] * 23, 'track': [0] * 23}}) assert(storage._http_latencies.pop_all() == {'httpLatencies': {'split': [0] * 23, 'segment': [0] * 23, 'impression': [0] * 23, 'impressionCount': [0] * 23, 'event': [0] * 23, 'telemetry': [0] * 23, 'token': [0] * 23}}) def test_record_config(self): @@ -490,7 +641,7 @@ def test_record_config(self): 'metricsRefreshRate': 10, 'storageType': None } - storage.record_config(config, {}) + storage.record_config(config, {}, 2, 1) storage.record_active_and_redundant_factories(1, 0) assert(storage._tel_config.get_stats() == {'oM': 0, 'sT': storage._tel_config._get_storage_type(config['operationMode'], config['storageType']), @@ -506,7 +657,9 @@ def test_record_config(self): 'tR': 0, 'nR': 0, 'aF': 1, - 'rF': 0} + 'rF': 0, + 'fsT': 2, + 'fsI': 1} ) def test_record_counters(self): @@ -597,6 +750,14 @@ def _get_method_latency(self, resource, storage): return storage._method_latencies._treatment_with_config elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG: return storage._method_latencies._treatments_with_config + elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET: + return storage._method_latencies._treatments_by_flag_set + elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS: + return storage._method_latencies._treatments_by_flag_sets + elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET: + return storage._method_latencies._treatments_with_config_by_flag_set + elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS: + return storage._method_latencies._treatments_with_config_by_flag_sets elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TRACK: return storage._method_latencies._track else: @@ -627,6 +788,10 @@ def test_pop_counters(self): storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS) storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG) [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG) for i in range(5)] + [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET) for i in range(3)] + [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS) for i in range(10)] + [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET) for i in range(7)] + [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS) for i in range(6)] [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TRACK) for i in range(3)] exceptions = storage.pop_exceptions() assert(storage._method_exceptions._treatment == 0) @@ -634,7 +799,7 @@ def test_pop_counters(self): assert(storage._method_exceptions._treatment_with_config == 0) assert(storage._method_exceptions._treatments_with_config == 0) assert(storage._method_exceptions._track == 0) - assert(exceptions == {'methodExceptions': {'treatment': 2, 'treatments': 1, 'treatment_with_config': 1, 'treatments_with_config': 5, 'track': 3}}) + assert(exceptions == {'methodExceptions': {'treatment': 2, 'treatments': 1, 'treatment_with_config': 1, 'treatments_with_config': 5, 'treatments_by_flag_set': 3, 'treatments_by_flag_sets': 10, 'treatments_with_config_by_flag_set': 7, 'treatments_with_config_by_flag_sets': 6, 'track': 3}}) storage.add_tag('tag1') storage.add_tag('tag2') @@ -686,6 +851,10 @@ def test_pop_latencies(self): [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS, i) for i in [7, 10, 14, 13]] [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, i) for i in [200]] [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, i) for i in [50, 40]] + [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, i) for i in [15, 20]] + [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, i) for i in [14, 25]] + [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, i) for i in [100]] + [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, i) for i in [50, 20]] [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TRACK, i) for i in [1, 10, 100]] latencies = storage.pop_latencies() @@ -694,8 +863,16 @@ def test_pop_latencies(self): assert(storage._method_latencies._treatment_with_config == [0] * 23) assert(storage._method_latencies._treatments_with_config == [0] * 23) assert(storage._method_latencies._track == [0] * 23) - assert(latencies == {'methodLatencies': {'treatment': [4] + [0] * 22, 'treatments': [4] + [0] * 22, - 'treatment_with_config': [1] + [0] * 22, 'treatments_with_config': [2] + [0] * 22, 'track': [3] + [0] * 22}}) + assert(latencies == {'methodLatencies': { + 'treatment': [4] + [0] * 22, + 'treatments': [4] + [0] * 22, + 'treatment_with_config': [1] + [0] * 22, + 'treatments_with_config': [2] + [0] * 22, + 'treatments_by_flag_set': [2] + [0] * 22, + 'treatments_by_flag_sets': [2] + [0] * 22, + 'treatments_with_config_by_flag_set': [1] + [0] * 22, + 'treatments_with_config_by_flag_sets': [2] + [0] * 22, + 'track': [3] + [0] * 22}}) [storage.record_sync_latency(ModelTelemetry.HTTPExceptionsAndLatencies.SPLIT, i) for i in [50, 10, 20, 40]] [storage.record_sync_latency(ModelTelemetry.HTTPExceptionsAndLatencies.SEGMENT, i) for i in [70, 100, 40, 30]] diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index 38a5b511..b5772b56 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -1,18 +1,18 @@ """Pluggable storage test module.""" import json import threading +import pytest from splitio.models.splits import Split from splitio.models import splits, segments from splitio.models.segments import Segment from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper +from splitio.storage import FlagSetsFilter from splitio.storage.pluggable import PluggableSplitStorage, PluggableSegmentStorage, PluggableImpressionsStorage, PluggableEventsStorage, PluggableTelemetryStorage from splitio.client.util import get_metadata, SdkMetadata from splitio.models.telemetry import MAX_TAGS, MethodExceptionsAndLatencies, OperationMode - from tests.integration import splits_json -import pytest class StorageMockAdapter(object): def __init__(self): @@ -138,9 +138,10 @@ def test_init(self): prefix = 'myprefix.' else: prefix = '' - assert(pluggable_split_storage._prefix == prefix + "SPLITIO.split.{split_name}") + assert(pluggable_split_storage._prefix == prefix + "SPLITIO.split.{feature_flag_name}") assert(pluggable_split_storage._traffic_type_prefix == prefix + "SPLITIO.trafficType.{traffic_type_name}") - assert(pluggable_split_storage._split_till_prefix == prefix + "SPLITIO.splits.till") + assert(pluggable_split_storage._flag_set_prefix == prefix + "SPLITIO.flagSet.{flag_set}") + assert(pluggable_split_storage._feature_flag_till_prefix == prefix + "SPLITIO.splits.till") # TODO: To be added when producer mode is aupported # def test_put_many(self): @@ -165,7 +166,7 @@ def test_get(self): split1 = splits.from_raw(splits_json['splitChange1_2']['splits'][0]) split_name = splits_json['splitChange1_2']['splits'][0]['name'] - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split_name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split_name), split1.to_json()) assert(pluggable_split_storage.get(split_name).to_json() == splits.from_raw(splits_json['splitChange1_2']['splits'][0]).to_json()) assert(pluggable_split_storage.get('not_existing') == None) @@ -178,8 +179,8 @@ def test_fetch_many(self): split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) fetched = pluggable_split_storage.fetch_many([split1.name, split2.name]) assert(fetched[split1.name].to_json() == split1.to_json()) assert(fetched[split2.name].to_json() == split2.to_json()) @@ -217,8 +218,8 @@ def test_get_split_names(self): split2_temp = splits_json['splitChange1_2']['splits'][0].copy() split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) assert(pluggable_split_storage.get_split_names() == [split1.name, split2.name]) def test_get_all(self): @@ -230,11 +231,26 @@ def test_get_all(self): split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) all_splits = pluggable_split_storage.get_all() assert([all_splits[0].to_json(), all_splits[1].to_json()] == [split1.to_json(), split2.to_json()]) + def test_flag_sets(self, mocker): + """Test Flag sets scenarios.""" + self.mock_adapter._keys = {'SPLITIO.flagSet.set1': ['split1'], 'SPLITIO.flagSet.set2': ['split1','split2']} + pluggable_split_storage = PluggableSplitStorage(self.mock_adapter) + assert pluggable_split_storage.flag_set_filter.flag_sets == set({}) + assert sorted(pluggable_split_storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] + + pluggable_split_storage.flag_set_filter = FlagSetsFilter(['set2', 'set3']) + assert pluggable_split_storage.get_feature_flags_by_sets(['set1']) == [] + assert sorted(pluggable_split_storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] + + storage2 = PluggableSplitStorage(self.mock_adapter, None, ['set2', 'set3']) + assert storage2.flag_set_filter.flag_sets == set({'set2', 'set3'}) + + # TODO: To be added when producer mode is aupported # def test_kill_locally(self): # self.mock_adapter._keys = {} @@ -657,12 +673,12 @@ def test_record_config(self): pluggable_telemetry_storage = PluggableTelemetryStorage(self.mock_adapter, self.sdk_metadata, prefix=sprefix) self.config = {} self.extra_config = {} - def record_config_mock(config, extra_config): + def record_config_mock(config, extra_config, fs, ifs): self.config = config self.extra_config = extra_config pluggable_telemetry_storage.record_config = record_config_mock - pluggable_telemetry_storage.record_config({'item': 'value'}, {'item2': 'value2'}) + pluggable_telemetry_storage.record_config({'item': 'value'}, {'item2': 'value2'}, 0, 0) assert(self.config == {'item': 'value'}) assert(self.extra_config == {'item2': 'value2'}) @@ -748,7 +764,7 @@ def test_push_config_stats(self): 'eventsPushRate': 60, 'metricsRefreshRate': 10, 'storageType': None - }, {} + }, {}, 0, 0 ) pluggable_telemetry_storage.record_active_and_redundant_factories(2, 1) pluggable_telemetry_storage.push_config_stats() diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 33fef5a6..1c54a8aa 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -14,7 +14,7 @@ from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MethodExceptionsAndLatencies - +from splitio.storage import FlagSetsFilter class RedisSplitStorageTests(object): """Redis split storage test cases.""" @@ -172,6 +172,20 @@ def test_is_valid_traffic_type_with_cache(self, mocker): time.sleep(1) assert storage.is_valid_traffic_type('any') is False + @mock.patch('splitio.storage.adapters.redis.RedisPipelineAdapter.execute', return_value = [{'split1', 'split2'}]) + def test_flag_sets(self, mocker): + """Test Flag sets scenarios.""" + adapter = build({}) + storage = RedisSplitStorage(adapter, True, 1) + assert storage.flag_set_filter.flag_sets == set({}) + assert sorted(storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] + + storage.flag_set_filter = FlagSetsFilter(['set2', 'set3']) + assert storage.get_feature_flags_by_sets(['set1']) == [] + assert sorted(storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] + + storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) + assert storage2.flag_set_filter.flag_sets == set({'set2', 'set3'}) class RedisSegmentStorageTests(object): """Redis segment storage test cases.""" @@ -399,7 +413,7 @@ def test_init(self, mocker): @mock.patch('splitio.models.telemetry.TelemetryConfig.record_config') def test_record_config(self, mocker): redis_telemetry = RedisTelemetryStorage(mocker.Mock(), mocker.Mock()) - redis_telemetry.record_config(mocker.Mock(), mocker.Mock()) + redis_telemetry.record_config(mocker.Mock(), mocker.Mock(), 0, 0) assert(mocker.called) @mock.patch('splitio.storage.adapters.redis.RedisAdapter.hset') @@ -418,7 +432,7 @@ def test_format_config_stats(self, mocker): 'rF': stats['rF'], 'sT': stats['sT'], 'oM': stats['oM'], - 't': redis_telemetry.pop_config_tags() + 't': redis_telemetry.pop_config_tags(), })) def test_record_active_and_redundant_factories(self, mocker): diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index 9799ba4d..17c88a38 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -3,6 +3,7 @@ import pytest import os import json +import copy from splitio.util.backoff import Backoff from splitio.api import APIException @@ -13,12 +14,51 @@ from splitio.sync.split import SplitSynchronizer, LocalSplitSynchronizer, LocalhostMode from tests.integration import splits_json +splits_raw = [{ + 'changeNumber': 123, + 'trafficTypeName': 'user', + 'name': 'some_name', + 'trafficAllocation': 100, + 'trafficAllocationSeed': 123456, + 'seed': 321654, + 'status': 'ACTIVE', + 'killed': False, + 'defaultTreatment': 'off', + 'algo': 2, + 'conditions': [ + { + 'partitions': [ + {'treatment': 'on', 'size': 50}, + {'treatment': 'off', 'size': 50} + ], + 'contitionType': 'WHITELIST', + 'label': 'some_label', + 'matcherGroup': { + 'matchers': [ + { + 'matcherType': 'WHITELIST', + 'whitelistMatcherData': { + 'whitelist': ['k1', 'k2', 'k3'] + }, + 'negate': False, + } + ], + 'combiner': 'AND' + } + } + ], + 'sets': ['set1', 'set2'] +}] + + class SplitsSynchronizerTests(object): """Split synchronizer test cases.""" + splits = copy.deepcopy(splits_raw) + def test_synchronize_splits_error(self, mocker): """Test that if fetching splits fails at some_point, the task will continue running.""" - storage = mocker.Mock(spec=SplitStorage) + storage = mocker.Mock(spec=InMemorySplitStorage) api = mocker.Mock() def run(x, c): @@ -26,6 +66,15 @@ def run(x, c): run._calls = 0 api.fetch_splits.side_effect = run storage.get_change_number.return_value = -1 + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] split_synchronizer = SplitSynchronizer(api, storage) @@ -34,7 +83,7 @@ def run(x, c): def test_synchronize_splits(self, mocker): """Test split sync.""" - storage = mocker.Mock(spec=SplitStorage) + storage = mocker.Mock(spec=InMemorySplitStorage) def change_number_mock(): change_number_mock._calls += 1 @@ -44,48 +93,23 @@ def change_number_mock(): change_number_mock._calls = 0 storage.get_change_number.side_effect = change_number_mock - api = mocker.Mock() - splits = [{ - 'changeNumber': 123, - 'trafficTypeName': 'user', - 'name': 'some_name', - 'trafficAllocation': 100, - 'trafficAllocationSeed': 123456, - 'seed': 321654, - 'status': 'ACTIVE', - 'killed': False, - 'defaultTreatment': 'off', - 'algo': 2, - 'conditions': [ - { - 'partitions': [ - {'treatment': 'on', 'size': 50}, - {'treatment': 'off', 'size': 50} - ], - 'contitionType': 'WHITELIST', - 'label': 'some_label', - 'matcherGroup': { - 'matchers': [ - { - 'matcherType': 'WHITELIST', - 'whitelistMatcherData': { - 'whitelist': ['k1', 'k2', 'k3'] - }, - 'negate': False, - } - ], - 'combiner': 'AND' - } - } - ] - }] + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] + api = mocker.Mock() def get_changes(*args, **kwargs): get_changes.called += 1 if get_changes.called == 1: return { - 'splits': splits, + 'splits': self.splits, 'since': -1, 'till': 123 } @@ -101,16 +125,27 @@ def get_changes(*args, **kwargs): split_synchronizer = SplitSynchronizer(api, storage) split_synchronizer.synchronize_splits() - assert mocker.call(-1, FetchOptions(True)) in api.fetch_splits.mock_calls - assert mocker.call(123, FetchOptions(True)) in api.fetch_splits.mock_calls + assert api.fetch_splits.mock_calls[0][1][0] == -1 + assert api.fetch_splits.mock_calls[0][1][1].cache_control_headers == True + assert api.fetch_splits.mock_calls[1][1][0] == 123 + assert api.fetch_splits.mock_calls[1][1][1].cache_control_headers == True - inserted_split = storage.put.mock_calls[0][1][0] + inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' def test_not_called_on_till(self, mocker): """Test that sync is not called when till is less than previous changenumber""" - storage = mocker.Mock(spec=SplitStorage) + storage = mocker.Mock(spec=InMemorySplitStorage) + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] def change_number_mock(): return 2 @@ -134,7 +169,7 @@ def test_synchronize_splits_cdn(self, mocker): """Test split sync with bypassing cdn.""" mocker.patch('splitio.sync.split._ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES', new=3) - storage = mocker.Mock(spec=SplitStorage) + storage = mocker.Mock(spec=InMemorySplitStorage) def change_number_mock(): change_number_mock._calls += 1 @@ -149,45 +184,10 @@ def change_number_mock(): storage.get_change_number.side_effect = change_number_mock api = mocker.Mock() - splits = [{ - 'changeNumber': 123, - 'trafficTypeName': 'user', - 'name': 'some_name', - 'trafficAllocation': 100, - 'trafficAllocationSeed': 123456, - 'seed': 321654, - 'status': 'ACTIVE', - 'killed': False, - 'defaultTreatment': 'off', - 'algo': 2, - 'conditions': [ - { - 'partitions': [ - {'treatment': 'on', 'size': 50}, - {'treatment': 'off', 'size': 50} - ], - 'contitionType': 'WHITELIST', - 'label': 'some_label', - 'matcherGroup': { - 'matchers': [ - { - 'matcherType': 'WHITELIST', - 'whitelistMatcherData': { - 'whitelist': ['k1', 'k2', 'k3'] - }, - 'negate': False, - } - ], - 'combiner': 'AND' - } - } - ] - }] - def get_changes(*args, **kwargs): get_changes.called += 1 if get_changes.called == 1: - return { 'splits': splits, 'since': -1, 'till': 123 } + return { 'splits': self.splits, 'since': -1, 'till': 123 } elif get_changes.called == 2: return { 'splits': [], 'since': 123, 'till': 123 } elif get_changes.called == 3: @@ -200,25 +200,123 @@ def get_changes(*args, **kwargs): get_changes.called = 0 api.fetch_splits.side_effect = get_changes + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] + split_synchronizer = SplitSynchronizer(api, storage) split_synchronizer._backoff = Backoff(1, 1) split_synchronizer.synchronize_splits() - assert mocker.call(-1, FetchOptions(True)) in api.fetch_splits.mock_calls - assert mocker.call(123, FetchOptions(True)) in api.fetch_splits.mock_calls + assert api.fetch_splits.mock_calls[0][1][0] == -1 + assert api.fetch_splits.mock_calls[0][1][1].cache_control_headers == True + assert api.fetch_splits.mock_calls[1][1][0] == 123 + assert api.fetch_splits.mock_calls[1][1][1].cache_control_headers == True split_synchronizer._backoff = Backoff(1, 0.1) split_synchronizer.synchronize_splits(12345) - assert mocker.call(12345, FetchOptions(True, 1234)) in api.fetch_splits.mock_calls + assert api.fetch_splits.mock_calls[3][1][0] == 1234 + assert api.fetch_splits.mock_calls[3][1][1].cache_control_headers == True assert len(api.fetch_splits.mock_calls) == 8 # 2 ok + BACKOFF(2 since==till + 2 re-attempts) + CDN(2 since==till) - inserted_split = storage.put.mock_calls[0][1][0] + inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' + def test_sync_flag_sets_with_config_sets(self, mocker): + """Test split sync with flag sets.""" + storage = InMemorySplitStorage(['set1', 'set2']) + + split = self.splits[0].copy() + split['name'] = 'second' + splits1 = [self.splits[0].copy(), split] + splits2 = self.splits.copy() + splits3 = self.splits.copy() + splits4 = self.splits.copy() + api = mocker.Mock() + def get_changes(*args, **kwargs): + get_changes.called += 1 + if get_changes.called == 1: + return { 'splits': splits1, 'since': 123, 'till': 123 } + elif get_changes.called == 2: + splits2[0]['sets'] = ['set3'] + return { 'splits': splits2, 'since': 124, 'till': 124 } + elif get_changes.called == 3: + splits3[0]['sets'] = ['set1'] + return { 'splits': splits3, 'since': 12434, 'till': 12434 } + splits4[0]['sets'] = ['set6'] + splits4[0]['name'] = 'new_split' + return { 'splits': splits4, 'since': 12438, 'till': 12438 } + get_changes.called = 0 + api.fetch_splits.side_effect = get_changes + + split_synchronizer = SplitSynchronizer(api, storage) + split_synchronizer._backoff = Backoff(1, 1) + split_synchronizer.synchronize_splits() + assert isinstance(storage.get('some_name'), Split) + + split_synchronizer.synchronize_splits(124) + assert storage.get('some_name') == None + + split_synchronizer.synchronize_splits(12434) + assert isinstance(storage.get('some_name'), Split) + + split_synchronizer.synchronize_splits(12438) + assert storage.get('new_name') == None + + def test_sync_flag_sets_without_config_sets(self, mocker): + """Test split sync with flag sets.""" + storage = InMemorySplitStorage() + + split = self.splits[0].copy() + split['name'] = 'second' + splits1 = [self.splits[0].copy(), split] + splits2 = self.splits.copy() + splits3 = self.splits.copy() + splits4 = self.splits.copy() + api = mocker.Mock() + def get_changes(*args, **kwargs): + get_changes.called += 1 + if get_changes.called == 1: + return { 'splits': splits1, 'since': 123, 'till': 123 } + elif get_changes.called == 2: + splits2[0]['sets'] = ['set3'] + return { 'splits': splits2, 'since': 124, 'till': 124 } + elif get_changes.called == 3: + splits3[0]['sets'] = ['set1'] + return { 'splits': splits3, 'since': 12434, 'till': 12434 } + splits4[0]['sets'] = ['set6'] + splits4[0]['name'] = 'third_split' + return { 'splits': splits4, 'since': 12438, 'till': 12438 } + get_changes.called = 0 + api.fetch_splits.side_effect = get_changes + + split_synchronizer = SplitSynchronizer(api, storage) + split_synchronizer._backoff = Backoff(1, 1) + split_synchronizer.synchronize_splits() + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(124) + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(12434) + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(12438) + assert isinstance(storage.get('third_split'), Split) + class LocalSplitsSynchronizerTests(object): """Split synchronizer test cases.""" + splits = copy.deepcopy(splits_raw) + def test_synchronize_splits_error(self, mocker): """Test that if fetching splits fails at some_point, the task will continue running.""" storage = mocker.Mock(spec=SplitStorage) @@ -232,80 +330,127 @@ def test_synchronize_splits(self, mocker): storage = InMemorySplitStorage() till = 123 - splits = [{ - 'changeNumber': 123, - 'trafficTypeName': 'user', - 'name': 'some_name', - 'trafficAllocation': 100, - 'trafficAllocationSeed': 123456, - 'seed': 321654, - 'status': 'ACTIVE', - 'killed': False, - 'defaultTreatment': 'off', - 'algo': 2, - 'conditions': [ - { - 'partitions': [ - {'treatment': 'on', 'size': 50}, - {'treatment': 'off', 'size': 50} - ], - 'contitionType': 'WHITELIST', - 'label': 'some_label', - 'matcherGroup': { - 'matchers': [ - { - 'matcherType': 'WHITELIST', - 'whitelistMatcherData': { - 'whitelist': ['k1', 'k2', 'k3'] - }, - 'negate': False, - } - ], - 'combiner': 'AND' - } - } - ] - }] - def read_feature_flags_from_json_file(*args, **kwargs): - return splits, till + return self.splits, till split_synchronizer = LocalSplitSynchronizer("split.json", storage, LocalhostMode.JSON) split_synchronizer._read_feature_flags_from_json_file = read_feature_flags_from_json_file split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' # Should sync when changenumber is not changed - splits[0]['killed'] = True + self.splits[0]['killed'] = True split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert inserted_split.killed # Should not sync when changenumber is less than stored till = 122 - splits[0]['killed'] = False + self.splits[0]['killed'] = False split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert inserted_split.killed # Should sync when changenumber is higher than stored till = 124 split_synchronizer._current_json_sha = "-1" split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert inserted_split.killed == False # Should sync when till is default (-1) till = -1 split_synchronizer._current_json_sha = "-1" - splits[0]['killed'] = True + self.splits[0]['killed'] = True split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert inserted_split.killed == True + def test_sync_flag_sets_with_config_sets(self, mocker): + """Test split sync with flag sets.""" + storage = InMemorySplitStorage(['set1', 'set2']) + + split = self.splits[0].copy() + split['name'] = 'second' + splits1 = [self.splits[0].copy(), split] + splits2 = self.splits.copy() + splits3 = self.splits.copy() + splits4 = self.splits.copy() + + self.called = 0 + def read_feature_flags_from_json_file(*args, **kwargs): + self.called += 1 + if self.called == 1: + return splits1, 123 + elif self.called == 2: + splits2[0]['sets'] = ['set3'] + return splits2, 124 + elif self.called == 3: + splits3[0]['sets'] = ['set1'] + return splits3, 12434 + splits4[0]['sets'] = ['set6'] + splits4[0]['name'] = 'new_split' + return splits4, 12438 + + split_synchronizer = LocalSplitSynchronizer("split.json", storage, LocalhostMode.JSON) + split_synchronizer._read_feature_flags_from_json_file = read_feature_flags_from_json_file + + split_synchronizer.synchronize_splits() + assert isinstance(storage.get('some_name'), Split) + + split_synchronizer.synchronize_splits(124) + assert storage.get('some_name') == None + + split_synchronizer.synchronize_splits(12434) + assert isinstance(storage.get('some_name'), Split) + + split_synchronizer.synchronize_splits(12438) + assert storage.get('new_name') == None + + def test_sync_flag_sets_without_config_sets(self, mocker): + """Test split sync with flag sets.""" + storage = InMemorySplitStorage() + + split = self.splits[0].copy() + split['name'] = 'second' + splits1 = [self.splits[0].copy(), split] + splits2 = self.splits.copy() + splits3 = self.splits.copy() + splits4 = self.splits.copy() + + self.called = 0 + def read_feature_flags_from_json_file(*args, **kwargs): + self.called += 1 + if self.called == 1: + return splits1, 123 + elif self.called == 2: + splits2[0]['sets'] = ['set3'] + return splits2, 124 + elif self.called == 3: + splits3[0]['sets'] = ['set1'] + return splits3, 12434 + splits4[0]['sets'] = ['set6'] + splits4[0]['name'] = 'third_split' + return splits4, 12438 + + split_synchronizer = LocalSplitSynchronizer("split.json", storage, LocalhostMode.JSON) + split_synchronizer._read_feature_flags_from_json_file = read_feature_flags_from_json_file + + split_synchronizer.synchronize_splits() + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(124) + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(12434) + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(12438) + assert isinstance(storage.get('third_split'), Split) + def test_reading_json(self, mocker): """Test reading json file.""" f = open("./splits.json", "w") @@ -341,7 +486,8 @@ def test_reading_json(self, mocker): 'combiner': 'AND' } } - ] + ], + 'sets': ['set1'] }], "till":1675095324253, "since":-1, diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index c57c9453..592543fd 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -2,7 +2,7 @@ from turtle import clear import unittest.mock as mock - +import pytest from splitio.sync.synchronizer import Synchronizer, SplitTasks, SplitSynchronizers, LocalhostSynchronizer from splitio.tasks.split_sync import SplitSynchronizationTask from splitio.tasks.unique_keys_sync import UniqueKeysSyncTask, ClearFilterSyncTask @@ -23,6 +23,14 @@ class SynchronizerTests(object): def test_sync_all_failed_splits(self, mocker): api = mocker.Mock() storage = mocker.Mock() + class flag_set_filter(): + def should_filter(): + return False + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] def run(x, c): raise APIException("something broke") @@ -38,6 +46,34 @@ def run(x, c): # test forcing to have only one retry attempt and then exit sychronizer.sync_all(1) # sync_all should not throw! + def test_sync_all_failed_splits_with_flagsets(self, mocker): + api = mocker.Mock() + storage = mocker.Mock() + class flag_set_filter(): + def should_filter(): + return False + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] + + def run(x, c): + raise APIException("something broke", 414) + api.fetch_splits.side_effect = run + + split_sync = SplitSynchronizer(api, storage) + split_synchronizers = SplitSynchronizers(split_sync, mocker.Mock(), mocker.Mock(), + mocker.Mock(), mocker.Mock()) + synchronizer = Synchronizer(split_synchronizers, mocker.Mock(spec=SplitTasks)) + + synchronizer.synchronize_splits(None) # APIExceptions are handled locally and should not be propagated! + + # test forcing to have only one retry attempt and then exit + synchronizer.sync_all(3) # sync_all should not throw! + assert synchronizer._break_sync_all + assert synchronizer._backoff._attempt == 0 + def test_sync_all_failed_segments(self, mocker): api = mocker.Mock() storage = mocker.Mock() @@ -141,6 +177,15 @@ def test_sync_all(self, mocker): split_storage = mocker.Mock(spec=SplitStorage) split_storage.get_change_number.return_value = 123 split_storage.get_segment_names.return_value = ['segmentA'] + class flag_set_filter(): + def should_filter(): + return False + def intersect(sets): + return True + split_storage.flag_set_filter = flag_set_filter + split_storage.flag_set_filter.flag_sets = {} + split_storage.flag_set_filter.sorted_flag_sets = [] + split_api = mocker.Mock() split_api.fetch_splits.return_value = {'splits': self.splits, 'since': 123, 'till': 123} @@ -159,7 +204,7 @@ def test_sync_all(self, mocker): synchronizer = Synchronizer(split_synchronizers, mocker.Mock(spec=SplitTasks)) synchronizer.sync_all() - inserted_split = split_storage.put.mock_calls[0][1][0] + inserted_split = split_storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' diff --git a/tests/sync/test_telemetry.py b/tests/sync/test_telemetry.py index 11257d0f..9ce82cc7 100644 --- a/tests/sync/test_telemetry.py +++ b/tests/sync/test_telemetry.py @@ -1,6 +1,8 @@ """Telemetry Worker tests.""" import unittest.mock as mock import json +import pytest + from splitio.sync.telemetry import TelemetrySynchronizer, InMemoryTelemetrySubmitter from splitio.engine.telemetry import TelemetryEvaluationConsumer, TelemetryInitConsumer, TelemetryRuntimeConsumer, TelemetryStorageConsumer from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemorySegmentStorage, InMemorySplitStorage @@ -32,7 +34,7 @@ def test_synchronize_telemetry(self, mocker): telemetry_storage = InMemoryTelemetryStorage() telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) split_storage = InMemorySplitStorage() - split_storage.put(Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)) + split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], 123) segment_storage = InMemorySegmentStorage() segment_storage.put(Segment('segment1', [], 123)) telemetry_submitter = InMemoryTelemetrySubmitter(telemetry_consumer, split_storage, segment_storage, api) @@ -51,6 +53,10 @@ def test_synchronize_telemetry(self, mocker): telemetry_storage._method_exceptions._treatments = 1 telemetry_storage._method_exceptions._treatment_with_config = 5 telemetry_storage._method_exceptions._treatments_with_config = 1 + telemetry_storage._method_exceptions._treatments_by_flag_set = 2 + telemetry_storage._method_exceptions._treatments_by_flag_sets = 3 + telemetry_storage._method_exceptions._treatments_with_config_by_flag_set = 4 + telemetry_storage._method_exceptions._treatments_with_config_by_flag_sets = 6 telemetry_storage._method_exceptions._track = 3 telemetry_storage._last_synchronization._split = 5 @@ -76,6 +82,10 @@ def test_synchronize_telemetry(self, mocker): telemetry_storage._method_latencies._treatments = [0] * 23 telemetry_storage._method_latencies._treatment_with_config = [0] * 23 telemetry_storage._method_latencies._treatments_with_config = [0] * 23 + telemetry_storage._method_latencies._treatments_by_flag_set = [1] + [0] * 22 + telemetry_storage._method_latencies._treatments_by_flag_sets = [0] * 23 + telemetry_storage._method_latencies._treatments_with_config_by_flag_set = [1] + [0] * 22 + telemetry_storage._method_latencies._treatments_with_config_by_flag_sets = [0] * 23 telemetry_storage._method_latencies._track = [0] * 23 telemetry_storage._http_latencies._split = [1] + [0] * 22 @@ -101,7 +111,7 @@ def test_synchronize_telemetry(self, mocker): 'activeFactoryCount': 1, 'notReady': 0, 'timeUntilReady': 1 - }, {} + }, {}, 0, 0 ) self.formatted_config = "" def record_init(*args, **kwargs): @@ -130,8 +140,8 @@ def record_stats(*args, **kwargs): "tR": 3, "sE": [], "sL": 3, - "mE": {"t": 10, "ts": 1, "tc": 5, "tcs": 1, "tr": 3}, - "mL": {"t": [1] + [0] * 22, "ts": [0] * 23, "tc": [0] * 23, "tcs": [0] * 23, "tr": [0] * 23}, + "mE": {"t": 10, "ts": 1, "tc": 5, "tcs": 1, "tf": 2, "tfs": 3, "tcf": 4, "tcfs": 6, "tr": 3}, + "mL": {"t": [1] + [0] * 22, "ts": [0] * 23, "tc": [0] * 23, "tcs": [0] * 23, "tf": [1] + [0] * 22, "tfs": [0] * 23, "tcf": [1] + [0] * 22, "tcfs": [0] * 23, "tr": [0] * 23}, "spC": 1, "seC": 1, "skC": 0, diff --git a/tests/tasks/test_split_sync.py b/tests/tasks/test_split_sync.py index adc90724..104bbccc 100644 --- a/tests/tasks/test_split_sync.py +++ b/tests/tasks/test_split_sync.py @@ -25,6 +25,16 @@ def change_number_mock(): change_number_mock._calls = 0 storage.get_change_number.side_effect = change_number_mock + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] + api = mocker.Mock() splits = [{ 'changeNumber': 123, @@ -89,10 +99,12 @@ def get_changes(*args, **kwargs): task.stop(stop_event) stop_event.wait() assert not task.is_running() - assert mocker.call(-1, fetch_options) in api.fetch_splits.mock_calls - assert mocker.call(123, fetch_options) in api.fetch_splits.mock_calls + assert api.fetch_splits.mock_calls[0][1][0] == -1 + assert api.fetch_splits.mock_calls[0][1][1].cache_control_headers == True + assert api.fetch_splits.mock_calls[1][1][0] == 123 + assert api.fetch_splits.mock_calls[1][1][1].cache_control_headers == True - inserted_split = storage.put.mock_calls[0][1][0] + inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py new file mode 100644 index 00000000..7608306d --- /dev/null +++ b/tests/util/test_storage_helper.py @@ -0,0 +1,129 @@ +"""Storage Helper tests.""" +import pytest + +from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets +from splitio.storage.inmemmory import InMemorySplitStorage +from splitio.models import splits +from splitio.storage import FlagSetsFilter +from tests.sync.test_splits_synchronizer import splits_raw as split_sample + +class StorageHelperTests(object): + + def test_update_feature_flag_storage(self, mocker): + storage = mocker.Mock(spec=InMemorySplitStorage) + split = splits.from_raw(split_sample[0]) + + self.added = [] + self.deleted = [] + self.change_number = 0 + def update(to_add, to_delete, change_number): + self.added = to_add + self.deleted = to_delete + self.change_number = change_number + storage.update = update + + def is_flag_set_exist(flag_set): + return False + storage.is_flag_set_exist = is_flag_set_exist + + class flag_set_filter(): + def should_filter(): + return False + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + + update_feature_flag_storage(storage, [split], 123) + assert self.added[0] == split + assert self.deleted == [] + assert self.change_number == 123 + + class flag_set_filter2(): + def should_filter(): + return True + def intersect(sets): + return False + storage.flag_set_filter = flag_set_filter2 + storage.flag_set_filter.flag_sets = set({'set1', 'set2'}) + + update_feature_flag_storage(storage, [split], 123) + assert self.added == [] + assert self.deleted[0] == split.name + + class flag_set_filter3(): + def should_filter(): + return True + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter3 + storage.flag_set_filter.flag_sets = set({'set1', 'set2'}) + + def is_flag_set_exist2(flag_set): + return True + storage.is_flag_set_exist = is_flag_set_exist2 + update_feature_flag_storage(storage, [split], 123) + assert self.added[0] == split + assert self.deleted == [] + + split_json = split_sample[0] + split_json['conditions'].append({ + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": False, + "userDefinedSegmentMatcherData": { + "segmentName": "segment1" + }, + "whitelistMatcherData": None + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 30 + }, + { + "treatment": "off", + "size": 70 + } + ] + } + ) + + split = splits.from_raw(split_json) + storage.config_flag_sets_used = 0 + assert update_feature_flag_storage(storage, [split], 123) == {'segment1'} + + def test_get_valid_flag_sets(self): + flag_sets = ['set1', 'set2'] + config_flag_sets = FlagSetsFilter([]) + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1', 'set2'] + + config_flag_sets = FlagSetsFilter(['set1']) + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1'] + + flag_sets = ['set2', 'set3'] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set2'] + + flag_sets = ['set3', 'set4'] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) + assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] + + flag_sets = [] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) + assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] + + def test_combine_valid_flag_sets(self): + results_set = [{'set1', 'set2'}, {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set1', 'set2', 'set3'} + + results_set = [{}, {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} + + results_set = ['set1', {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} \ No newline at end of file