diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 2cd80dde..57ec558c 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -26,6 +26,7 @@ class ConditionOperatorTypes(object): AND = 'and' OR = 'or' NOT = 'not' + operators = [AND, OR, NOT] class ConditionMatchTypes(object): diff --git a/optimizely/lib/pymmh3.py b/optimizely/lib/pymmh3.py index 0f107709..4997de21 100755 --- a/optimizely/lib/pymmh3.py +++ b/optimizely/lib/pymmh3.py @@ -127,25 +127,25 @@ def fmix(k): for block_start in xrange(0, nblocks * 8, 8): # ??? big endian? k1 = ( - key[2 * block_start + 7] << 56 - | key[2 * block_start + 6] << 48 - | key[2 * block_start + 5] << 40 - | key[2 * block_start + 4] << 32 - | key[2 * block_start + 3] << 24 - | key[2 * block_start + 2] << 16 - | key[2 * block_start + 1] << 8 - | key[2 * block_start + 0] + key[2 * block_start + 7] << 56 | + key[2 * block_start + 6] << 48 | + key[2 * block_start + 5] << 40 | + key[2 * block_start + 4] << 32 | + key[2 * block_start + 3] << 24 | + key[2 * block_start + 2] << 16 | + key[2 * block_start + 1] << 8 | + key[2 * block_start + 0] ) k2 = ( - key[2 * block_start + 15] << 56 - | key[2 * block_start + 14] << 48 - | key[2 * block_start + 13] << 40 - | key[2 * block_start + 12] << 32 - | key[2 * block_start + 11] << 24 - | key[2 * block_start + 10] << 16 - | key[2 * block_start + 9] << 8 - | key[2 * block_start + 8] + key[2 * block_start + 15] << 56 | + key[2 * block_start + 14] << 48 | + key[2 * block_start + 13] << 40 | + key[2 * block_start + 12] << 32 | + key[2 * block_start + 11] << 24 | + key[2 * block_start + 10] << 16 | + key[2 * block_start + 9] << 8 | + key[2 * block_start + 8] ) k1 = (c1 * k1) & 0xFFFFFFFFFFFFFFFF @@ -258,31 +258,31 @@ def fmix(h): # body for block_start in xrange(0, nblocks * 16, 16): k1 = ( - key[block_start + 3] << 24 - | key[block_start + 2] << 16 - | key[block_start + 1] << 8 - | key[block_start + 0] + key[block_start + 3] << 24 | + key[block_start + 2] << 16 | + key[block_start + 1] << 8 | + key[block_start + 0] ) k2 = ( - key[block_start + 7] << 24 - | key[block_start + 6] << 16 - | key[block_start + 5] << 8 - | key[block_start + 4] + key[block_start + 7] << 24 | + key[block_start + 6] << 16 | + key[block_start + 5] << 8 | + key[block_start + 4] ) k3 = ( - key[block_start + 11] << 24 - | key[block_start + 10] << 16 - | key[block_start + 9] << 8 - | key[block_start + 8] + key[block_start + 11] << 24 | + key[block_start + 10] << 16 | + key[block_start + 9] << 8 | + key[block_start + 8] ) k4 = ( - key[block_start + 15] << 24 - | key[block_start + 14] << 16 - | key[block_start + 13] << 8 - | key[block_start + 12] + key[block_start + 15] << 24 | + key[block_start + 14] << 16 | + key[block_start + 13] << 8 | + key[block_start + 12] ) k1 = (c1 * k1) & 0xFFFFFFFF diff --git a/optimizely/optimizely_config.py b/optimizely/optimizely_config.py index f204263a..11cc4b5c 100644 --- a/optimizely/optimizely_config.py +++ b/optimizely/optimizely_config.py @@ -12,13 +12,15 @@ # limitations under the License. import copy +from .helpers.condition import ConditionOperatorTypes from .project_config import ProjectConfig class OptimizelyConfig(object): def __init__(self, revision, experiments_map, features_map, datafile=None, - sdk_key=None, environment_key=None, attributes=None, events=None): + sdk_key=None, environment_key=None, attributes=None, events=None, + audiences=None): self.revision = revision self.experiments_map = experiments_map self.features_map = features_map @@ -27,6 +29,7 @@ def __init__(self, revision, experiments_map, features_map, datafile=None, self.environment_key = environment_key self.attributes = attributes or [] self.events = events or [] + self.audiences = audiences or [] def get_datafile(self): """ Get the datafile associated with OptimizelyConfig. @@ -36,44 +39,13 @@ def get_datafile(self): """ return self._datafile - def get_sdk_key(self): - """ Get the sdk key associated with OptimizelyConfig. - - Returns: - A string containing sdk key. - """ - return self.sdk_key - - def get_environment_key(self): - """ Get the environemnt key associated with OptimizelyConfig. - - Returns: - A string containing environment key. - """ - return self.environment_key - - def get_attributes(self): - """ Get the attributes associated with OptimizelyConfig - - returns: - A list of attributes. - """ - return self.attributes - - def get_events(self): - """ Get the events associated with OptimizelyConfig - - returns: - A list of events. - """ - return self.events - class OptimizelyExperiment(object): - def __init__(self, id, key, variations_map): + def __init__(self, id, key, variations_map, audiences=''): self.id = id self.key = key self.variations_map = variations_map + self.audiences = audiences class OptimizelyFeature(object): @@ -82,6 +54,8 @@ def __init__(self, id, key, experiments_map, variables_map): self.key = key self.experiments_map = experiments_map self.variables_map = variables_map + self.delivery_rules = [] + self.experiment_rules = [] class OptimizelyVariation(object): @@ -113,6 +87,13 @@ def __init__(self, id, key, experiment_ids): self.experiment_ids = experiment_ids +class OptimizelyAudience(object): + def __init__(self, id, name, conditions): + self.id = id + self.name = name + self.conditions = conditions + + class OptimizelyConfigService(object): """ Class encapsulating methods to be used in creating instance of OptimizelyConfig. """ @@ -136,9 +117,121 @@ def __init__(self, project_config): self.environment_key = project_config.environment_key self.attributes = project_config.attributes self.events = project_config.events + self.rollouts = project_config.rollouts self._create_lookup_maps() + ''' + Merging typed_audiences with audiences from project_config. + The typed_audiences has higher presidence. + ''' + + typed_audiences = project_config.typed_audiences[:] + optly_typed_audiences = [] + for typed_audience in typed_audiences: + optly_audience = OptimizelyAudience( + typed_audience.get('id'), + typed_audience.get('name'), + typed_audience.get('conditions') + ) + optly_typed_audiences.append(optly_audience) + + for old_audience in project_config.audiences: + # check if old_audience.id exists in new_audiences.id from typed_audiences + if len([new_audience for new_audience in project_config.typed_audiences + if new_audience.get('id') == old_audience.get('id')]) == 0: + if old_audience.get('id') == "$opt_dummy_audience": + continue + else: + # Convert audiences lists to OptimizelyAudience array + optly_audience = OptimizelyAudience( + old_audience.get('id'), + old_audience.get('name'), + old_audience.get('conditions') + ) + optly_typed_audiences.append(optly_audience) + + self.audiences = optly_typed_audiences + + def replace_ids_with_names(self, conditions, audiences_map): + ''' + Gets conditions and audiences_map [id:name] + + Returns: + a string of conditions with id's swapped with names + or empty string if no conditions found. + + ''' + if conditions is not None: + return self.stringify_conditions(conditions, audiences_map) + else: + return '' + + def lookup_name_from_id(self, audience_id, audiences_map): + ''' + Gets and audience ID and audiences map + + Returns: + The name corresponding to the ID + or '' if not found. + ''' + name = None + try: + name = audiences_map[audience_id] + except KeyError: + name = audience_id + + return name + + def stringify_conditions(self, conditions, audiences_map): + ''' + Gets a list of conditions from an entities.Experiment + and an audiences_map [id:name] + + Returns: + A string of conditions and names for the provided + list of conditions. + ''' + ARGS = ConditionOperatorTypes.operators + condition = 'OR' + conditions_str = '' + length = len(conditions) + + if length == 0: + return '' + if length == 1: + return '"' + self.lookup_name_from_id(conditions[0], audiences_map) + '"' + if length == 2 and conditions[0] in ARGS and \ + type(conditions[1]) is not list and \ + conditions[1] not in ARGS: + if conditions[0] != "not": + return '"' + self.lookup_name_from_id(conditions[1], audiences_map) + '"' + else: + return conditions[0].upper() + \ + ' "' + self.lookup_name_from_id(conditions[1], audiences_map) + '"' + if length > 1: + for i in range(length): + if conditions[i] in ARGS: + condition = conditions[i].upper() + else: + if type(conditions[i]) == list: + if i + 1 < length: + conditions_str += '(' + self.stringify_conditions(conditions[i], audiences_map) + ') ' + else: + conditions_str += condition + \ + ' (' + self.stringify_conditions(conditions[i], audiences_map) + ')' + else: + audience_name = self.lookup_name_from_id(conditions[i], audiences_map) + if audience_name is not None: + if i + 1 < length - 1: + conditions_str += '"' + audience_name + '" ' + condition + ' ' + elif i + 1 == length: + conditions_str += condition + ' "' + audience_name + '"' + else: + conditions_str += '"' + audience_name + '" ' + + return conditions_str or '' + def get_config(self): """ Gets instance of OptimizelyConfig @@ -159,8 +252,10 @@ def get_config(self): self._datafile, self.sdk_key, self.environment_key, - self.attributes, - self.events) + self._get_attributes_list(self.attributes), + self._get_events_list(self.events), + self.audiences + ) def _create_lookup_maps(self): """ Creates lookup maps to avoid redundant iteration of config objects. """ @@ -248,7 +343,8 @@ def _get_all_experiments(self): return experiments def _get_experiments_maps(self): - """ Gets maps for all the experiments in the project config. + """ Gets maps for all the experiments in the project config and + updates the experiment with updated experiment audiences string. Returns: dict, dict -- experiment key/id to OptimizelyExperiment maps. @@ -257,12 +353,21 @@ def _get_experiments_maps(self): experiments_key_map = {} # Id map comes in handy to figure out feature experiment. experiments_id_map = {} + # Audiences map to use for updating experiments with new audience conditions string + audiences_map = {} + + # Build map from OptimizelyAudience array + for optly_audience in self.audiences: + audiences_map[optly_audience.id] = optly_audience.name all_experiments = self._get_all_experiments() for exp in all_experiments: optly_exp = OptimizelyExperiment( exp['id'], exp['key'], self._get_variations_map(exp) ) + # Updating each OptimizelyExperiment + audiences = self.replace_ids_with_names(exp.get('audienceConditions', []), audiences_map) + optly_exp.audiences = audiences or '' experiments_key_map[exp['key']] = optly_exp experiments_id_map[exp['id']] = optly_exp @@ -279,19 +384,96 @@ def _get_features_map(self, experiments_id_map): dict -- feaure key to OptimizelyFeature map """ features_map = {} + experiment_rules = [] for feature in self.feature_flags: + + delivery_rules = self._get_delivery_rules(self.rollouts, feature.get('rolloutId')) + experiment_rules = [] + exp_map = {} for experiment_id in feature.get('experimentIds', []): optly_exp = experiments_id_map[experiment_id] exp_map[optly_exp.key] = optly_exp + experiment_rules.append(optly_exp) variables_map = self.feature_key_variable_key_to_variable_map[feature['key']] optly_feature = OptimizelyFeature( feature['id'], feature['key'], exp_map, variables_map ) + optly_feature.experiment_rules = experiment_rules + optly_feature.delivery_rules = delivery_rules features_map[feature['key']] = optly_feature return features_map + + def _get_delivery_rules(self, rollouts, rollout_id): + """ Gets an array of rollouts for the project config + + returns: + an array of OptimizelyExperiments as delivery rules. + """ + # Return list for delivery rules + delivery_rules = [] + # Audiences map to use for updating experiments with new audience conditions string + audiences_map = {} + + # Gets a rollout based on provided rollout_id + rollout = [rollout for rollout in rollouts if rollout.get('id') == rollout_id] + + if rollout: + rollout = rollout[0] + # Build map from OptimizelyAudience array + for optly_audience in self.audiences: + audiences_map[optly_audience.id] = optly_audience.name + + # Get the experiments_map for that rollout + experiments = rollout.get('experiments_map') + if experiments: + for experiment in experiments: + optly_exp = OptimizelyExperiment( + experiment['id'], experiment['key'], self._get_variations_map(experiment) + ) + audiences = self.replace_ids_with_names(experiment.get('audienceConditions', []), audiences_map) + optly_exp.audiences = audiences + + delivery_rules.append(optly_exp) + + return delivery_rules + + def _get_attributes_list(self, attributes): + """ Gets attributes list for the project config + + Returns: + List - OptimizelyAttributes + """ + attributes_list = [] + + for attribute in attributes: + optly_attribute = OptimizelyAttribute( + attribute['id'], + attribute['key'] + ) + attributes_list.append(optly_attribute) + + return attributes_list + + def _get_events_list(self, events): + """ Gets events list for the project_config + + Returns: + List - OptimizelyEvents + """ + events_list = [] + + for event in events: + optly_event = OptimizelyEvent( + event['id'], + event['key'], + event['experimentIds'] + ) + events_list.append(optly_event) + + return events_list diff --git a/tests/base.py b/tests/base.py index 48b89106..05127caf 100644 --- a/tests/base.py +++ b/tests/base.py @@ -756,7 +756,8 @@ def setUp(self, config_dict='config_dict'): 'projectId': '11624721371', 'variables': [], 'featureFlags': [ - {'experimentIds': [], 'rolloutId': '11551226731', 'variables': [], 'id': '11477755619', 'key': 'feat'}, + {'experimentIds': [], 'rolloutId': '11551226731', 'variables': [], 'id': '11477755619', + 'key': 'feat'}, { 'experimentIds': ['11564051718'], 'rolloutId': '11638870867', diff --git a/tests/test_optimizely_config.py b/tests/test_optimizely_config.py index 29bd2443..adbace59 100644 --- a/tests/test_optimizely_config.py +++ b/tests/test_optimizely_config.py @@ -13,7 +13,7 @@ import json -from optimizely import optimizely +from optimizely import optimizely, project_config from optimizely import optimizely_config from . import base @@ -29,7 +29,27 @@ def setUp(self): 'sdk_key': None, 'environment_key': None, 'attributes': [{'key': 'test_attribute', 'id': '111094'}], - 'events': [{'key': 'test_event', 'experimentIds': ['111127'], 'id': '111095'}], + 'events': [{'key': 'test_event', 'experiment_ids': ['111127'], 'id': '111095'}], + 'audiences': [ + { + 'name': 'Test attribute users 1', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', + 'id': '11154' + }, + { + 'name': 'Test attribute users 2', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', + 'id': '11159' + }, + { + 'name': 'Test attribute users 3', + 'conditions': "[\"and\", [\"or\", [\"or\", {\"match\": \"exact\", \"name\": \ + \"experiment_attr\", \"type\": \"custom_attribute\", \"value\": \"group_experiment\"}]]]", + 'id': '11160', + } + ], 'experiments_map': { 'test_experiment2': { 'variations_map': { @@ -51,7 +71,8 @@ def setUp(self): } }, 'id': '111133', - 'key': 'test_experiment2' + 'key': 'test_experiment2', + 'audiences': '' }, 'test_experiment': { 'variations_map': { @@ -155,7 +176,8 @@ def setUp(self): } }, 'id': '111127', - 'key': 'test_experiment' + 'key': 'test_experiment', + 'audiences': '' }, 'group_exp_1': { 'variations_map': { @@ -177,7 +199,8 @@ def setUp(self): } }, 'id': '32222', - 'key': 'group_exp_1' + 'key': 'group_exp_1', + 'audiences': '' }, 'group_exp_2': { 'variations_map': { @@ -199,7 +222,8 @@ def setUp(self): } }, 'id': '32223', - 'key': 'group_exp_2' + 'key': 'group_exp_2', + 'audiences': '' }, 'group_2_exp_1': { 'variations_map': { @@ -213,7 +237,8 @@ def setUp(self): }, }, 'id': '42222', - 'key': 'group_2_exp_1' + 'key': 'group_2_exp_1', + 'audiences': '"Test attribute users 3"' }, 'group_2_exp_2': { 'variations_map': { @@ -227,7 +252,8 @@ def setUp(self): }, }, 'id': '42223', - 'key': 'group_2_exp_2' + 'key': 'group_2_exp_2', + 'audiences': '"Test attribute users 3"' }, 'group_2_exp_3': { 'variations_map': { @@ -241,7 +267,8 @@ def setUp(self): }, }, 'id': '42224', - 'key': 'group_2_exp_3' + 'key': 'group_2_exp_3', + 'audiences': '"Test attribute users 3"' }, 'test_experiment3': { 'variations_map': { @@ -255,7 +282,8 @@ def setUp(self): }, }, 'id': '111134', - 'key': 'test_experiment3' + 'key': 'test_experiment3', + 'audiences': '"Test attribute users 3"' }, 'test_experiment4': { 'variations_map': { @@ -269,7 +297,8 @@ def setUp(self): }, }, 'id': '111135', - 'key': 'test_experiment4' + 'key': 'test_experiment4', + 'audiences': '"Test attribute users 3"' }, 'test_experiment5': { 'variations_map': { @@ -283,7 +312,8 @@ def setUp(self): }, }, 'id': '111136', - 'key': 'test_experiment5' + 'key': 'test_experiment5', + 'audiences': '"Test attribute users 3"' } }, 'features_map': { @@ -435,9 +465,118 @@ def setUp(self): } }, 'id': '111127', - 'key': 'test_experiment' + 'key': 'test_experiment', + 'audiences': '' } }, + 'delivery_rules': [], + 'experiment_rules': [ + { + 'id': '111127', + 'key': 'test_experiment', + 'variations_map': { + 'control': { + 'id': '111128', + 'key': 'control', + 'feature_enabled': False, + 'variables_map': { + 'is_working': { + 'id': '127', + 'key': 'is_working', + 'type': 'boolean', + 'value': 'true' + }, + 'environment': { + 'id': '128', + 'key': 'environment', + 'type': 'string', + 'value': 'devel' + }, + 'cost': { + 'id': '129', + 'key': 'cost', + 'type': 'double', + 'value': '10.99' + }, + 'count': { + 'id': '130', + 'key': 'count', + 'type': 'integer', + 'value': '999' + }, + 'variable_without_usage': { + 'id': '131', + 'key': 'variable_without_usage', + 'type': 'integer', + 'value': '45' + }, + 'object': { + 'id': '132', + 'key': 'object', + 'type': 'json', + 'value': '{"test": 12}' + }, + 'true_object': { + 'id': '133', + 'key': 'true_object', + 'type': 'json', + 'value': '{"true_test": 23.54}' + } + } + }, + 'variation': { + 'id': '111129', + 'key': 'variation', + 'feature_enabled': True, + 'variables_map': { + 'is_working': { + 'id': '127', + 'key': 'is_working', + 'type': 'boolean', + 'value': 'true' + }, + 'environment': { + 'id': '128', + 'key': 'environment', + 'type': 'string', + 'value': 'staging' + }, + 'cost': { + 'id': '129', + 'key': 'cost', + 'type': 'double', + 'value': '10.02' + }, + 'count': { + 'id': '130', + 'key': 'count', + 'type': 'integer', + 'value': '4243' + }, + 'variable_without_usage': { + 'id': '131', + 'key': 'variable_without_usage', + 'type': 'integer', + 'value': '45' + }, + 'object': { + 'id': '132', + 'key': 'object', + 'type': 'json', + 'value': '{"test": 123}' + }, + 'true_object': { + 'id': '133', + 'key': 'true_object', + 'type': 'json', + 'value': '{"true_test": 1.4}' + } + } + } + }, + 'audiences': '' + } + ], 'id': '91111', 'key': 'test_feature_in_experiment' }, @@ -477,6 +616,8 @@ def setUp(self): 'experiments_map': { }, + 'delivery_rules': [], + 'experiment_rules': [], 'id': '91112', 'key': 'test_feature_in_rollout' }, @@ -505,9 +646,32 @@ def setUp(self): } }, 'id': '32222', - 'key': 'group_exp_1' + 'key': 'group_exp_1', + 'audiences': '' } }, + 'delivery_rules': [], + 'experiment_rules': [ + { + 'id': '32222', + 'key': 'group_exp_1', + 'variations_map': { + 'group_exp_1_control': { + 'id': '28901', + 'key': 'group_exp_1_control', + 'feature_enabled': None, + 'variables_map': {} + }, + 'group_exp_1_variation': { + 'id': '28902', + 'key': 'group_exp_1_variation', + 'feature_enabled': None, + 'variables_map': {} + } + }, + 'audiences': '' + } + ], 'id': '91113', 'key': 'test_feature_in_group' }, @@ -536,9 +700,32 @@ def setUp(self): } }, 'id': '32223', - 'key': 'group_exp_2' + 'key': 'group_exp_2', + 'audiences': '' } }, + 'delivery_rules': [], + 'experiment_rules': [ + { + 'id': '32223', + 'key': 'group_exp_2', + 'variations_map': { + 'group_exp_2_control': { + 'id': '28905', + 'key': 'group_exp_2_control', + 'feature_enabled': None, + 'variables_map': {} + }, + 'group_exp_2_variation': { + 'id': '28906', + 'key': 'group_exp_2_variation', + 'feature_enabled': None, + 'variables_map': {} + } + }, + 'audiences': '' + } + ], 'id': '91114', 'key': 'test_feature_in_experiment_and_rollout' }, @@ -559,7 +746,8 @@ def setUp(self): }, }, 'id': '42222', - 'key': 'group_2_exp_1' + 'key': 'group_2_exp_1', + 'audiences': '"Test attribute users 3"' }, 'group_2_exp_2': { 'variations_map': { @@ -573,7 +761,8 @@ def setUp(self): }, }, 'id': '42223', - 'key': 'group_2_exp_2' + 'key': 'group_2_exp_2', + 'audiences': '"Test attribute users 3"' }, 'group_2_exp_3': { 'variations_map': { @@ -587,9 +776,52 @@ def setUp(self): }, }, 'id': '42224', - 'key': 'group_2_exp_3' + 'key': 'group_2_exp_3', + 'audiences': '"Test attribute users 3"' } }, + 'delivery_rules': [], + 'experiment_rules': [ + { + 'id': '42222', + 'key': 'group_2_exp_1', + 'variations_map': { + 'var_1': { + 'id': '38901', + 'key': 'var_1', + 'feature_enabled': None, + 'variables_map': {} + } + }, + 'audiences': '"Test attribute users 3"' + }, + { + 'id': '42223', + 'key': 'group_2_exp_2', + 'variations_map': { + 'var_1': { + 'id': '38905', + 'key': 'var_1', + 'feature_enabled': None, + 'variables_map': {} + } + }, + 'audiences': '"Test attribute users 3"' + }, + { + 'id': '42224', + 'key': 'group_2_exp_3', + 'variations_map': { + 'var_1': { + 'id': '38906', + 'key': 'var_1', + 'feature_enabled': None, + 'variables_map': {} + } + }, + 'audiences': '"Test attribute users 3"' + } + ], 'id': '91115', 'key': 'test_feature_in_exclusion_group' }, @@ -610,7 +842,8 @@ def setUp(self): }, }, 'id': '111134', - 'key': 'test_experiment3' + 'key': 'test_experiment3', + 'audiences': '"Test attribute users 3"' }, 'test_experiment4': { 'variations_map': { @@ -624,7 +857,8 @@ def setUp(self): }, }, 'id': '111135', - 'key': 'test_experiment4' + 'key': 'test_experiment4', + 'audiences': '"Test attribute users 3"' }, 'test_experiment5': { 'variations_map': { @@ -638,9 +872,52 @@ def setUp(self): }, }, 'id': '111136', - 'key': 'test_experiment5' + 'key': 'test_experiment5', + 'audiences': '"Test attribute users 3"' } }, + 'delivery_rules': [], + 'experiment_rules': [ + { + 'id': '111134', + 'key': 'test_experiment3', + 'variations_map': { + 'control': { + 'id': '222239', + 'key': 'control', + 'feature_enabled': None, + 'variables_map': {} + } + }, + 'audiences': '"Test attribute users 3"' + }, + { + 'id': '111135', + 'key': 'test_experiment4', + 'variations_map': { + 'control': { + 'id': '222240', + 'key': 'control', + 'feature_enabled': None, + 'variables_map': {} + } + }, + 'audiences': '"Test attribute users 3"' + }, + { + 'id': '111136', + 'key': 'test_experiment5', + 'variations_map': { + 'control': { + 'id': '222241', + 'key': 'control', + 'feature_enabled': None, + 'variables_map': {} + } + }, + 'audiences': '"Test attribute users 3"' + } + ], 'id': '91116', 'key': 'test_feature_in_multiple_experiments' } @@ -652,6 +929,209 @@ def setUp(self): self.actual_config = self.opt_config_service.get_config() self.actual_config_dict = self.to_dict(self.actual_config) + self.typed_audiences_config = { + 'version': '2', + 'rollouts': [], + 'projectId': '10431130345', + 'variables': [], + 'featureFlags': [], + 'experiments': [ + { + 'status': 'Running', + 'key': 'ab_running_exp_untargeted', + 'layerId': '10417730432', + 'trafficAllocation': [{'entityId': '10418551353', 'endOfRange': 10000}], + 'audienceIds': [], + 'variations': [ + {'variables': [], 'id': '10418551353', 'key': 'all_traffic_variation'}, + {'variables': [], 'id': '10418510624', 'key': 'no_traffic_variation'}, + ], + 'forcedVariations': {}, + 'id': '10420810910', + } + ], + 'audiences': [ + { + 'id': '3468206642', + 'name': 'exactString', + 'conditions': '["and", ["or", ["or", {"name": "house", ' + '"type": "custom_attribute", "value": "Gryffindor"}]]]', + }, + { + 'id': '3988293898', + 'name': '$$dummySubstringString', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3988293899', + 'name': '$$dummyExists', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206646', + 'name': '$$dummyExactNumber', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206647', + 'name': '$$dummyGtNumber', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206644', + 'name': '$$dummyLtNumber', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206643', + 'name': '$$dummyExactBoolean', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206645', + 'name': '$$dummyMultipleCustomAttrs', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '0', + 'name': '$$dummy', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + ], + 'typedAudiences': [ + { + 'id': '3988293898', + 'name': 'substringString', + 'conditions': [ + 'and', + [ + 'or', + [ + 'or', + { + 'name': 'house', + 'type': 'custom_attribute', + 'match': 'substring', + 'value': 'Slytherin', + }, + ], + ], + ], + }, + { + 'id': '3988293899', + 'name': 'exists', + 'conditions': [ + 'and', + [ + 'or', + ['or', {'name': 'favorite_ice_cream', 'type': 'custom_attribute', 'match': 'exists'}], + ], + ], + }, + { + 'id': '3468206646', + 'name': 'exactNumber', + 'conditions': [ + 'and', + [ + 'or', + ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'exact', 'value': 45.5}], + ], + ], + }, + { + 'id': '3468206647', + 'name': 'gtNumber', + 'conditions': [ + 'and', + ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'gt', 'value': 70}]], + ], + }, + { + 'id': '3468206644', + 'name': 'ltNumber', + 'conditions': [ + 'and', + ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'lt', 'value': 1.0}]], + ], + }, + { + 'id': '3468206643', + 'name': 'exactBoolean', + 'conditions': [ + 'and', + [ + 'or', + [ + 'or', + {'name': 'should_do_it', 'type': 'custom_attribute', 'match': 'exact', 'value': True}, + ], + ], + ], + }, + { + 'id': '3468206645', + 'name': 'multiple_custom_attrs', + 'conditions': [ + "and", + [ + "or", + [ + "or", + {"type": "custom_attribute", "name": "browser", "value": "chrome"}, + {"type": "custom_attribute", "name": "browser", "value": "firefox"}, + ], + ], + ], + }, + { + "id": "18278344267", + "name": "semverReleaseLt1.2.3Gt1.0.0", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "1.2.3", + "type": "custom_attribute", + "name": "android-release", + "match": "semver_lt" + } + ] + ], + [ + "or", + [ + "or", + { + "value": "1.0.0", + "type": "custom_attribute", + "name": "android-release", + "match": "semver_gt" + } + ] + ] + ] + } + ], + 'groups': [], + 'attributes': [], + 'accountId': '10367498574', + 'events': [{'experimentIds': ['10420810910'], 'id': '10404198134', 'key': 'winning'}], + 'revision': '1337', + } + def to_dict(self, obj): return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) @@ -749,7 +1229,7 @@ def test__get_sdk_key(self): expected_value = 'testSdkKey' - self.assertEqual(expected_value, config.get_sdk_key()) + self.assertEqual(expected_value, config.sdk_key) def test__get_sdk_key_invalid(self): """ Negative Test that tests get_sdk_key does not return the expected value. """ @@ -763,7 +1243,7 @@ def test__get_sdk_key_invalid(self): invalid_value = 123 - self.assertNotEqual(invalid_value, config.get_sdk_key()) + self.assertNotEqual(invalid_value, config.sdk_key) def test__get_environment_key(self): """ Test that get_environment_key returns the expected value. """ @@ -777,7 +1257,7 @@ def test__get_environment_key(self): expected_value = 'TestEnvironmentKey' - self.assertEqual(expected_value, config.get_environment_key()) + self.assertEqual(expected_value, config.environment_key) def test__get_environment_key_invalid(self): """ Negative Test that tests get_environment_key does not return the expected value. """ @@ -791,7 +1271,7 @@ def test__get_environment_key_invalid(self): invalid_value = 321 - self.assertNotEqual(invalid_value, config.get_environment_key()) + self.assertNotEqual(invalid_value, config.environment_key) def test__get_attributes(self): """ Test that the get_attributes returns the expected value. """ @@ -819,8 +1299,8 @@ def test__get_attributes(self): 'key': '234' }] - self.assertEqual(expected_value, config.get_attributes()) - self.assertEqual(len(config.get_attributes()), 2) + self.assertEqual(expected_value, config.attributes) + self.assertEqual(len(config.attributes), 2) def test__get_events(self): """ Test that the get_events returns the expected value. """ @@ -861,5 +1341,126 @@ def test__get_events(self): } }] - self.assertEqual(expected_value, config.get_events()) - self.assertEqual(len(config.get_events()), 2) + self.assertEqual(expected_value, config.events) + self.assertEqual(len(config.events), 2) + + def test_get_audiences(self): + ''' Test to confirm get_audiences returns proper value ''' + config_dict = self.typed_audiences_config + + proj_conf = project_config.ProjectConfig( + json.dumps(config_dict), + logger=None, + error_handler=None + ) + + config_service = optimizely_config.OptimizelyConfigService(proj_conf) + + for audience in config_service.audiences: + self.assertIsInstance(audience, optimizely_config.OptimizelyAudience) + + config = config_service.get_config() + + for audience in config.audiences: + self.assertIsInstance(audience, optimizely_config.OptimizelyAudience) + + self.assertEqual(len(config.audiences), len(config_service.audiences)) + + def test_stringify_audience_conditions_all_cases(self): + audiences_map = { + '1': 'us', + '2': 'female', + '3': 'adult', + '11': 'fr', + '12': 'male', + '13': 'kid' + } + + config = optimizely_config.OptimizelyConfig( + revision='101', + experiments_map={}, + features_map={}, + environment_key='TestEnvironmentKey', + attributes={}, + events={}, + audiences=None + ) + + audiences_input = [ + [], + ["or", "1", "2"], + ["and", "1", "2", "3"], + ["not", "1"], + ["or", "1"], + ["and", "1"], + ["1"], + ["1", "2"], + ["and", ["or", "1", "2"], "3"], + ["and", ["or", "1", ["and", "2", "3"]], ["and", "11", ["or", "12", "13"]]], + ["not", ["and", "1", "2"]], + ["or", "1", "100000"], + ["and", "and"] + ] + + audiences_output = [ + '', + '"us" OR "female"', + '"us" AND "female" AND "adult"', + 'NOT "us"', + '"us"', + '"us"', + '"us"', + '"us" OR "female"', + '("us" OR "female") AND "adult"', + '("us" OR ("female" AND "adult")) AND ("fr" AND ("male" OR "kid"))', + 'NOT ("us" AND "female")', + '"us" OR "100000"', + '' + ] + + config_service = optimizely_config.OptimizelyConfigService(config) + + for i in range(len(audiences_input)): + result = config_service.stringify_conditions(audiences_input[i], audiences_map) + self.assertEqual(audiences_output[i], result) + + def test_optimizely_audience_conversion(self): + ''' Test to confirm that audience conversion works and has expected output ''' + config_dict = self.typed_audiences_config + + TOTAL_AUDEINCES_ONCE_MERGED = 10 + + proj_conf = project_config.ProjectConfig( + json.dumps(config_dict), + logger=None, + error_handler=None + ) + + config_service = optimizely_config.OptimizelyConfigService(proj_conf) + + for audience in config_service.audiences: + self.assertIsInstance(audience, optimizely_config.OptimizelyAudience) + + self.assertEqual(len(config_service.audiences), TOTAL_AUDEINCES_ONCE_MERGED) + + def test_get_variations_from_experiments_map(self): + config_dict = self.typed_audiences_config + + proj_conf = project_config.ProjectConfig( + json.dumps(config_dict), + logger=None, + error_handler=None + ) + + config_service = optimizely_config.OptimizelyConfigService(proj_conf) + + experiments_key_map, experiments_id_map = config_service._get_experiments_maps() + + optly_experiment = experiments_id_map['10420810910'] + + for variation in optly_experiment.variations_map.values(): + self.assertIsInstance(variation, optimizely_config.OptimizelyVariation) + if variation.id == '10418551353': + self.assertEqual(variation.key, 'all_traffic_variation') + else: + self.assertEqual(variation.key, 'no_traffic_variation')