diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 52e9d02b..98060e8e 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -413,39 +413,6 @@ def get_variation_for_rollout(self, project_config, rollout, user_id, attributes return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons - def get_experiment_in_group(self, project_config, group, bucketing_id): - """ Determine which experiment in the group the user is bucketed into. - - Args: - project_config: Instance of ProjectConfig. - group: The group to bucket the user into. - bucketing_id: ID to be used for bucketing the user. - - Returns: - Experiment if the user is bucketed into an experiment in the specified group. None otherwise - and array of log messages representing decision making. - """ - decide_reasons = [] - experiment_id = self.bucketer.find_bucket( - project_config, bucketing_id, group.id, group.trafficAllocation) - if experiment_id: - experiment = project_config.get_experiment_from_id(experiment_id) - if experiment: - message = 'User with bucketing ID "%s" is in experiment %s of group %s.' % \ - (bucketing_id, experiment.key, group.id) - self.logger.info( - message - ) - decide_reasons.append(message) - return experiment, decide_reasons - message = 'User with bucketing ID "%s" is not in any experiments of group %s.' % (bucketing_id, group.id) - self.logger.info( - message - ) - decide_reasons.append(message) - - return None, decide_reasons - def get_variation_for_feature(self, project_config, feature, user_id, attributes=None, ignore_user_profile=False): """ Returns the experiment/variation the user is bucketed in for the given feature. @@ -462,31 +429,18 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes decide_reasons = [] bucketing_id, reasons = self._get_bucketing_id(user_id, attributes) decide_reasons += reasons - # First check if the feature is in a mutex group - if feature.groupId: - group = project_config.get_group(feature.groupId) - if group: - experiment, reasons = self.get_experiment_in_group(project_config, group, bucketing_id) - decide_reasons += reasons - if experiment and experiment.id in feature.experimentIds: + + # Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments + if feature.experimentIds: + # Evaluate each experiment ID and return the first bucketed experiment variation + for experiment in feature.experimentIds: + experiment = project_config.get_experiment_from_id(experiment) + if experiment: variation, variation_reasons = self.get_variation( project_config, experiment, user_id, attributes, ignore_user_profile) decide_reasons += variation_reasons if variation: return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST), decide_reasons - else: - self.logger.error(enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature')) - - # Next check if the feature is being experimented on - elif feature.experimentIds: - # If an experiment is not in a group, then the feature can only be associated with one experiment - experiment = project_config.get_experiment_from_id(feature.experimentIds[0]) - if experiment: - variation, variation_reasons = self.get_variation( - project_config, experiment, user_id, attributes, ignore_user_profile) - decide_reasons += variation_reasons - if variation: - return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST), decide_reasons # Next check if user is part of a rollout if feature.rolloutId: diff --git a/optimizely/project_config.py b/optimizely/project_config.py index 77b89e67..c0004495 100644 --- a/optimizely/project_config.py +++ b/optimizely/project_config.py @@ -128,13 +128,6 @@ def __init__(self, datafile, logger, error_handler): # Add this experiment in experiment-feature map. self.experiment_feature_map[exp_id] = [feature.id] - experiment_in_feature = self.experiment_id_map[exp_id] - # Check if any of the experiments are in a group and add the group id for faster bucketing later on - if experiment_in_feature.groupId: - feature.groupId = experiment_in_feature.groupId - # Experiments in feature can only belong to one mutex group - break - @staticmethod def _generate_key_map(entity_list, key, entity_class): """ Helper method to generate map from key to entity object for given list of dicts. diff --git a/tests/base.py b/tests/base.py index 254be7c5..83506c8f 100644 --- a/tests/base.py +++ b/tests/base.py @@ -196,6 +196,78 @@ def setUp(self, config_dict='config_dict'): }, ], }, + { + 'key': 'test_experiment3', + 'status': 'Running', + 'layerId': '6', + "audienceConditions": [ + "or", + "11160" + ], + 'audienceIds': ['11160'], + 'id': '111134', + 'forcedVariations': {}, + 'trafficAllocation': [ + {'entityId': '222239', 'endOfRange': 2500}, + {'entityId': '', 'endOfRange': 5000}, + {'entityId': '', 'endOfRange': 7500}, + {'entityId': '', 'endOfRange': 10000} + ], + 'variations': [ + { + 'id': '222239', + 'key': 'control', + 'variables': [], + } + ], + }, + { + 'key': 'test_experiment4', + 'status': 'Running', + 'layerId': '7', + "audienceConditions": [ + "or", + "11160" + ], + 'audienceIds': ['11160'], + 'id': '111135', + 'forcedVariations': {}, + 'trafficAllocation': [ + {'entityId': '222240', 'endOfRange': 5000}, + {'entityId': '', 'endOfRange': 7500}, + {'entityId': '', 'endOfRange': 10000} + ], + 'variations': [ + { + 'id': '222240', + 'key': 'control', + 'variables': [], + } + ], + }, + { + 'key': 'test_experiment5', + 'status': 'Running', + 'layerId': '8', + "audienceConditions": [ + "or", + "11160" + ], + 'audienceIds': ['11160'], + 'id': '111136', + 'forcedVariations': {}, + 'trafficAllocation': [ + {'entityId': '222241', 'endOfRange': 7500}, + {'entityId': '', 'endOfRange': 10000} + ], + 'variations': [ + { + 'id': '222241', + 'key': 'control', + 'variables': [], + } + ], + }, ], 'groups': [ { @@ -239,6 +311,72 @@ def setUp(self, config_dict='config_dict'): {'entityId': '32222', "endOfRange": 3000}, {'entityId': '32223', 'endOfRange': 7500}, ], + }, + { + 'id': '19229', + 'policy': 'random', + 'experiments': [ + { + 'id': '42222', + 'key': 'group_2_exp_1', + 'status': 'Running', + "audienceConditions": [ + "or", + "11160" + ], + 'audienceIds': ['11160'], + 'layerId': '211183', + 'variations': [ + {'key': 'var_1', 'id': '38901'}, + ], + 'forcedVariations': {}, + 'trafficAllocation': [ + {'entityId': '38901', 'endOfRange': 10000} + ], + }, + { + 'id': '42223', + 'key': 'group_2_exp_2', + 'status': 'Running', + "audienceConditions": [ + "or", + "11160" + ], + 'audienceIds': ['11160'], + 'layerId': '211184', + 'variations': [ + {'key': 'var_1', 'id': '38905'} + ], + 'forcedVariations': {}, + 'trafficAllocation': [ + {'entityId': '38905', 'endOfRange': 10000} + ], + }, + { + 'id': '42224', + 'key': 'group_2_exp_3', + 'status': 'Running', + "audienceConditions": [ + "or", + "11160" + ], + 'audienceIds': ['11160'], + 'layerId': '211185', + 'variations': [ + {'key': 'var_1', 'id': '38906'} + ], + 'forcedVariations': {}, + 'trafficAllocation': [ + {'entityId': '38906', 'endOfRange': 10000} + ], + } + ], + 'trafficAllocation': [ + {'entityId': '42222', "endOfRange": 2500}, + {'entityId': '42223', 'endOfRange': 5000}, + {'entityId': '42224', "endOfRange": 7500}, + {'entityId': '', 'endOfRange': 10000}, + ], } ], 'attributes': [{'key': 'test_attribute', 'id': '111094'}], @@ -255,6 +393,12 @@ def setUp(self, config_dict='config_dict'): '{"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', + } ], 'rollouts': [ {'id': '201111', 'experiments': []}, @@ -364,6 +508,20 @@ def setUp(self, config_dict='config_dict'): 'rolloutId': '211111', 'variables': [], }, + { + 'id': '91115', + 'key': 'test_feature_in_exclusion_group', + 'experimentIds': ['42222', '42223', '42224'], + 'rolloutId': '211111', + 'variables': [], + }, + { + 'id': '91116', + 'key': 'test_feature_in_multiple_experiments', + 'experimentIds': ['111134', '111135', '111136'], + 'rolloutId': '211111', + 'variables': [], + }, ], } diff --git a/tests/test_config.py b/tests/test_config.py index e8836471..4bf1f61c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -501,7 +501,7 @@ def test_init__with_v4_datafile(self): '211111', {'number_of_projects': entities.Variable('131', 'number_of_projects', 'integer', '10')}, ), - 'test_feature_in_group': entities.FeatureFlag('91113', 'test_feature_in_group', ['32222'], '', {}, '19228'), + 'test_feature_in_group': entities.FeatureFlag('91113', 'test_feature_in_group', ['32222'], '', {}), } expected_rollout_id_map = { diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index f4023d0a..97fefce7 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -1320,9 +1320,6 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) "group_exp_1", "28901" ) with mock.patch( - "optimizely.decision_service.DecisionService.get_experiment_in_group", - return_value=(self.project_config.get_experiment_from_key("group_exp_1"), []), - ) as mock_get_experiment_in_group, mock.patch( "optimizely.decision_service.DecisionService.get_variation", return_value=(expected_variation, []), ) as mock_decision: @@ -1338,9 +1335,6 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) variation_received, ) - mock_get_experiment_in_group.assert_called_once_with( - self.project_config, self.project_config.get_group("19228"), 'test_user') - mock_decision.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_key("group_exp_1"), @@ -1349,17 +1343,14 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) False ) - def test_get_variation_for_feature__returns_none_for_user_not_in_group(self): - """ Test that get_variation_for_feature returns None for - user not in group and the feature is not part of a rollout. """ + def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self): + """ Test that get_variation_for_feature returns None for user not in the associated experiment. """ - feature = self.project_config.get_feature_from_key("test_feature_in_group") + feature = self.project_config.get_feature_from_key("test_feature_in_experiment") with mock.patch( - "optimizely.decision_service.DecisionService.get_experiment_in_group", + "optimizely.decision_service.DecisionService.get_variation", return_value=[None, []], - ) as mock_get_experiment_in_group, mock.patch( - "optimizely.decision_service.DecisionService.get_variation" ) as mock_decision: variation_received, _ = self.decision_service.get_variation_for_feature( self.project_config, feature, "test_user" @@ -1369,16 +1360,21 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_group(self): variation_received, ) - mock_get_experiment_in_group.assert_called_once_with( - self.project_config, self.project_config.get_group("19228"), "test_user") - - self.assertFalse(mock_decision.called) - - def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self): - """ Test that get_variation_for_feature returns None for user not in the associated experiment. """ + mock_decision.assert_called_once_with( + self.project_config, + self.project_config.get_experiment_from_key("test_experiment"), + "test_user", + None, + False + ) - feature = self.project_config.get_feature_from_key("test_feature_in_experiment") + def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_not_associated_with_feature( + self, + ): + """ Test that if a user is in the mutex group but the experiment is + not targeting a feature, then None is returned. """ + feature = self.project_config.get_feature_from_key("test_feature_in_group") with mock.patch( "optimizely.decision_service.DecisionService.get_variation", return_value=[None, []], @@ -1392,89 +1388,301 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self ) mock_decision.assert_called_once_with( - self.project_config, - self.project_config.get_experiment_from_key("test_experiment"), - "test_user", - None, - False + self.project_config, self.project_config.get_experiment_from_id("32222"), "test_user", None, False ) - def test_get_variation_for_feature__returns_none_for_invalid_group_id(self): - """ Test that get_variation_for_feature returns None for unknown group ID. """ + def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group_bucket_less_than_2500( + self, + ): + """ Test that if a user is in the mutex group and the user bucket value should be less than 2500.""" - feature = self.project_config.get_feature_from_key("test_feature_in_group") - feature.groupId = "aabbccdd" + feature = self.project_config.get_feature_from_key("test_feature_in_exclusion_group") + expected_experiment = self.project_config.get_experiment_from_key("group_2_exp_1") + expected_variation = self.project_config.get_variation_from_id( + "group_2_exp_1", "38901" + ) + user_attr = {"experiment_attr": "group_experiment"} + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=2400) as mock_generate_bucket_value,\ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: - with self.mock_decision_logger as mock_decision_service_logging: variation_received, _ = self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" + self.project_config, feature, "test_user", user_attr ) + self.assertEqual( - decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.FEATURE_TEST, + ), variation_received, ) - mock_decision_service_logging.error.assert_called_once_with( - enums.Errors.INVALID_GROUP_ID.format("_get_variation_for_feature") + + mock_config_logging.debug.assert_called_with('Assigned bucket 2400 to user with bucketing ID "test_user".') + mock_generate_bucket_value.assert_called_with('test_user42222') + + def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group_bucket_range_2500_5000( + self, + ): + """ Test that if a user is in the mutex group and the user bucket value should be equal to 2500 + or less than 5000.""" + + feature = self.project_config.get_feature_from_key("test_feature_in_exclusion_group") + expected_experiment = self.project_config.get_experiment_from_key("group_2_exp_2") + expected_variation = self.project_config.get_variation_from_id( + "group_2_exp_2", "38905" ) + user_attr = {"experiment_attr": "group_experiment"} + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4000) as mock_generate_bucket_value,\ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: - def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_not_associated_with_feature( + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user", user_attr + ) + self.assertEqual( + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.FEATURE_TEST, + ), + variation_received, + ) + mock_config_logging.debug.assert_called_with('Assigned bucket 4000 to user with bucketing ID "test_user".') + mock_generate_bucket_value.assert_called_with('test_user42223') + + def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group_bucket_range_5000_7500( self, ): - """ Test that if a user is in the mutex group but the experiment is - not targeting a feature, then None is returned. """ + """ Test that if a user is in the mutex group and the user bucket value should be equal to 5000 + or less than 7500.""" - feature = self.project_config.get_feature_from_key("test_feature_in_group") + feature = self.project_config.get_feature_from_key("test_feature_in_exclusion_group") + expected_experiment = self.project_config.get_experiment_from_key("group_2_exp_3") + expected_variation = self.project_config.get_variation_from_id( + "group_2_exp_3", "38906" + ) + user_attr = {"experiment_attr": "group_experiment"} with mock.patch( - "optimizely.decision_service.DecisionService.get_experiment_in_group", - return_value=[self.project_config.get_experiment_from_key("group_exp_2"), []], - ) as mock_decision: + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=6500) as mock_generate_bucket_value,\ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation_received, _ = self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" + self.project_config, feature, "test_user", user_attr ) self.assertEqual( - decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.FEATURE_TEST, + ), variation_received, ) + mock_config_logging.debug.assert_called_with('Assigned bucket 6500 to user with bucketing ID "test_user".') + mock_generate_bucket_value.assert_called_with('test_user42224') - mock_decision.assert_called_once_with( - self.project_config, self.project_config.get_group("19228"), "test_user" + def test_get_variation_for_feature__returns_variation_for_rollout_in_mutex_group_bucket_greater_than_7500( + self, + ): + """ Test that if a user is in the mutex group and the user bucket value should be greater than 7500.""" + + feature = self.project_config.get_feature_from_key("test_feature_in_exclusion_group") + user_attr = {"experiment_attr": "group_experiment"} + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=8000) as mock_generate_bucket_value,\ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: + + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user", user_attr + ) + self.assertEqual( + decision_service.Decision( + None, + None, + enums.DecisionSources.ROLLOUT, + ), + variation_received, + ) + + mock_generate_bucket_value.assert_called_with('test_user211147') + mock_config_logging.debug.assert_called_with('Assigned bucket 8000 to user with bucketing ID "test_user".') + + def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_bucket_less_than_2500( + self, + ): + """ Test that if a user is in the non-mutex group and the user bucket value should be less than 2500.""" + + feature = self.project_config.get_feature_from_key("test_feature_in_multiple_experiments") + expected_experiment = self.project_config.get_experiment_from_key("test_experiment3") + expected_variation = self.project_config.get_variation_from_id( + "test_experiment3", "222239" ) + user_attr = {"experiment_attr": "group_experiment"} + + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=2400) as mock_generate_bucket_value,\ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: + + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user", user_attr + ) + self.assertEqual( + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.FEATURE_TEST, + ), + variation_received, + ) + mock_config_logging.debug.assert_called_with('Assigned bucket 2400 to user with bucketing ID "test_user".') + mock_generate_bucket_value.assert_called_with('test_user111134') + + def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_bucket_range_2500_5000( + self, + ): + """ Test that if a user is in the non-mutex group and the user bucket value should be equal to 2500 + or less than 5000.""" - def test_get_experiment_in_group(self): - """ Test that get_experiment_in_group returns the bucketed experiment for the user. """ + feature = self.project_config.get_feature_from_key("test_feature_in_multiple_experiments") + expected_experiment = self.project_config.get_experiment_from_key("test_experiment4") + expected_variation = self.project_config.get_variation_from_id( + "test_experiment4", "222240" + ) + user_attr = {"experiment_attr": "group_experiment"} - group = self.project_config.get_group("19228") - experiment = self.project_config.get_experiment_from_id("32222") with mock.patch( - "optimizely.bucketer.Bucketer.find_bucket", return_value="32222" - ), self.mock_decision_logger as mock_decision_service_logging: - variation_received, _ = self.decision_service.get_experiment_in_group( - self.project_config, group, "test_user" + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4000) as mock_generate_bucket_value,\ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: + + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user", user_attr ) self.assertEqual( - experiment, + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.FEATURE_TEST, + ), variation_received, ) + mock_config_logging.debug.assert_called_with('Assigned bucket 4000 to user with bucketing ID "test_user".') + mock_generate_bucket_value.assert_called_with('test_user111135') - mock_decision_service_logging.info.assert_called_once_with( - 'User with bucketing ID "test_user" is in experiment group_exp_1 of group 19228.' + def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_bucket_range_5000_7500( + self, + ): + """ Test that if a user is in the non-mutex group and the user bucket value should be equal to 5000 + or less than 7500.""" + + feature = self.project_config.get_feature_from_key("test_feature_in_multiple_experiments") + expected_experiment = self.project_config.get_experiment_from_key("test_experiment5") + expected_variation = self.project_config.get_variation_from_id( + "test_experiment5", "222241" ) + user_attr = {"experiment_attr": "group_experiment"} + + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=6500) as mock_generate_bucket_value,\ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: + + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user", user_attr + ) + self.assertEqual( + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.FEATURE_TEST, + ), + variation_received, + ) + mock_config_logging.debug.assert_called_with('Assigned bucket 6500 to user with bucketing ID "test_user".') + mock_generate_bucket_value.assert_called_with('test_user111136') - def test_get_experiment_in_group__returns_none_if_user_not_in_group(self): - """ Test that get_experiment_in_group returns None if the user is not bucketed into the group. """ + def test_get_variation_for_feature__returns_variation_for_rollout_in_experiment_bucket_greater_than_7500( + self, + ): + """ Test that if a user is in the non-mutex group and the user bucket value should be greater than 7500.""" - group = self.project_config.get_group("19228") + feature = self.project_config.get_feature_from_key("test_feature_in_multiple_experiments") + user_attr = {"experiment_attr": "group_experiment"} with mock.patch( - "optimizely.bucketer.Bucketer.find_bucket", return_value=None - ), self.mock_decision_logger as mock_decision_service_logging: - variation_received, _ = self.decision_service.get_experiment_in_group( - self.project_config, group, "test_user" + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=8000) as mock_generate_bucket_value, \ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user", user_attr ) - self.assertIsNone( - variation_received + self.assertEqual( + decision_service.Decision( + None, + None, + enums.DecisionSources.ROLLOUT, + ), + variation_received, ) - mock_decision_service_logging.info.assert_called_once_with( - 'User with bucketing ID "test_user" is not in any experiments of group 19228.' + mock_generate_bucket_value.assert_called_with('test_user211147') + mock_config_logging.debug.assert_called_with('Assigned bucket 8000 to user with bucketing ID "test_user".') + + def test_get_variation_for_feature__returns_variation_for_rollout_in_mutex_group_audience_mismatch( + self, + ): + """ Test that if a user is in the mutex group and the user bucket value should be less than 2500 and + missing target by audience.""" + + feature = self.project_config.get_feature_from_key("test_feature_in_exclusion_group") + expected_experiment = self.project_config.get_experiment_from_id("211147") + expected_variation = self.project_config.get_variation_from_id( + "211147", "211149" ) + user_attr = {"experiment_attr": "group_experiment_invalid"} + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=2400) as mock_generate_bucket_value, \ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user", user_attr + ) + + self.assertEqual( + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.ROLLOUT, + ), + variation_received, + ) + + mock_config_logging.debug.assert_called_with('Assigned bucket 2400 to user with bucketing ID "test_user".') + mock_generate_bucket_value.assert_called_with('test_user211147') + + def test_get_variation_for_feature_returns_rollout_in_experiment_bucket_range_2500_5000_audience_mismatch( + self, + ): + """ Test that if a user is in the non-mutex group and the user bucket value should be equal to 2500 + or less than 5000 missing target by audience.""" + + feature = self.project_config.get_feature_from_key("test_feature_in_multiple_experiments") + expected_experiment = self.project_config.get_experiment_from_id("211147") + expected_variation = self.project_config.get_variation_from_id( + "211147", "211149" + ) + user_attr = {"experiment_attr": "group_experiment_invalid"} + + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4000) as mock_generate_bucket_value, \ + mock.patch.object(self.project_config, 'logger') as mock_config_logging: + variation_received, _ = self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user", user_attr + ) + self.assertEqual( + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.ROLLOUT, + ), + variation_received, + ) + mock_config_logging.debug.assert_called_with('Assigned bucket 4000 to user with bucketing ID "test_user".') + mock_generate_bucket_value.assert_called_with('test_user211147') diff --git a/tests/test_optimizely_config.py b/tests/test_optimizely_config.py index 695cdc91..94e1fb00 100644 --- a/tests/test_optimizely_config.py +++ b/tests/test_optimizely_config.py @@ -196,6 +196,90 @@ def setUp(self): }, 'id': '32223', 'key': 'group_exp_2' + }, + 'group_2_exp_1': { + 'variations_map': { + 'var_1': { + 'variables_map': { + + }, + 'id': '38901', + 'key': 'var_1', + 'feature_enabled': None + }, + }, + 'id': '42222', + 'key': 'group_2_exp_1' + }, + 'group_2_exp_2': { + 'variations_map': { + 'var_1': { + 'variables_map': { + + }, + 'id': '38905', + 'key': 'var_1', + 'feature_enabled': None + }, + }, + 'id': '42223', + 'key': 'group_2_exp_2' + }, + 'group_2_exp_3': { + 'variations_map': { + 'var_1': { + 'variables_map': { + + }, + 'id': '38906', + 'key': 'var_1', + 'feature_enabled': None + }, + }, + 'id': '42224', + 'key': 'group_2_exp_3' + }, + 'test_experiment3': { + 'variations_map': { + 'control': { + 'variables_map': { + + }, + 'id': '222239', + 'key': 'control', + 'feature_enabled': None + }, + }, + 'id': '111134', + 'key': 'test_experiment3' + }, + 'test_experiment4': { + 'variations_map': { + 'control': { + 'variables_map': { + + }, + 'id': '222240', + 'key': 'control', + 'feature_enabled': None + }, + }, + 'id': '111135', + 'key': 'test_experiment4' + }, + 'test_experiment5': { + 'variations_map': { + 'control': { + 'variables_map': { + + }, + 'id': '222241', + 'key': 'control', + 'feature_enabled': None + }, + }, + 'id': '111136', + 'key': 'test_experiment5' } }, 'features_map': { @@ -453,6 +537,108 @@ def setUp(self): }, 'id': '91114', 'key': 'test_feature_in_experiment_and_rollout' + }, + 'test_feature_in_exclusion_group': { + 'variables_map': { + + }, + 'experiments_map': { + 'group_2_exp_1': { + 'variations_map': { + 'var_1': { + 'variables_map': { + + }, + 'id': '38901', + 'key': 'var_1', + 'feature_enabled': None + }, + }, + 'id': '42222', + 'key': 'group_2_exp_1' + }, + 'group_2_exp_2': { + 'variations_map': { + 'var_1': { + 'variables_map': { + + }, + 'id': '38905', + 'key': 'var_1', + 'feature_enabled': None + }, + }, + 'id': '42223', + 'key': 'group_2_exp_2' + }, + 'group_2_exp_3': { + 'variations_map': { + 'var_1': { + 'variables_map': { + + }, + 'id': '38906', + 'key': 'var_1', + 'feature_enabled': None + }, + }, + 'id': '42224', + 'key': 'group_2_exp_3' + } + }, + 'id': '91115', + 'key': 'test_feature_in_exclusion_group' + }, + 'test_feature_in_multiple_experiments': { + 'variables_map': { + + }, + 'experiments_map': { + 'test_experiment3': { + 'variations_map': { + 'control': { + 'variables_map': { + + }, + 'id': '222239', + 'key': 'control', + 'feature_enabled': None + }, + }, + 'id': '111134', + 'key': 'test_experiment3' + }, + 'test_experiment4': { + 'variations_map': { + 'control': { + 'variables_map': { + + }, + 'id': '222240', + 'key': 'control', + 'feature_enabled': None + }, + }, + 'id': '111135', + 'key': 'test_experiment4' + }, + 'test_experiment5': { + 'variations_map': { + 'control': { + 'variables_map': { + + }, + 'id': '222241', + 'key': 'control', + 'feature_enabled': None + }, + }, + 'id': '111136', + 'key': 'test_experiment5' + } + }, + 'id': '91116', + 'key': 'test_feature_in_multiple_experiments' } }, 'revision': '1', diff --git a/tests/test_user_context.py b/tests/test_user_context.py index abc18a87..7c979028 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -1040,7 +1040,9 @@ def test_decide_for_all(self): 'test_feature_in_experiment', 'test_feature_in_rollout', 'test_feature_in_group', - 'test_feature_in_experiment_and_rollout' + 'test_feature_in_experiment_and_rollout', + 'test_feature_in_exclusion_group', + 'test_feature_in_multiple_experiments' ], options )