diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index 6bc92333..c16a1be2 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -12,16 +12,18 @@ # limitations under the License. from collections import namedtuple + from six import string_types from . import bucketer +from .decision.optimizely_decide_option import OptimizelyDecideOption from .helpers import audience as audience_helper from .helpers import enums from .helpers import experiment as experiment_helper from .helpers import validator +from .optimizely_user_context import OptimizelyUserContext from .user_profile import UserProfile - Decision = namedtuple('Decision', 'experiment variation source') @@ -42,14 +44,14 @@ def __init__(self, logger, user_profile_service): def _get_bucketing_id(self, user_id, attributes): """ Helper method to determine bucketing ID for the user. - Args: - user_id: ID for user. - attributes: Dict representing user attributes. May consist of bucketing ID to be used. + Args: + user_id: ID for user. + attributes: Dict representing user attributes. May consist of bucketing ID to be used. - Returns: - String representing bucketing ID if it is a String type in attributes else return user ID - array of log messages representing decision making. - """ + Returns: + String representing bucketing ID if it is a String type in attributes else return user ID + array of log messages representing decision making. + """ decide_reasons = [] attributes = attributes or {} bucketing_id = attributes.get(enums.ControlAttributes.BUCKETING_ID) @@ -66,15 +68,15 @@ def _get_bucketing_id(self, user_id, attributes): def set_forced_variation(self, project_config, experiment_key, user_id, variation_key): """ Sets users to a map of experiments to forced variations. - Args: - project_config: Instance of ProjectConfig. - experiment_key: Key for experiment. - user_id: The user ID. - variation_key: Key for variation. If None, then clear the existing experiment-to-variation mapping. + Args: + project_config: Instance of ProjectConfig. + experiment_key: Key for experiment. + user_id: The user ID. + variation_key: Key for variation. If None, then clear the existing experiment-to-variation mapping. - Returns: - A boolean value that indicates if the set completed successfully. - """ + Returns: + A boolean value that indicates if the set completed successfully. + """ experiment = project_config.get_experiment_from_key(experiment_key) if not experiment: # The invalid experiment key will be logged inside this call. @@ -124,15 +126,15 @@ def set_forced_variation(self, project_config, experiment_key, user_id, variatio def get_forced_variation(self, project_config, experiment_key, user_id): """ Gets the forced variation key for the given user and experiment. - Args: - project_config: Instance of ProjectConfig. - experiment_key: Key for experiment. - user_id: The user ID. + Args: + project_config: Instance of ProjectConfig. + experiment_key: Key for experiment. + user_id: The user ID. - Returns: - The variation which the given user and experiment should be forced into and - array of log messages representing decision making. - """ + Returns: + The variation which the given user and experiment should be forced into and + array of log messages representing decision making. + """ decide_reasons = [] if user_id not in self.forced_variation_map: message = 'User "%s" is not in the forced variation map.' % user_id @@ -172,46 +174,49 @@ def get_whitelisted_variation(self, project_config, experiment, user_id): """ Determine if a user is forced into a variation (through whitelisting) for the given experiment and return that variation. - Args: - project_config: Instance of ProjectConfig. - experiment: Object representing the experiment for which user is to be bucketed. - user_id: ID for the user. + Args: + project_config: Instance of ProjectConfig. + experiment: Object representing the experiment for which user is to be bucketed. + user_id: ID for the user. - Returns: - Variation in which the user with ID user_id is forced into. None if no variation and - array of log messages representing decision making. - """ + Returns: + Variation in which the user with ID user_id is forced into. None if no variation and + array of log messages representing decision making. + """ decide_reasons = [] forced_variations = experiment.forcedVariations + if forced_variations and user_id in forced_variations: - variation_key = forced_variations.get(user_id) - variation = project_config.get_variation_from_key(experiment.key, variation_key) - if variation: - message = 'User "%s" is forced in variation "%s".' % (user_id, variation_key) + forced_variation_key = forced_variations.get(user_id) + forced_variation = project_config.get_variation_from_key(experiment.key, forced_variation_key) + + if forced_variation: + message = 'User "%s" is forced in variation "%s".' % (user_id, forced_variation_key) self.logger.info(message) decide_reasons.append(message) - return variation, decide_reasons + + return forced_variation, decide_reasons return None, decide_reasons def get_stored_variation(self, project_config, experiment, user_profile): """ Determine if the user has a stored variation available for the given experiment and return that. - Args: - project_config: Instance of ProjectConfig. - experiment: Object representing the experiment for which user is to be bucketed. - user_profile: UserProfile object representing the user's profile. + Args: + project_config: Instance of ProjectConfig. + experiment: Object representing the experiment for which user is to be bucketed. + user_profile: UserProfile object representing the user's profile. - Returns: - Variation if available. None otherwise. - """ + Returns: + Variation if available. None otherwise. + """ user_id = user_profile.user_id variation_id = user_profile.get_variation_for_experiment(experiment.id) if variation_id: variation = project_config.get_variation_from_id(experiment.key, variation_id) if variation: - message = 'Found a stored decision. User "%s" is in variation "%s" of experiment "%s".'\ + message = 'Found a stored decision. User "%s" is in variation "%s" of experiment "%s".' \ % (user_id, variation.key, experiment.key) self.logger.info( message @@ -220,28 +225,33 @@ def get_stored_variation(self, project_config, experiment, user_profile): return None - def get_variation( - self, project_config, experiment, user_id, attributes, ignore_user_profile=False - ): + def get_variation(self, project_config, experiment, user_context, options=None): """ Top-level function to help determine variation user should be put in. - First, check if experiment is running. - Second, check if user is forced in a variation. - Third, check if there is a stored decision for the user and return the corresponding variation. - Fourth, figure out if user is in the experiment by evaluating audience conditions if any. - Fifth, bucket the user and return the variation. - - Args: - project_config: Instance of ProjectConfig. - experiment: Experiment for which user variation needs to be determined. - user_id: ID for user. - attributes: Dict representing user attributes. - ignore_user_profile: True to ignore the user profile lookup. Defaults to False. - - Returns: - Variation user should see. None if user is not in experiment or experiment is not running - And an array of log messages representing decision making. - """ + First, check if experiment is running. + Second, check if user is forced in a variation. + Third, check if there is a stored decision for the user and return the corresponding variation. + Fourth, figure out if user is in the experiment by evaluating audience conditions if any. + Fifth, bucket the user and return the variation. + + Args: + project_config: Instance of ProjectConfig. + experiment: Experiment for which user variation needs to be determined. + user_context: contains user id and attributes + options: Decide options. + + Returns: + Variation user should see. None if user is not in experiment or experiment is not running + And an array of log messages representing decision making. + """ + user_id = user_context.user_id + attributes = user_context.get_user_attributes() + + if options: + ignore_user_profile = OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in options + else: + ignore_user_profile = False + decide_reasons = [] # Check if experiment is running if not experiment_helper.is_experiment_running(experiment): @@ -323,112 +333,184 @@ def get_variation( decide_reasons.append(message) return None, decide_reasons - def get_variation_for_rollout(self, project_config, rollout, user_id, attributes=None): + def get_variation_for_rollout(self, project_config, feature, user): """ Determine which experiment/variation the user is in for a given rollout. Returns the variation of the first experiment the user qualifies for. - Args: - project_config: Instance of ProjectConfig. - rollout: Rollout for which we are getting the variation. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + project_config: Instance of ProjectConfig. + flagKey: Feature key. + rollout: Rollout for which we are getting the variation. + user: ID and attributes for user. + options: Decide options. + + Returns: + Decision namedtuple consisting of experiment and variation for the user and + array of log messages representing decision making. + """ + decide_reasons = [] + + if not feature or not feature.rolloutId: + return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons - Returns: - Decision namedtuple consisting of experiment and variation for the user and - array of log messages representing decision making. - """ + rollout = project_config.get_rollout_from_id(feature.rolloutId) + rollout_rules = project_config.get_rollout_experiments(rollout) + + if not rollout or not rollout_rules: + message = 'There is no rollout of feature {}.'.format(feature.key) + self.logger.debug(message) + decide_reasons.append(message) + return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons + + index = 0 + while index < len(rollout_rules): + decision_response, reasons_received = self.get_variation_from_delivery_rule(project_config, + feature, + rollout_rules, index, user) + + decide_reasons += reasons_received + + variation, skip_to_everyone_else = decision_response + + if variation: + rule = rollout_rules[index] + feature_decision = Decision(experiment=rule, variation=variation, + source=enums.DecisionSources.ROLLOUT) + + return feature_decision, decide_reasons + + # the last rule is special for "Everyone Else" + index = len(rollout_rules) - 1 if skip_to_everyone_else else index + 1 + + return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons + + def get_variation_from_experiment_rule(self, config, flag_key, rule, user, options): + """ Checks for experiment rule if decision is forced and returns it. + Otherwise returns a regular decision. + + Args: + config: Instance of ProjectConfig. + flag_key: Key of the flag. + rule: Experiment rule. + user: ID and attributes for user. + options: Decide options. + + Returns: + Decision namedtuple consisting of experiment and variation for the user and + array of log messages representing decision making. + """ decide_reasons = [] - # Go through each experiment in order and try to get the variation for the user - if rollout and len(rollout.experiments) > 0: - for idx in range(len(rollout.experiments) - 1): - logging_key = str(idx + 1) - rollout_rule = project_config.get_experiment_from_id(rollout.experiments[idx].get('id')) - - # Check if user meets audience conditions for targeting rule - audience_conditions = rollout_rule.get_audience_conditions_or_ids() - user_meets_audience_conditions, reasons_received = audience_helper.does_user_meet_audience_conditions( - project_config, - audience_conditions, - enums.RolloutRuleAudienceEvaluationLogs, - logging_key, - attributes, - self.logger) - decide_reasons += reasons_received - if not user_meets_audience_conditions: - message = 'User "{}" does not meet conditions for targeting rule {}.'.format(user_id, logging_key) - self.logger.debug( - message - ) - decide_reasons.append(message) - continue - message = 'User "{}" meets audience conditions for targeting rule {}.'.format(user_id, idx + 1) + + # check forced decision first + optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(flag_key, rule.key) + + forced_decision_variation, reasons_received = user.find_validated_forced_decision( + optimizely_decision_context) + decide_reasons += reasons_received + + if forced_decision_variation: + return forced_decision_variation, decide_reasons + + # regular decision + decision_variation, variation_reasons = self.get_variation(config, rule, user, options) + decide_reasons += variation_reasons + return decision_variation, decide_reasons + + def get_variation_from_delivery_rule(self, config, feature, rules, rule_index, user): + """ Checks for delivery rule if decision is forced and returns it. + Otherwise returns a regular decision. + + Args: + config: Instance of ProjectConfig. + flag_key: Key of the flag. + rules: Experiment rule. + rule_index: integer index of the rule in the list. + user: ID and attributes for user. + + Returns: + If forced decision, it returns namedtuple consisting of forced_decision_variation and skip_to_everyone_else + and decision reason log messages. + + If regular decision it returns a tuple of bucketed_variation and skip_to_everyone_else + and decision reason log messages + """ + decide_reasons = [] + skip_to_everyone_else = False + bucketed_variation = None + + # check forced decision first + rule = rules[rule_index] + optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(feature.key, rule.key) + forced_decision_variation, reasons_received = user.find_validated_forced_decision(optimizely_decision_context) + + decide_reasons += reasons_received + + if forced_decision_variation: + return (forced_decision_variation, skip_to_everyone_else), decide_reasons + + # regular decision + user_id = user.user_id + attributes = user.get_user_attributes() + + bucketing_id, bucket_reasons = self._get_bucketing_id(user_id, attributes) + decide_reasons += bucket_reasons + + everyone_else = (rule_index == len(rules) - 1) + logging_key = "Everyone Else" if everyone_else else str(rule_index + 1) + + rollout_rule = config.get_experiment_from_id(rule.id) + audience_conditions = rollout_rule.get_audience_conditions_or_ids() + + audience_decision_response, reasons_received_audience = audience_helper.does_user_meet_audience_conditions( + config, audience_conditions, enums.RolloutRuleAudienceEvaluationLogs, logging_key, attributes, self.logger) + + decide_reasons += reasons_received_audience + + if audience_decision_response: + message = 'User "{}" meets audience conditions for targeting rule {}.'.format(user_id, logging_key) + self.logger.debug(message) + decide_reasons.append(message) + + bucketed_variation, bucket_reasons = self.bucketer.bucket(config, rollout_rule, user_id, + bucketing_id) + decide_reasons.extend(bucket_reasons) + + if bucketed_variation: + message = 'User "{}" bucketed into a targeting rule {}.'.format(user_id, logging_key) self.logger.debug(message) decide_reasons.append(message) - # Determine bucketing ID to be used - bucketing_id, bucket_reasons = self._get_bucketing_id(user_id, attributes) - decide_reasons += bucket_reasons - variation, reasons = self.bucketer.bucket(project_config, rollout_rule, user_id, bucketing_id) - decide_reasons += reasons - if variation: - message = 'User "{}" is in the traffic group of targeting rule {}.'.format(user_id, logging_key) - self.logger.debug( - message - ) - decide_reasons.append(message) - return Decision(rollout_rule, variation, enums.DecisionSources.ROLLOUT), decide_reasons - else: - message = 'User "{}" is not in the traffic group for targeting rule {}. ' \ - 'Checking "Everyone Else" rule now.'.format(user_id, logging_key) - # Evaluate no further rules - self.logger.debug( - message - ) - decide_reasons.append(message) - break - - # Evaluate last rule i.e. "Everyone Else" rule - everyone_else_rule = project_config.get_experiment_from_id(rollout.experiments[-1].get('id')) - audience_conditions = everyone_else_rule.get_audience_conditions_or_ids() - audience_eval, audience_reasons = audience_helper.does_user_meet_audience_conditions( - project_config, - audience_conditions, - enums.RolloutRuleAudienceEvaluationLogs, - 'Everyone Else', - attributes, - self.logger - ) - decide_reasons += audience_reasons - if audience_eval: - # Determine bucketing ID to be used - bucketing_id, bucket_id_reasons = self._get_bucketing_id(user_id, attributes) - decide_reasons += bucket_id_reasons - variation, bucket_reasons = self.bucketer.bucket( - project_config, everyone_else_rule, user_id, bucketing_id) - decide_reasons += bucket_reasons - if variation: - message = 'User "{}" meets conditions for targeting rule "Everyone Else".'.format(user_id) - self.logger.debug(message) - decide_reasons.append(message) - return Decision(everyone_else_rule, variation, enums.DecisionSources.ROLLOUT,), decide_reasons - return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons + elif not everyone_else: + # skip this logging for EveryoneElse since this has a message not for everyone_else + message = 'User "{}" not bucketed into a targeting rule {}. ' \ + 'Checking "Everyone Else" rule now.'.format(user_id, logging_key) + self.logger.debug(message) + decide_reasons.append(message) + + # skip the rest of rollout rules to the everyone-else rule if audience matches but not bucketed. + skip_to_everyone_else = True + + else: + message = 'User "{}" does not meet audience conditions for targeting rule {}.'.format(user_id, logging_key) + self.logger.debug(message) + decide_reasons.append(message) + + return (bucketed_variation, skip_to_everyone_else), decide_reasons - def get_variation_for_feature(self, project_config, feature, user_id, attributes=None, ignore_user_profile=False): + def get_variation_for_feature(self, project_config, feature, user_context, options=None): """ Returns the experiment/variation the user is bucketed in for the given feature. - Args: - project_config: Instance of ProjectConfig. - feature: Feature for which we are determining if it is enabled or not for the given user. - user_id: ID for user. - attributes: Dict representing user attributes. - ignore_user_profile: True if we should bypass the user profile service + Args: + project_config: Instance of ProjectConfig. + feature: Feature for which we are determining if it is enabled or not for the given user. + user: user context for user. + attributes: Dict representing user attributes. + options: Decide options. - Returns: - Decision namedtuple consisting of experiment and variation for the user. + Returns: + Decision namedtuple consisting of experiment and variation for the user. """ decide_reasons = [] - bucketing_id, reasons = self._get_bucketing_id(user_id, attributes) - decide_reasons += reasons # Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments if feature.experimentIds: @@ -436,15 +518,10 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes 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) + variation, variation_reasons = self.get_variation_from_experiment_rule( + project_config, feature.key, experiment, user_context, options) 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: - rollout = project_config.get_rollout_from_id(feature.rolloutId) - return self.get_variation_for_rollout(project_config, rollout, user_id, attributes) - else: - return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons + return self.get_variation_for_rollout(project_config, feature, user_context) diff --git a/optimizely/entities.py b/optimizely/entities.py index 88cd49c4..15576568 100644 --- a/optimizely/entities.py +++ b/optimizely/entities.py @@ -74,6 +74,23 @@ def get_audience_conditions_or_ids(self): def __str__(self): return self.key + @staticmethod + def get_default(): + """ returns an empty experiment object. """ + experiment = Experiment( + id='', + key='', + layerId='', + status='', + variations=[], + trafficAllocation=[], + audienceIds=[], + audienceConditions=[], + forcedVariations={} + ) + + return experiment + class FeatureFlag(BaseEntity): def __init__(self, id, key, experimentIds, rolloutId, variables, groupId=None, **kwargs): @@ -94,6 +111,7 @@ def __init__(self, id, policy, experiments, trafficAllocation, **kwargs): class Layer(BaseEntity): + """Layer acts as rollout.""" def __init__(self, id, experiments, **kwargs): self.id = id self.experiments = experiments diff --git a/optimizely/event/user_event_factory.py b/optimizely/event/user_event_factory.py index 1db9fc95..38217883 100644 --- a/optimizely/event/user_event_factory.py +++ b/optimizely/event/user_event_factory.py @@ -51,6 +51,9 @@ def create_impression_event( if variation_id and experiment_id: variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id) + # need this condition when we send events involving forced decisions + elif variation_id and flag_key: + variation = project_config.get_flag_variation(flag_key, 'id', variation_id) event_context = user_event.EventContext( project_config.account_id, project_config.project_id, project_config.revision, project_config.anonymize_ip, ) diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index 8339eee6..aed202eb 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -115,6 +115,17 @@ class Errors(object): UNSUPPORTED_DATAFILE_VERSION = 'This version of the Python SDK does not support the given datafile version: "{}".' +class ForcedDecisionLogs(object): + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED = 'Variation ({}) is mapped to flag ({}), rule ({}) and user ({}) ' \ + 'in the forced decision map.' + USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED = 'Variation ({}) is mapped to flag ({}) and user ({}) ' \ + 'in the forced decision map.' + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID = 'Invalid variation is mapped to flag ({}), rule ({}) ' \ + 'and user ({}) in the forced decision map.' + USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED_BUT_INVALID = 'Invalid variation is mapped to flag ({}) ' \ + 'and user ({}) in the forced decision map.' + + class HTTPHeaders(object): AUTHORIZATION = 'Authorization' IF_MODIFIED_SINCE = 'If-Modified-Since' diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 1383674a..ea68e92c 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -24,6 +24,7 @@ from .decision.optimizely_decide_option import OptimizelyDecideOption from .decision.optimizely_decision import OptimizelyDecision from .decision.optimizely_decision_message import OptimizelyDecisionMessage +from .decision_service import Decision from .error_handler import NoOpErrorHandler as noop_error_handler from .event import event_factory, user_event_factory from .event.event_processor import ForwardingEventProcessor @@ -55,28 +56,31 @@ def __init__( ): """ Optimizely init method for managing Custom projects. - Args: - datafile: Optional JSON string representing the project. Must provide at least one of datafile or sdk_key. - event_dispatcher: Provides a dispatch_event method which if given a URL and params sends a request to it. - logger: Optional component which provides a log method to log messages. By default nothing would be logged. - error_handler: Optional component which provides a handle_error method to handle exceptions. - By default all exceptions will be suppressed. - skip_json_validation: Optional boolean param which allows skipping JSON schema validation upon object invocation. - By default JSON schema validation will be performed. - user_profile_service: Optional component which provides methods to store and manage user profiles. - sdk_key: Optional string uniquely identifying the datafile corresponding to project and environment combination. - Must provide at least one of datafile or sdk_key. - config_manager: Optional component which implements optimizely.config_manager.BaseConfigManager. - notification_center: Optional instance of notification_center.NotificationCenter. Useful when providing own - config_manager.BaseConfigManager implementation which can be using the - same NotificationCenter instance. - event_processor: Optional component which processes the given event(s). - By default optimizely.event.event_processor.ForwardingEventProcessor is used - which simply forwards events to the event dispatcher. - To enable event batching configure and use optimizely.event.event_processor.BatchEventProcessor. - datafile_access_token: Optional string used to fetch authenticated datafile for a secure project environment. - default_decide_options: Optional list of decide options used with the decide APIs. - """ + Args: + datafile: Optional JSON string representing the project. Must provide at least one of datafile or sdk_key. + event_dispatcher: Provides a dispatch_event method which if given a URL and params sends a request to it. + logger: Optional component which provides a log method to log messages. By default nothing would be logged. + error_handler: Optional component which provides a handle_error method to handle exceptions. + By default all exceptions will be suppressed. + skip_json_validation: Optional boolean param which allows skipping JSON schema validation upon object + invocation. + By default JSON schema validation will be performed. + user_profile_service: Optional component which provides methods to store and manage user profiles. + sdk_key: Optional string uniquely identifying the datafile corresponding to project and environment + combination. + Must provide at least one of datafile or sdk_key. + config_manager: Optional component which implements optimizely.config_manager.BaseConfigManager. + notification_center: Optional instance of notification_center.NotificationCenter. Useful when providing own + config_manager.BaseConfigManager implementation which can be using the + same NotificationCenter instance. + event_processor: Optional component which processes the given event(s). + By default optimizely.event.event_processor.ForwardingEventProcessor is used + which simply forwards events to the event dispatcher. + To enable event batching configure and use + optimizely.event.event_processor.BatchEventProcessor. + datafile_access_token: Optional string used to fetch authenticated datafile for a secure project environment. + default_decide_options: Optional list of decide options used with the decide APIs. + """ self.logger_name = '.'.join([__name__, self.__class__.__name__]) self.is_valid = True self.event_dispatcher = event_dispatcher or default_event_dispatcher @@ -134,9 +138,9 @@ def __init__( def _validate_instantiation_options(self): """ Helper method to validate all instantiation parameters. - Raises: - Exception if provided instantiation options are valid. - """ + Raises: + Exception if provided instantiation options are valid. + """ if self.config_manager and not validator.is_config_manager_valid(self.config_manager): raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('config_manager')) @@ -158,14 +162,14 @@ def _validate_instantiation_options(self): def _validate_user_inputs(self, attributes=None, event_tags=None): """ Helper method to validate user inputs. - Args: - attributes: Dict representing user attributes. - event_tags: Dict representing metadata associated with an event. + Args: + attributes: Dict representing user attributes. + event_tags: Dict representing metadata associated with an event. - Returns: - Boolean True if inputs are valid. False otherwise. + Returns: + Boolean True if inputs are valid. False otherwise. - """ + """ if attributes and not validator.are_attributes_valid(attributes): self.logger.error('Provided attributes are in an invalid format.') @@ -183,17 +187,20 @@ def _send_impression_event(self, project_config, experiment, variation, flag_key user_id, attributes): """ Helper method to send impression event. - Args: - project_config: Instance of ProjectConfig. - experiment: Experiment for which impression event is being sent. - variation: Variation picked for user for the given experiment. - flag_key: key for a feature flag. - rule_key: key for an experiment. - rule_type: type for the source. - enabled: boolean representing if feature is enabled - user_id: ID for user. - attributes: Dict representing user attributes and values which need to be recorded. - """ + Args: + project_config: Instance of ProjectConfig. + experiment: Experiment for which impression event is being sent. + variation: Variation picked for user for the given experiment. + flag_key: key for a feature flag. + rule_key: key for an experiment. + rule_type: type for the source. + enabled: boolean representing if feature is enabled + user_id: ID for user. + attributes: Dict representing user attributes and values which need to be recorded. + """ + if not experiment: + experiment = entities.Experiment.get_default() + variation_id = variation.id if variation is not None else None user_event = user_event_factory.UserEventFactory.create_impression_event( project_config, experiment, variation_id, flag_key, rule_key, rule_type, enabled, user_id, attributes @@ -215,20 +222,20 @@ def _get_feature_variable_for_type( ): """ Helper method to determine value for a certain variable attached to a feature flag based on type of variable. - Args: - project_config: Instance of ProjectConfig. - feature_key: Key of the feature whose variable's value is being accessed. - variable_key: Key of the variable whose value is to be accessed. - variable_type: Type of variable which could be one of boolean/double/integer/string. - user_id: ID for user. - attributes: Dict representing user attributes. - - Returns: - Value of the variable. None if: - - Feature key is invalid. - - Variable key is invalid. - - Mismatch with type of variable. - """ + Args: + project_config: Instance of ProjectConfig. + feature_key: Key of the feature whose variable's value is being accessed. + variable_key: Key of the variable whose value is to be accessed. + variable_type: Type of variable which could be one of boolean/double/integer/string. + user_id: ID for user. + attributes: Dict representing user attributes. + + Returns: + Value of the variable. None if: + - Feature key is invalid. + - Variable key is invalid. + - Mismatch with type of variable. + """ if not validator.is_non_empty_string(feature_key): self.logger.error(enums.Errors.INVALID_INPUT.format('feature_key')) return None @@ -264,7 +271,10 @@ def _get_feature_variable_for_type( feature_enabled = False source_info = {} variable_value = variable.defaultValue - decision, _ = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, attributes) + + user_context = self.create_user_context(user_id, attributes) + decision, _ = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_context) + if decision.variation: feature_enabled = decision.variation.featureEnabled @@ -315,20 +325,20 @@ def _get_feature_variable_for_type( return actual_value def _get_all_feature_variables_for_type( - self, project_config, feature_key, user_id, attributes, + self, project_config, feature_key, user_id, attributes, ): """ Helper method to determine value for all variables attached to a feature flag. - Args: - project_config: Instance of ProjectConfig. - feature_key: Key of the feature whose variable's value is being accessed. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + project_config: Instance of ProjectConfig. + feature_key: Key of the feature whose variable's value is being accessed. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - Dictionary of all variables. None if: - - Feature key is invalid. - """ + Returns: + Dictionary of all variables. None if: + - Feature key is invalid. + """ if not validator.is_non_empty_string(feature_key): self.logger.error(enums.Errors.INVALID_INPUT.format('feature_key')) return None @@ -347,8 +357,9 @@ def _get_all_feature_variables_for_type( feature_enabled = False source_info = {} - decision, _ = self.decision_service.get_variation_for_feature( - project_config, feature_flag, user_id, attributes) + user_context = self.create_user_context(user_id, attributes) + decision, _ = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_context) + if decision.variation: feature_enabled = decision.variation.featureEnabled @@ -409,15 +420,15 @@ def _get_all_feature_variables_for_type( def activate(self, experiment_key, user_id, attributes=None): """ Buckets visitor and sends impression event to Optimizely. - Args: - experiment_key: Experiment which needs to be activated. - user_id: ID for user. - attributes: Dict representing user attributes and values which need to be recorded. + Args: + experiment_key: Experiment which needs to be activated. + user_id: ID for user. + attributes: Dict representing user attributes and values which need to be recorded. - Returns: - Variation key representing the variation the user will be bucketed in. - None if user is not in experiment or if experiment is not Running. - """ + Returns: + Variation key representing the variation the user will be bucketed in. + None if user is not in experiment or if experiment is not Running. + """ if not self.is_valid: self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('activate')) @@ -455,12 +466,12 @@ def activate(self, experiment_key, user_id, attributes=None): def track(self, event_key, user_id, attributes=None, event_tags=None): """ Send conversion event to Optimizely. - Args: - event_key: Event key representing the event which needs to be recorded. - user_id: ID for user. - attributes: Dict representing visitor attributes and values which need to be recorded. - event_tags: Dict representing metadata associated with the event. - """ + Args: + event_key: Event key representing the event which needs to be recorded. + user_id: ID for user. + attributes: Dict representing visitor attributes and values which need to be recorded. + event_tags: Dict representing metadata associated with the event. + """ if not self.is_valid: self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('track')) @@ -503,15 +514,15 @@ def track(self, event_key, user_id, attributes=None, event_tags=None): def get_variation(self, experiment_key, user_id, attributes=None): """ Gets variation where user will be bucketed. - Args: - experiment_key: Experiment for which user variation needs to be determined. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + experiment_key: Experiment for which user variation needs to be determined. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - Variation key representing the variation the user will be bucketed in. - None if user is not in experiment or if experiment is not Running. - """ + Returns: + Variation key representing the variation the user will be bucketed in. + None if user is not in experiment or if experiment is not Running. + """ if not self.is_valid: self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('get_variation')) @@ -540,7 +551,9 @@ def get_variation(self, experiment_key, user_id, attributes=None): if not self._validate_user_inputs(attributes): return None - variation, _ = self.decision_service.get_variation(project_config, experiment, user_id, attributes) + user_context = self.create_user_context(user_id, attributes) + + variation, _ = self.decision_service.get_variation(project_config, experiment, user_context) if variation: variation_key = variation.key @@ -562,14 +575,14 @@ def get_variation(self, experiment_key, user_id, attributes=None): def is_feature_enabled(self, feature_key, user_id, attributes=None): """ Returns true if the feature is enabled for the given user. - Args: - feature_key: The key of the feature for which we are determining if it is enabled or not for the given user. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + feature_key: The key of the feature for which we are determining if it is enabled or not for the given user. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - True if the feature is enabled for the user. False otherwise. - """ + Returns: + True if the feature is enabled for the user. False otherwise. + """ if not self.is_valid: self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('is_feature_enabled')) @@ -597,7 +610,8 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None): feature_enabled = False source_info = {} - decision, _ = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes) + user_context = self.create_user_context(user_id, attributes) + decision, _ = self.decision_service.get_variation_for_feature(project_config, feature, user_context) is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST is_source_rollout = decision.source == enums.DecisionSources.ROLLOUT @@ -645,13 +659,13 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None): def get_enabled_features(self, user_id, attributes=None): """ Returns the list of features that are enabled for the user. - Args: - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - A list of the keys of the features that are enabled for the user. - """ + Returns: + A list of the keys of the features that are enabled for the user. + """ enabled_features = [] if not self.is_valid: @@ -679,17 +693,17 @@ def get_enabled_features(self, user_id, attributes=None): def get_feature_variable(self, feature_key, variable_key, user_id, attributes=None): """ Returns value for a variable attached to a feature flag. - Args: - feature_key: Key of the feature whose variable's value is being accessed. - variable_key: Key of the variable whose value is to be accessed. - user_id: ID for user. - attributes: Dict representing user attributes. - - Returns: - Value of the variable. None if: - - Feature key is invalid. - - Variable key is invalid. - """ + Args: + feature_key: Key of the feature whose variable's value is being accessed. + variable_key: Key of the variable whose value is to be accessed. + user_id: ID for user. + attributes: Dict representing user attributes. + + Returns: + Value of the variable. None if: + - Feature key is invalid. + - Variable key is invalid. + """ project_config = self.config_manager.get_config() if not project_config: self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable')) @@ -700,18 +714,18 @@ def get_feature_variable(self, feature_key, variable_key, user_id, attributes=No def get_feature_variable_boolean(self, feature_key, variable_key, user_id, attributes=None): """ Returns value for a certain boolean variable attached to a feature flag. - Args: - feature_key: Key of the feature whose variable's value is being accessed. - variable_key: Key of the variable whose value is to be accessed. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + feature_key: Key of the feature whose variable's value is being accessed. + variable_key: Key of the variable whose value is to be accessed. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - Boolean value of the variable. None if: - - Feature key is invalid. - - Variable key is invalid. - - Mismatch with type of variable. - """ + Returns: + Boolean value of the variable. None if: + - Feature key is invalid. + - Variable key is invalid. + - Mismatch with type of variable. + """ variable_type = entities.Variable.Type.BOOLEAN project_config = self.config_manager.get_config() @@ -726,18 +740,18 @@ def get_feature_variable_boolean(self, feature_key, variable_key, user_id, attri def get_feature_variable_double(self, feature_key, variable_key, user_id, attributes=None): """ Returns value for a certain double variable attached to a feature flag. - Args: - feature_key: Key of the feature whose variable's value is being accessed. - variable_key: Key of the variable whose value is to be accessed. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + feature_key: Key of the feature whose variable's value is being accessed. + variable_key: Key of the variable whose value is to be accessed. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - Double value of the variable. None if: - - Feature key is invalid. - - Variable key is invalid. - - Mismatch with type of variable. - """ + Returns: + Double value of the variable. None if: + - Feature key is invalid. + - Variable key is invalid. + - Mismatch with type of variable. + """ variable_type = entities.Variable.Type.DOUBLE project_config = self.config_manager.get_config() @@ -752,18 +766,18 @@ def get_feature_variable_double(self, feature_key, variable_key, user_id, attrib def get_feature_variable_integer(self, feature_key, variable_key, user_id, attributes=None): """ Returns value for a certain integer variable attached to a feature flag. - Args: - feature_key: Key of the feature whose variable's value is being accessed. - variable_key: Key of the variable whose value is to be accessed. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + feature_key: Key of the feature whose variable's value is being accessed. + variable_key: Key of the variable whose value is to be accessed. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - Integer value of the variable. None if: - - Feature key is invalid. - - Variable key is invalid. - - Mismatch with type of variable. - """ + Returns: + Integer value of the variable. None if: + - Feature key is invalid. + - Variable key is invalid. + - Mismatch with type of variable. + """ variable_type = entities.Variable.Type.INTEGER project_config = self.config_manager.get_config() @@ -778,18 +792,18 @@ def get_feature_variable_integer(self, feature_key, variable_key, user_id, attri def get_feature_variable_string(self, feature_key, variable_key, user_id, attributes=None): """ Returns value for a certain string variable attached to a feature. - Args: - feature_key: Key of the feature whose variable's value is being accessed. - variable_key: Key of the variable whose value is to be accessed. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + feature_key: Key of the feature whose variable's value is being accessed. + variable_key: Key of the variable whose value is to be accessed. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - String value of the variable. None if: - - Feature key is invalid. - - Variable key is invalid. - - Mismatch with type of variable. - """ + Returns: + String value of the variable. None if: + - Feature key is invalid. + - Variable key is invalid. + - Mismatch with type of variable. + """ variable_type = entities.Variable.Type.STRING project_config = self.config_manager.get_config() @@ -804,18 +818,18 @@ def get_feature_variable_string(self, feature_key, variable_key, user_id, attrib def get_feature_variable_json(self, feature_key, variable_key, user_id, attributes=None): """ Returns value for a certain JSON variable attached to a feature. - Args: - feature_key: Key of the feature whose variable's value is being accessed. - variable_key: Key of the variable whose value is to be accessed. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + feature_key: Key of the feature whose variable's value is being accessed. + variable_key: Key of the variable whose value is to be accessed. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - Dictionary object of the variable. None if: - - Feature key is invalid. - - Variable key is invalid. - - Mismatch with type of variable. - """ + Returns: + Dictionary object of the variable. None if: + - Feature key is invalid. + - Variable key is invalid. + - Mismatch with type of variable. + """ variable_type = entities.Variable.Type.JSON project_config = self.config_manager.get_config() @@ -830,15 +844,15 @@ def get_feature_variable_json(self, feature_key, variable_key, user_id, attribut def get_all_feature_variables(self, feature_key, user_id, attributes=None): """ Returns dictionary of all variables and their corresponding values in the context of a feature. - Args: - feature_key: Key of the feature whose variable's value is being accessed. - user_id: ID for user. - attributes: Dict representing user attributes. + Args: + feature_key: Key of the feature whose variable's value is being accessed. + user_id: ID for user. + attributes: Dict representing user attributes. - Returns: - Dictionary mapping variable key to variable value. None if: - - Feature key is invalid. - """ + Returns: + Dictionary mapping variable key to variable value. None if: + - Feature key is invalid. + """ project_config = self.config_manager.get_config() if not project_config: @@ -852,15 +866,15 @@ def get_all_feature_variables(self, feature_key, user_id, attributes=None): def set_forced_variation(self, experiment_key, user_id, variation_key): """ Force a user into a variation for a given experiment. - Args: - experiment_key: A string key identifying the experiment. - user_id: The user ID. - variation_key: A string variation key that specifies the variation which the user. - will be forced into. If null, then clear the existing experiment-to-variation mapping. + Args: + experiment_key: A string key identifying the experiment. + user_id: The user ID. + variation_key: A string variation key that specifies the variation which the user. + will be forced into. If null, then clear the existing experiment-to-variation mapping. - Returns: - A boolean value that indicates if the set completed successfully. - """ + Returns: + A boolean value that indicates if the set completed successfully. + """ if not self.is_valid: self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('set_forced_variation')) @@ -884,13 +898,13 @@ def set_forced_variation(self, experiment_key, user_id, variation_key): def get_forced_variation(self, experiment_key, user_id): """ Gets the forced variation for a given user and experiment. - Args: - experiment_key: A string key identifying the experiment. - user_id: The user ID. + Args: + experiment_key: A string key identifying the experiment. + user_id: The user ID. - Returns: - The forced variation key. None if no forced variation key. - """ + Returns: + The forced variation key. None if no forced variation key. + """ if not self.is_valid: self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('get_forced_variation')) @@ -954,7 +968,7 @@ def create_user_context(self, user_id, attributes=None): self.logger.error(enums.Errors.INVALID_INPUT.format('attributes')) return None - return OptimizelyUserContext(self, user_id, attributes) + return OptimizelyUserContext(self, self.logger, user_id, attributes) def _decide(self, user_context, key, decide_options=None): """ @@ -1019,18 +1033,28 @@ def _decide(self, user_context, key, decide_options=None): decision_source = DecisionSources.ROLLOUT source_info = {} decision_event_dispatched = False - ignore_ups = OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in decide_options - - decision, decision_reasons = self.decision_service.get_variation_for_feature(config, feature_flag, user_id, - attributes, ignore_ups) + # Check forced decisions first + optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(flag_key=key, rule_key=rule_key) + forced_decision_response = user_context.find_validated_forced_decision(optimizely_decision_context) + variation, decision_reasons = forced_decision_response reasons += decision_reasons + if variation: + decision = Decision(None, variation, enums.DecisionSources.FEATURE_TEST) + else: + # Regular decision + decision, decision_reasons = self.decision_service.get_variation_for_feature(config, + feature_flag, + user_context, decide_options) + + reasons += decision_reasons + # Fill in experiment and variation if returned (rollouts can have featureEnabled variables as well.) if decision.experiment is not None: experiment = decision.experiment source_info["experiment"] = experiment - rule_key = experiment.key + rule_key = experiment.key if experiment else None if decision.variation is not None: variation = decision.variation variation_key = variation.key @@ -1045,6 +1069,7 @@ def _decide(self, user_context, key, decide_options=None): self._send_impression_event(config, experiment, variation, flag_key, rule_key or '', decision_source, feature_enabled, user_id, attributes) + decision_event_dispatched = True # Generate all variables map if decide options doesn't include excludeVariables @@ -1123,7 +1148,6 @@ def _decide_all(self, user_context, decide_options=None): def _decide_for_keys(self, user_context, keys, decide_options=None): """ - Args: user_context: UserContent keys: list of feature keys to run decide on. diff --git a/optimizely/optimizely_user_context.py b/optimizely/optimizely_user_context.py index 9416f65d..64d0a3ba 100644 --- a/optimizely/optimizely_user_context.py +++ b/optimizely/optimizely_user_context.py @@ -13,19 +13,24 @@ # limitations under the License. # +import copy import threading +from .decision.optimizely_decision_message import OptimizelyDecisionMessage +from .helpers import enums + class OptimizelyUserContext(object): """ Representation of an Optimizely User Context using which APIs are to be called. """ - def __init__(self, optimizely_client, user_id, user_attributes=None): + def __init__(self, optimizely_client, logger, user_id, user_attributes=None): """ Create an instance of the Optimizely User Context. Args: optimizely_client: client used when calling decisions for this user context + logger: logger for logging user_id: user id of this user context user_attributes: user attributes to use for this user context @@ -34,6 +39,7 @@ def __init__(self, optimizely_client, user_id, user_attributes=None): """ self.client = optimizely_client + self.logger = logger self.user_id = user_id if not isinstance(user_attributes, dict): @@ -41,9 +47,40 @@ def __init__(self, optimizely_client, user_id, user_attributes=None): self._user_attributes = user_attributes.copy() if user_attributes else {} self.lock = threading.Lock() + self.forced_decisions_map = {} + + # decision context + class OptimizelyDecisionContext(object): + """ Using class with attributes here instead of namedtuple because + class is extensible, it's easy to add another attribute if we wanted + to extend decision context. + """ + def __init__(self, flag_key, rule_key=None): + self.flag_key = flag_key + self.rule_key = rule_key + + def __hash__(self): + return hash((self.flag_key, self.rule_key)) + + def __eq__(self, other): + return (self.flag_key, self.rule_key) == (other.flag_key, other.rule_key) + + # forced decision + class OptimizelyForcedDecision(object): + def __init__(self, variation_key): + self.variation_key = variation_key def _clone(self): - return OptimizelyUserContext(self.client, self.user_id, self.get_user_attributes()) + if not self.client: + return None + + user_context = OptimizelyUserContext(self.client, self.logger, self.user_id, self.get_user_attributes()) + + with self.lock: + if self.forced_decisions_map: + user_context.forced_decisions_map = copy.deepcopy(self.forced_decisions_map) + + return user_context def get_user_attributes(self): with self.lock: @@ -114,3 +151,151 @@ def as_json(self): 'user_id': self.user_id, 'attributes': self.get_user_attributes(), } + + def set_forced_decision(self, decision_context, decision): + """ + Sets the forced decision for a given decision context. + + Args: + decision_context: a decision context. + decision: a forced decision. + + Returns: + True if the forced decision has been set successfully. + """ + if not self.client.is_valid or not self.client.config_manager.get_config(): + self.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY) + return False + + with self.lock: + self.forced_decisions_map[decision_context] = decision + + return True + + def get_forced_decision(self, decision_context): + """ + Gets the forced decision (variation key) for a given decision context. + + Args: + decision_context: a decision context. + + Returns: + A forced_decision or None if forced decisions are not set for the parameters. + """ + if not self.client.is_valid or not self.client.config_manager.get_config(): + self.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY) + return None + + forced_decision = self.find_forced_decision(decision_context) + + return forced_decision + + def remove_forced_decision(self, decision_context): + """ + Removes the forced decision for a given decision context. + + Args: + decision_context: a decision context. + + Returns: + Returns: true if the forced decision has been removed successfully. + """ + if not self.client.is_valid or not self.client.config_manager.get_config(): + self.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY) + return False + + with self.lock: + if decision_context in self.forced_decisions_map: + del self.forced_decisions_map[decision_context] + return True + + return False + + def remove_all_forced_decisions(self): + """ + Removes all forced decisions bound to this user context. + + Returns: + True if forced decisions have been removed successfully. + """ + if not self.client.is_valid or not self.client.config_manager.get_config(): + self.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY) + return False + + with self.lock: + self.forced_decisions_map.clear() + + return True + + def find_forced_decision(self, decision_context): + """ + Gets forced decision from forced decision map. + + Args: + decision_context: a decision context. + + Returns: + Forced decision. + """ + with self.lock: + if not self.forced_decisions_map: + return None + + # must allow None to be returned for the Flags only case + return self.forced_decisions_map.get(decision_context) + + def find_validated_forced_decision(self, decision_context): + """ + Gets forced decisions based on flag key, rule key and variation. + + Args: + decision context: a decision context + + Returns: + Variation of the forced decision. + """ + reasons = [] + + forced_decision = self.find_forced_decision(decision_context) + + flag_key = decision_context.flag_key + rule_key = decision_context.rule_key + + if forced_decision: + # we use config here so we can use get_flag_variation() function which is defined in project_config + # otherwise we would us self.client instead of config + config = self.client.config_manager.get_config() if self.client else None + variation = config.get_flag_variation(flag_key, 'key', forced_decision.variation_key) + if variation: + if rule_key: + user_has_forced_decision = enums.ForcedDecisionLogs \ + .USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED.format(forced_decision.variation_key, + flag_key, + rule_key, + self.user_id) + + else: + user_has_forced_decision = enums.ForcedDecisionLogs \ + .USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED.format(forced_decision.variation_key, + flag_key, + self.user_id) + + reasons.append(user_has_forced_decision) + self.logger.debug(user_has_forced_decision) + + return variation, reasons + + else: + if rule_key: + user_has_forced_decision_but_invalid = enums.ForcedDecisionLogs \ + .USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID.format(flag_key, + rule_key, + self.user_id) + else: + user_has_forced_decision_but_invalid = enums.ForcedDecisionLogs \ + .USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED_BUT_INVALID.format(flag_key, self.user_id) + + reasons.append(user_has_forced_decision_but_invalid) + self.logger.debug(user_has_forced_decision_but_invalid) + + return None, reasons diff --git a/optimizely/project_config.py b/optimizely/project_config.py index 8a696599..2ba5c0f5 100644 --- a/optimizely/project_config.py +++ b/optimizely/project_config.py @@ -12,11 +12,12 @@ # limitations under the License. import json +from collections import OrderedDict -from .helpers import condition as condition_helper -from .helpers import enums from . import entities from . import exceptions +from .helpers import condition as condition_helper +from .helpers import enums SUPPORTED_VERSIONS = [ enums.DatafileVersions.V2, @@ -123,21 +124,42 @@ def __init__(self, datafile, logger, error_handler): # As we cannot create json variables in datafile directly, here we convert # the variables of string type and json subType to json type # This is needed to fully support json variables - for feature in self.feature_key_map: - for variable in self.feature_key_map[feature].variables: + self.experiment_feature_map = {} + self.flag_rules_map = {} + + for feature_key, feature_value in self.feature_key_map.items(): + for variable in self.feature_key_map[feature_key].variables: sub_type = variable.get('subType', '') if variable['type'] == entities.Variable.Type.STRING and sub_type == entities.Variable.Type.JSON: variable['type'] = entities.Variable.Type.JSON - # Dict containing map of experiment ID to feature ID. - # for checking that experiment is a feature experiment or not. - self.experiment_feature_map = {} - for feature in self.feature_key_map.values(): - feature.variables = self._generate_key_map(feature.variables, 'key', entities.Variable) - - for exp_id in feature.experimentIds: + # loop over features=flags already happening + # get feature variables for eacg flag/feature + feature_value.variables = self._generate_key_map(feature_value.variables, 'key', entities.Variable) + for exp_id in feature_value.experimentIds: # Add this experiment in experiment-feature map. - self.experiment_feature_map[exp_id] = [feature.id] + self.experiment_feature_map[exp_id] = [feature_value.id] + + # all rules(experiment rules and delivery rules) for each flag + for flag in self.feature_flags: + experiments = [] + if len(flag['experimentIds']) > 0: + for exp_id in flag['experimentIds']: + experiments.append(self.experiment_id_map[exp_id]) + if not flag['rolloutId'] == '': + rollout = self.rollout_id_map[flag['rolloutId']] + + rollout_experiments = self.get_rollout_experiments(rollout) + + if rollout and rollout.experiments: + experiments.extend(rollout_experiments) + + self.flag_rules_map[flag['key']] = experiments + + # All variations for each flag + # Datafile does not contain a separate entity for this. + # We collect variations used in each rule (experiment rules and delivery rules) + self.flag_variations_map = self.get_all_variations_for_each_rule(self.flag_rules_map) @staticmethod def _generate_key_map(entity_list, key, entity_class): @@ -152,7 +174,10 @@ def _generate_key_map(entity_list, key, entity_class): Map mapping key to entity object. """ - key_map = {} + # using ordered dict here to preserve insertion order of entities + # OrderedDict() is needed for Py versions 3.5 and less to work. + # Insertion order has been made default in dicts since Py 3.6 + key_map = OrderedDict() for obj in entity_list: key_map[obj[key]] = entity_class(**obj) @@ -175,6 +200,21 @@ def _deserialize_audience(audience_map): return audience_map + def get_rollout_experiments(self, rollout): + """ Helper method to get rollout experiments. + + Args: + rollout: rollout + + Returns: + Mapped rollout experiments. + """ + + rollout_experiments_id_map = self._generate_key_map(rollout.experiments, 'id', entities.Experiment) + rollout_experiments = [experiment for experiment in rollout_experiments_id_map.values()] + + return rollout_experiments + def get_typecast_value(self, value, type): """ Helper method to determine actual value based on type of feature variable. @@ -340,6 +380,7 @@ def get_variation_from_key(self, experiment_key, variation_key): Args: experiment: Key representing parent experiment of variation. variation_key: Key representing the variation. + Variation is of type variation object or None. Returns Object representing the variation. @@ -485,7 +526,6 @@ def get_variable_value_for_variation(self, variable, variation): if not variable or not variation: return None - if variation.id not in self.variation_variable_usage_map: self.logger.error('Variation with ID "%s" is not in the datafile.' % variation.id) return None @@ -598,3 +638,63 @@ def get_variation_from_key_by_experiment_id(self, experiment_id, variation_key): variation_key, experiment_id) return {} + + def get_all_variations_for_each_rule(self, flag_rules_map): + """ Helper method to get all variation objects from each flag. + collects variations used in each rule (experiment rules and delivery rules). + + Args: + flag_rules_map: A dictionary. A map of all rules per flag. + + Returns: + Map of flag variations. + """ + flag_variations_map = {} + + for flag_key, rules in flag_rules_map.items(): + variations = [] + for rule in rules: + # get variations as objects (rule.variations gives list) + variation_objects = self.variation_id_map_by_experiment_id[rule.id].values() + for variation in variation_objects: + # append variation if it's not already in the list + if variation not in variations: + variations.append(variation) + + flag_variations_map[flag_key] = variations + + return flag_variations_map + + def get_flag_variation(self, flag_key, variation_attribute, target_value): + """ + Gets variation by specified variation attribute. + For example if variation_attribute is id, the function gets variation by using variation_id. + If object_attribute is key, the function gets variation by using variation_key. + + We used to have two separate functions: + get_flag_variation_by_id() + get_flag_variation_by_key() + + This function consolidates both functions into one. + + Important to always relate object_attribute to the target value. + Should never enter for example object_attribute=key and target_value=variation_id. + Correct is object_attribute=key and target_value=variation_key. + + Args: + flag_key: flag key + variation_attribute: id or key for example. The part after the dot notation (id in variation.id) + target_value: target value we want to get for example variation_id or variation_key + + Returns: + Variation as a map. + """ + if not flag_key: + return None + + variations = self.flag_variations_map.get(flag_key) + for variation in variations: + if getattr(variation, variation_attribute) == target_value: + return variation + + return None diff --git a/tests/test_config.py b/tests/test_config.py index fe0f8f38..96450368 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -20,7 +20,6 @@ from optimizely import logger from optimizely import optimizely from optimizely.helpers import enums - from . import base diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index 97fefce7..635d22e0 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -12,11 +12,13 @@ # limitations under the License. import json + import mock from optimizely import decision_service from optimizely import entities from optimizely import optimizely +from optimizely import optimizely_user_context from optimizely import user_profile from optimizely.helpers import enums from . import base @@ -51,7 +53,7 @@ def test_get_bucketing_id__no_bucketing_id_attribute(self): def test_get_bucketing_id__bucketing_id_attribute(self): """ Test that _get_bucketing_id returns correct bucketing ID when there is bucketing ID attribute. """ with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: bucketing_id, _ = self.decision_service._get_bucketing_id( "test_user", {"$opt_bucketing_id": "user_bucket_value"} @@ -65,7 +67,7 @@ def test_get_bucketing_id__bucketing_id_attribute(self): def test_get_bucketing_id__bucketing_id_attribute_not_a_string(self): """ Test that _get_bucketing_id returns user ID as bucketing ID when bucketing ID attribute is not a string""" with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: bucketing_id, _ = self.decision_service._get_bucketing_id( "test_user", {"$opt_bucketing_id": True} @@ -140,7 +142,7 @@ def test_set_forced_variation__invalid_variation_key(self): ) ) with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: self.assertIs( self.decision_service.set_forced_variation( @@ -246,7 +248,7 @@ def test_set_forced_variation_when_called_to_remove_forced_variation(self): ) with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: self.assertTrue( self.decision_service.set_forced_variation( @@ -264,7 +266,7 @@ def test_set_forced_variation_when_called_to_remove_forced_variation(self): ) with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: self.assertTrue( self.decision_service.set_forced_variation( @@ -326,7 +328,7 @@ def test_get_forced_variation_with_none_set_for_user(self): self.decision_service.forced_variation_map["test_user"] = {} with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: variation, _ = self.decision_service.get_forced_variation( self.project_config, "test_experiment", "test_user" @@ -347,7 +349,7 @@ def test_get_forced_variation_missing_variation_mapped_to_experiment(self): ] = None with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: variation, _ = self.decision_service.get_forced_variation( self.project_config, "test_experiment", "test_user" @@ -365,7 +367,7 @@ def test_get_whitelisted_variation__user_in_forced_variation(self): experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: variation, _ = self.decision_service.get_whitelisted_variation( self.project_config, experiment, "user_1" @@ -384,8 +386,8 @@ def test_get_whitelisted_variation__user_in_invalid_variation(self): experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch( - "optimizely.project_config.ProjectConfig.get_variation_from_key", - return_value=None, + "optimizely.project_config.ProjectConfig.get_variation_from_key", + return_value=None, ) as mock_get_variation_id: variation, _ = self.decision_service.get_whitelisted_variation( self.project_config, experiment, "user_1" @@ -404,7 +406,7 @@ def test_get_stored_variation__stored_decision_available(self): "test_user", experiment_bucket_map={"111127": {"variation_id": "111128"}} ) with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging: variation = self.decision_service.get_stored_variation( self.project_config, experiment, profile @@ -433,11 +435,15 @@ def test_get_stored_variation__no_stored_decision_available(self): def test_get_variation__experiment_not_running(self): """ Test that get_variation returns None if experiment is not Running. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") # Mark experiment paused experiment.status = "Paused" with mock.patch( - "optimizely.decision_service.DecisionService.get_forced_variation" + "optimizely.decision_service.DecisionService.get_forced_variation" ) as mock_get_forced_variation, mock.patch.object( self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( @@ -452,7 +458,7 @@ def test_get_variation__experiment_not_running(self): "optimizely.user_profile.UserProfileService.save" ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user, None ) self.assertIsNone( variation @@ -472,10 +478,17 @@ def test_get_variation__experiment_not_running(self): def test_get_variation__bucketing_id_provided(self): """ Test that get_variation calls bucket with correct bucketing ID if provided. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={ + "random_key": "random_value", + "$opt_bucketing_id": "user_bucket_value", + }) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch( - "optimizely.decision_service.DecisionService.get_forced_variation", - return_value=[None, []], + "optimizely.decision_service.DecisionService.get_forced_variation", + return_value=[None, []], ), mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", return_value=None, @@ -488,11 +501,7 @@ def test_get_variation__bucketing_id_provided(self): variation, _ = self.decision_service.get_variation( self.project_config, experiment, - "test_user", - { - "random_key": "random_value", - "$opt_bucketing_id": "user_bucket_value", - }, + user ) # Assert that bucket is called with appropriate bucketing ID @@ -503,10 +512,13 @@ def test_get_variation__bucketing_id_provided(self): def test_get_variation__user_whitelisted_for_variation(self): """ Test that get_variation returns whitelisted variation if user is whitelisted. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch( - "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=[entities.Variation("111128", "control"), []], + "optimizely.decision_service.DecisionService.get_whitelisted_variation", + return_value=[entities.Variation("111128", "control"), []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation" ) as mock_get_stored_variation, mock.patch( @@ -519,7 +531,7 @@ def test_get_variation__user_whitelisted_for_variation(self): "optimizely.user_profile.UserProfileService.save" ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user ) self.assertEqual( entities.Variation("111128", "control"), @@ -539,10 +551,13 @@ def test_get_variation__user_whitelisted_for_variation(self): def test_get_variation__user_has_stored_decision(self): """ Test that get_variation returns stored decision if user has variation available for given experiment. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch( - "optimizely.decision_service.DecisionService.get_whitelisted_variation", - return_value=[None, []], + "optimizely.decision_service.DecisionService.get_whitelisted_variation", + return_value=[None, []], ) as mock_get_whitelisted_variation, mock.patch( "optimizely.decision_service.DecisionService.get_stored_variation", return_value=entities.Variation("111128", "control"), @@ -560,7 +575,7 @@ def test_get_variation__user_has_stored_decision(self): "optimizely.user_profile.UserProfileService.save" ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user, None ) self.assertEqual( entities.Variation("111128", "control"), @@ -584,14 +599,18 @@ def test_get_variation__user_has_stored_decision(self): self.assertEqual(0, mock_save.call_count) def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_available( - self, + self, ): """ Test that get_variation buckets and returns variation if no forced variation or decision available. Also, stores decision if user profile service is available. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", return_value=[None, []], @@ -610,7 +629,7 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_a "optimizely.user_profile.UserProfileService.save" ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user, None ) self.assertEqual( entities.Variation("111129", "variation"), @@ -619,7 +638,7 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_a # Assert that user is bucketed and new decision is stored mock_get_whitelisted_variation.assert_called_once_with( - self.project_config, experiment, "test_user" + self.project_config, experiment, user.user_id ) mock_lookup.assert_called_once_with("test_user") self.assertEqual(1, mock_get_stored_variation.call_count) @@ -628,7 +647,7 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_a experiment.get_audience_conditions_or_ids(), enums.ExperimentAudienceEvaluationLogs, "test_experiment", - None, + user.get_user_attributes(), mock_decision_service_logging ) mock_bucket.assert_called_once_with( @@ -642,7 +661,7 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_a ) def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_not_available( - self, + self, ): """ Test that get_variation buckets and returns variation if no forced variation and no user profile service available. """ @@ -650,9 +669,13 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_n # Unset user profile service self.decision_service.user_profile_service = None + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", return_value=[None, []], @@ -669,7 +692,7 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_n "optimizely.user_profile.UserProfileService.save" ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user, None ) self.assertEqual( entities.Variation("111129", "variation"), @@ -687,7 +710,7 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_n experiment.get_audience_conditions_or_ids(), enums.ExperimentAudienceEvaluationLogs, "test_experiment", - None, + user.get_user_attributes(), mock_decision_service_logging ) mock_bucket.assert_called_once_with( @@ -698,9 +721,13 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_n def test_get_variation__user_does_not_meet_audience_conditions(self): """ Test that get_variation returns None if user is not in experiment. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", return_value=[None, []], @@ -718,7 +745,7 @@ def test_get_variation__user_does_not_meet_audience_conditions(self): "optimizely.user_profile.UserProfileService.save" ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user, None ) self.assertIsNone( variation @@ -737,7 +764,7 @@ def test_get_variation__user_does_not_meet_audience_conditions(self): experiment.get_audience_conditions_or_ids(), enums.ExperimentAudienceEvaluationLogs, "test_experiment", - None, + user.get_user_attributes(), mock_decision_service_logging ) self.assertEqual(0, mock_bucket.call_count) @@ -746,9 +773,13 @@ def test_get_variation__user_does_not_meet_audience_conditions(self): def test_get_variation__user_profile_in_invalid_format(self): """ Test that get_variation handles invalid user profile gracefully. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", return_value=[None, []], @@ -766,7 +797,7 @@ def test_get_variation__user_profile_in_invalid_format(self): "optimizely.user_profile.UserProfileService.save" ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user, None ) self.assertEqual( entities.Variation("111129", "variation"), @@ -785,7 +816,7 @@ def test_get_variation__user_profile_in_invalid_format(self): experiment.get_audience_conditions_or_ids(), enums.ExperimentAudienceEvaluationLogs, "test_experiment", - None, + user.get_user_attributes(), mock_decision_service_logging ) mock_decision_service_logging.warning.assert_called_once_with( @@ -804,9 +835,13 @@ def test_get_variation__user_profile_in_invalid_format(self): def test_get_variation__user_profile_lookup_fails(self): """ Test that get_variation acts gracefully when lookup fails. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", return_value=[None, []], @@ -824,7 +859,7 @@ def test_get_variation__user_profile_lookup_fails(self): "optimizely.user_profile.UserProfileService.save" ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user, None ) self.assertEqual( entities.Variation("111129", "variation"), @@ -843,7 +878,7 @@ def test_get_variation__user_profile_lookup_fails(self): experiment.get_audience_conditions_or_ids(), enums.ExperimentAudienceEvaluationLogs, "test_experiment", - None, + user.get_user_attributes(), mock_decision_service_logging ) mock_decision_service_logging.exception.assert_called_once_with( @@ -862,9 +897,13 @@ def test_get_variation__user_profile_lookup_fails(self): def test_get_variation__user_profile_save_fails(self): """ Test that get_variation acts gracefully when save fails. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", return_value=[None, []], @@ -882,7 +921,7 @@ def test_get_variation__user_profile_save_fails(self): side_effect=Exception("major problem"), ) as mock_save: variation, _ = self.decision_service.get_variation( - self.project_config, experiment, "test_user", None + self.project_config, experiment, user, None ) self.assertEqual( entities.Variation("111129", "variation"), @@ -900,9 +939,10 @@ def test_get_variation__user_profile_save_fails(self): experiment.get_audience_conditions_or_ids(), enums.ExperimentAudienceEvaluationLogs, "test_experiment", - None, + user.get_user_attributes(), mock_decision_service_logging ) + mock_decision_service_logging.exception.assert_called_once_with( 'Unable to save user profile for user "test_user".' ) @@ -919,9 +959,13 @@ def test_get_variation__user_profile_save_fails(self): def test_get_variation__ignore_user_profile_when_specified(self): """ Test that we ignore the user profile service if specified. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) experiment = self.project_config.get_experiment_from_key("test_experiment") with mock.patch.object( - self.decision_service, "logger" + self.decision_service, "logger" ) as mock_decision_service_logging, mock.patch( "optimizely.decision_service.DecisionService.get_whitelisted_variation", return_value=[None, []], @@ -938,9 +982,8 @@ def test_get_variation__ignore_user_profile_when_specified(self): variation, _ = self.decision_service.get_variation( self.project_config, experiment, - "test_user", - None, - ignore_user_profile=True, + user, + options=['IGNORE_USER_PROFILE_SERVICE'], ) self.assertEqual( entities.Variation("111129", "variation"), @@ -956,7 +999,7 @@ def test_get_variation__ignore_user_profile_when_specified(self): experiment.get_audience_conditions_or_ids(), enums.ExperimentAudienceEvaluationLogs, "test_experiment", - None, + user.get_user_attributes(), mock_decision_service_logging ) mock_bucket.assert_called_once_with( @@ -976,13 +1019,23 @@ def setUp(self): self.mock_config_logger = mock.patch.object(self.project_config, "logger") def test_get_variation_for_rollout__returns_none_if_no_experiments(self): - """ Test that get_variation_for_rollout returns None if there are no experiments (targeting rules). """ + """ Test that get_variation_for_rollout returns None if there are no experiments (targeting rules). + For this we assign None to the feature parameter. + There is one rolloutId in the datafile that has no experiments associsted with it. + rolloutId is tied to feature. That's why we make feature None which means there are no experiments. + """ + + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) with self.mock_config_logger as mock_logging: - no_experiment_rollout = self.project_config.get_rollout_from_id("201111") + feature = None variation_received, _ = self.decision_service.get_variation_for_rollout( - self.project_config, no_experiment_rollout, "test_user" + self.project_config, feature, user ) + self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), variation_received, @@ -995,16 +1048,20 @@ def test_get_variation_for_rollout__returns_decision_if_user_in_rollout(self): """ Test that get_variation_for_rollout returns Decision with experiment/variation if user meets targeting conditions for a rollout rule. """ - rollout = self.project_config.get_rollout_from_id("211111") + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) + feature = self.project_config.get_feature_from_key("test_feature_in_rollout") with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ), self.mock_decision_logger as mock_decision_service_logging, mock.patch( "optimizely.bucketer.Bucketer.bucket", return_value=[self.project_config.get_variation_from_id("211127", "211129"), []], ) as mock_bucket: variation_received, _ = self.decision_service.get_variation_for_rollout( - self.project_config, rollout, "test_user" + self.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1017,33 +1074,36 @@ def test_get_variation_for_rollout__returns_decision_if_user_in_rollout(self): # Check all log messages mock_decision_service_logging.debug.assert_has_calls([ - mock.call('User "test_user" meets audience conditions for targeting rule 1.')] - ) + mock.call('User "test_user" meets audience conditions for targeting rule 1.'), + mock.call('User "test_user" bucketed into a targeting rule 1.')]) # Check that bucket is called with correct parameters mock_bucket.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_id("211127"), "test_user", - "test_user", + 'test_user', ) def test_get_variation_for_rollout__calls_bucket_with_bucketing_id(self): """ Test that get_variation_for_rollout calls Bucketer.bucket with bucketing ID when provided. """ - rollout = self.project_config.get_rollout_from_id("211111") + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"$opt_bucketing_id": "user_bucket_value"}) + feature = self.project_config.get_feature_from_key("test_feature_in_rollout") with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ), self.mock_decision_logger as mock_decision_service_logging, mock.patch( "optimizely.bucketer.Bucketer.bucket", return_value=[self.project_config.get_variation_from_id("211127", "211129"), []], ) as mock_bucket: variation_received, _ = self.decision_service.get_variation_for_rollout( self.project_config, - rollout, - "test_user", - {"$opt_bucketing_id": "user_bucket_value"}, + feature, + user ) self.assertEqual( decision_service.Decision( @@ -1063,26 +1123,30 @@ def test_get_variation_for_rollout__calls_bucket_with_bucketing_id(self): self.project_config, self.project_config.get_experiment_from_id("211127"), "test_user", - "user_bucket_value", + 'user_bucket_value' ) def test_get_variation_for_rollout__skips_to_everyone_else_rule(self): """ Test that if a user is in an audience, but does not qualify for the experiment, then it skips to the Everyone Else rule. """ - rollout = self.project_config.get_rollout_from_id("211111") + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) + feature = self.project_config.get_feature_from_key("test_feature_in_rollout") everyone_else_exp = self.project_config.get_experiment_from_id("211147") variation_to_mock = self.project_config.get_variation_from_id( "211147", "211149" ) with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[True, []] ) as mock_audience_check, self.mock_decision_logger as mock_decision_service_logging, mock.patch( "optimizely.bucketer.Bucketer.bucket", side_effect=[[None, []], [variation_to_mock, []]] ): variation_received, _ = self.decision_service.get_variation_for_rollout( - self.project_config, rollout, "test_user" + self.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1099,7 +1163,7 @@ def test_get_variation_for_rollout__skips_to_everyone_else_rule(self): self.project_config.get_experiment_from_key("211127").get_audience_conditions_or_ids(), enums.RolloutRuleAudienceEvaluationLogs, '1', - None, + user.get_user_attributes(), mock_decision_service_logging, ), mock.call( @@ -1107,7 +1171,7 @@ def test_get_variation_for_rollout__skips_to_everyone_else_rule(self): self.project_config.get_experiment_from_key("211147").get_audience_conditions_or_ids(), enums.RolloutRuleAudienceEvaluationLogs, 'Everyone Else', - None, + user.get_user_attributes(), mock_decision_service_logging, ), ], @@ -1118,26 +1182,26 @@ def test_get_variation_for_rollout__skips_to_everyone_else_rule(self): mock_decision_service_logging.debug.assert_has_calls( [ mock.call('User "test_user" meets audience conditions for targeting rule 1.'), - mock.call( - 'User "test_user" is not in the traffic group for targeting rule 1. ' - 'Checking "Everyone Else" rule now.' - ), - mock.call( - 'User "test_user" meets conditions for targeting rule "Everyone Else".' - ), + mock.call('User "test_user" not bucketed into a targeting rule 1. Checking "Everyone Else" rule now.'), + mock.call('User "test_user" meets audience conditions for targeting rule Everyone Else.'), + mock.call('User "test_user" bucketed into a targeting rule Everyone Else.'), ] ) def test_get_variation_for_rollout__returns_none_for_user_not_in_rollout(self): """ Test that get_variation_for_rollout returns None for the user not in the associated rollout. """ - rollout = self.project_config.get_rollout_from_id("211111") + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) + feature = self.project_config.get_feature_from_key("test_feature_in_rollout") with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[False, []] + "optimizely.helpers.audience.does_user_meet_audience_conditions", return_value=[False, []] ) as mock_audience_check, self.mock_decision_logger as mock_decision_service_logging: variation_received, _ = self.decision_service.get_variation_for_rollout( - self.project_config, rollout, "test_user" + self.project_config, feature, user ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), @@ -1152,7 +1216,7 @@ def test_get_variation_for_rollout__returns_none_for_user_not_in_rollout(self): self.project_config.get_experiment_from_key("211127").get_audience_conditions_or_ids(), enums.RolloutRuleAudienceEvaluationLogs, "1", - None, + user.get_user_attributes(), mock_decision_service_logging, ), mock.call( @@ -1160,7 +1224,7 @@ def test_get_variation_for_rollout__returns_none_for_user_not_in_rollout(self): self.project_config.get_experiment_from_key("211137").get_audience_conditions_or_ids(), enums.RolloutRuleAudienceEvaluationLogs, "2", - None, + user.get_user_attributes(), mock_decision_service_logging, ), mock.call( @@ -1168,7 +1232,7 @@ def test_get_variation_for_rollout__returns_none_for_user_not_in_rollout(self): self.project_config.get_experiment_from_key("211147").get_audience_conditions_or_ids(), enums.RolloutRuleAudienceEvaluationLogs, "Everyone Else", - None, + user.get_user_attributes(), mock_decision_service_logging, ), ], @@ -1179,20 +1243,24 @@ def test_get_variation_for_rollout__returns_none_for_user_not_in_rollout(self): mock_decision_service_logging.debug.assert_has_calls( [ mock.call( - 'User "test_user" does not meet conditions for targeting rule 1.' + 'User "test_user" does not meet audience conditions for targeting rule 1.' ), mock.call( - 'User "test_user" does not meet conditions for targeting rule 2.' + 'User "test_user" does not meet audience conditions for targeting rule 2.' ), ] ) def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( - self, + self, ): """ Test that get_variation_for_feature returns the variation of the experiment the feature is associated with. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) feature = self.project_config.get_feature_from_key("test_feature_in_experiment") expected_experiment = self.project_config.get_experiment_from_key( @@ -1207,7 +1275,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( ) with decision_patch as mock_decision, self.mock_decision_logger: variation_received, _ = self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" + self.project_config, feature, user, options=None ) self.assertEqual( decision_service.Decision( @@ -1221,15 +1289,18 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( mock_decision.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_key("test_experiment"), - "test_user", - None, - False + user, + None ) def test_get_variation_for_feature__returns_variation_for_feature_in_rollout(self): """ Test that get_variation_for_feature returns the variation of the experiment in the rollout that the user is bucketed into. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) feature = self.project_config.get_feature_from_key("test_feature_in_rollout") expected_variation = self.project_config.get_variation_from_id( @@ -1242,16 +1313,15 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_rollout(sel with get_variation_for_rollout_patch as mock_get_variation_for_rollout, \ 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, user, False ) self.assertEqual( expected_variation, variation_received, ) - expected_rollout = self.project_config.get_rollout_from_id("211111") mock_get_variation_for_rollout.assert_called_once_with( - self.project_config, expected_rollout, "test_user", None + self.project_config, feature, user ) # Assert no log messages were generated @@ -1259,11 +1329,15 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_rollout(sel self.assertEqual(0, len(mock_decision_service_logging.method_calls)) def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_but_in_rollout( - self, + self, ): """ Test that get_variation_for_feature returns the variation of the experiment in the feature's rollout even if the user is not bucketed into the feature's experiment. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) feature = self.project_config.get_feature_from_key( "test_feature_in_experiment_and_rollout" ) @@ -1273,13 +1347,13 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_ "211127", "211129" ) with mock.patch( - "optimizely.helpers.audience.does_user_meet_audience_conditions", - side_effect=[[False, []], [True, []]], - ) as mock_audience_check, self.mock_decision_logger as mock_decision_service_logging, mock.patch( + "optimizely.helpers.audience.does_user_meet_audience_conditions", + side_effect=[[False, []], [True, []]], + ) as mock_audience_check, \ + self.mock_decision_logger as mock_decision_service_logging, mock.patch( "optimizely.bucketer.Bucketer.bucket", return_value=[expected_variation, []]): - decision, _ = self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" + self.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1296,7 +1370,7 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_ self.project_config.get_experiment_from_key("group_exp_2").get_audience_conditions_or_ids(), enums.ExperimentAudienceEvaluationLogs, "group_exp_2", - None, + {}, mock_decision_service_logging, ) @@ -1305,7 +1379,7 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_ self.project_config.get_experiment_from_key("211127").get_audience_conditions_or_ids(), enums.RolloutRuleAudienceEvaluationLogs, "1", - None, + user.get_user_attributes(), mock_decision_service_logging, ) @@ -1313,6 +1387,10 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) """ Test that get_variation_for_feature returns the variation of the experiment the user is bucketed in the feature's group. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) feature = self.project_config.get_feature_from_key("test_feature_in_group") expected_experiment = self.project_config.get_experiment_from_key("group_exp_1") @@ -1320,11 +1398,11 @@ 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_variation", - return_value=(expected_variation, []), + "optimizely.decision_service.DecisionService.get_variation", + return_value=(expected_variation, []), ) as mock_decision: variation_received, _ = self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" + self.project_config, feature, user, options=None ) self.assertEqual( decision_service.Decision( @@ -1338,22 +1416,25 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) mock_decision.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_key("group_exp_1"), - "test_user", - None, - False + user, + None ) 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. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) feature = self.project_config.get_feature_from_key("test_feature_in_experiment") with mock.patch( - "optimizely.decision_service.DecisionService.get_variation", - return_value=[None, []], + "optimizely.decision_service.DecisionService.get_variation", + return_value=[None, []], ) as mock_decision: variation_received, _ = self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" + self.project_config, feature, user ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), @@ -1363,24 +1444,27 @@ 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 + user, + None ) def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_not_associated_with_feature( - self, + self, ): """ Test that if a user is in the mutex group but the experiment is not targeting a feature, then None is returned. """ + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={}) feature = self.project_config.get_feature_from_key("test_feature_in_group") with mock.patch( - "optimizely.decision_service.DecisionService.get_variation", - return_value=[None, []], + "optimizely.decision_service.DecisionService.get_variation", + return_value=[None, []], ) as mock_decision: variation_received, _ = self.decision_service.get_variation_for_feature( - self.project_config, feature, "test_user" + self.project_config, feature, user, False ) self.assertEqual( decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), @@ -1388,26 +1472,28 @@ def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_no ) mock_decision.assert_called_once_with( - self.project_config, self.project_config.get_experiment_from_id("32222"), "test_user", None, False + self.project_config, self.project_config.get_experiment_from_id("32222"), user, False ) def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group_bucket_less_than_2500( - self, + self, ): """ Test that if a user is in the mutex group and the user bucket value should be less than 2500.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"experiment_attr": "group_experiment"}) 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,\ + '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.project_config, feature, user ) self.assertEqual( @@ -1423,23 +1509,26 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group 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, + 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.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"experiment_attr": "group_experiment"}) + 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,\ + '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.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1453,24 +1542,26 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group 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, + self, ): """ Test that if a user is in the mutex group and the user bucket value should be equal to 5000 or less than 7500.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"experiment_attr": "group_experiment"}) 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.bucketer.Bucketer._generate_bucket_value', return_value=6500) as mock_generate_bucket_value,\ + '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.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1484,19 +1575,23 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group mock_generate_bucket_value.assert_called_with('test_user42224') def test_get_variation_for_feature__returns_variation_for_rollout_in_mutex_group_bucket_greater_than_7500( - self, + self, ): """ Test that if a user is in the mutex group and the user bucket value should be greater than 7500.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"experiment_attr": "group_experiment"}) 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,\ + '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.project_config, feature, user ) + self.assertEqual( decision_service.Decision( None, @@ -1506,27 +1601,30 @@ def test_get_variation_for_feature__returns_variation_for_rollout_in_mutex_group 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".') + 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, + self, ): """ Test that if a user is in the non-mutex group and the user bucket value should be less than 2500.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"experiment_attr": "group_experiment"}) 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,\ + '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.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1540,24 +1638,25 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_ 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, + 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.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"experiment_attr": "group_experiment"}) 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"} - with mock.patch( - 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4000) as mock_generate_bucket_value,\ + '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.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1571,24 +1670,26 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_ mock_generate_bucket_value.assert_called_with('test_user111135') def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_bucket_range_5000_7500( - self, + 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.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"experiment_attr": "group_experiment"}) 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,\ + '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.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1606,13 +1707,17 @@ def test_get_variation_for_feature__returns_variation_for_rollout_in_experiment_ ): """ Test that if a user is in the non-mutex group and the user bucket value should be greater than 7500.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={"experiment_attr": "group_experiment"}) 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._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.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1622,9 +1727,9 @@ def test_get_variation_for_feature__returns_variation_for_rollout_in_experiment_ ), 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".') + 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, @@ -1632,19 +1737,22 @@ def test_get_variation_for_feature__returns_variation_for_rollout_in_mutex_group """ 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.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={ + "experiment_attr": "group_experiment_invalid"}) 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.project_config, feature, user ) - self.assertEqual( decision_service.Decision( expected_experiment, @@ -1654,8 +1762,9 @@ def test_get_variation_for_feature__returns_variation_for_rollout_in_mutex_group 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') + 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, @@ -1663,18 +1772,22 @@ def test_get_variation_for_feature_returns_rollout_in_experiment_bucket_range_25 """ 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.""" + user = optimizely_user_context.OptimizelyUserContext(optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={ + "experiment_attr": "group_experiment_invalid"}) 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.project_config, feature, user ) self.assertEqual( decision_service.Decision( @@ -1684,5 +1797,6 @@ def test_get_variation_for_feature_returns_rollout_in_experiment_bucket_range_25 ), 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') + 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.py b/tests/test_optimizely.py index 23454342..185f9033 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -12,9 +12,10 @@ # limitations under the License. import json -import mock from operator import itemgetter +import mock + from optimizely import config_manager from optimizely import decision_service from optimizely import entities @@ -32,7 +33,6 @@ class OptimizelyTest(base.BaseTest): - strTest = None try: @@ -70,7 +70,7 @@ def _validate_event_object(self, event_obj, expected_url, expected_params, expec self.assertEqual(expected_headers, event_obj.get('headers')) def _validate_event_object_event_tags( - self, event_obj, expected_event_metric_params, expected_event_features_params + self, event_obj, expected_event_metric_params, expected_event_features_params ): """ Helper method to validate properties of the event object related to event tags. """ @@ -199,7 +199,7 @@ def test_init__unsupported_datafile_version__logs_error(self): mock_client_logger = mock.MagicMock() with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger), mock.patch( - 'optimizely.error_handler.NoOpErrorHandler.handle_error' + 'optimizely.error_handler.NoOpErrorHandler.handle_error' ) as mock_error_handler: opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_unsupported_version)) @@ -236,7 +236,7 @@ def test_init__sdk_key_only(self): """ Test that if only sdk_key is provided then PollingConfigManager is used. """ with mock.patch('optimizely.config_manager.PollingConfigManager._set_config'), mock.patch( - 'threading.Thread.start' + 'threading.Thread.start' ): opt_obj = optimizely.Optimizely(sdk_key='test_sdk_key') @@ -246,7 +246,7 @@ def test_init__sdk_key_and_datafile(self): """ Test that if both sdk_key and datafile is provided then PollingConfigManager is used. """ with mock.patch('optimizely.config_manager.PollingConfigManager._set_config'), mock.patch( - 'threading.Thread.start' + 'threading.Thread.start' ): opt_obj = optimizely.Optimizely(datafile=json.dumps(self.config_dict), sdk_key='test_sdk_key') @@ -259,7 +259,7 @@ def test_init__sdk_key_and_datafile_access_token(self): """ with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager._set_config'), mock.patch( - 'threading.Thread.start' + 'threading.Thread.start' ): opt_obj = optimizely.Optimizely(datafile_access_token='test_datafile_access_token', sdk_key='test_sdk_key') @@ -271,7 +271,7 @@ def test_invalid_json_raises_schema_validation_off(self): # Not JSON mock_client_logger = mock.MagicMock() with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger), mock.patch( - 'optimizely.error_handler.NoOpErrorHandler.handle_error' + 'optimizely.error_handler.NoOpErrorHandler.handle_error' ) as mock_error_handler: opt_obj = optimizely.Optimizely('invalid_json', skip_json_validation=True) @@ -286,7 +286,7 @@ def test_invalid_json_raises_schema_validation_off(self): # JSON having valid version, but entities have invalid format with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger), mock.patch( - 'optimizely.error_handler.NoOpErrorHandler.handle_error' + 'optimizely.error_handler.NoOpErrorHandler.handle_error' ) as mock_error_handler: opt_obj = optimizely.Optimizely( {'version': '2', 'events': 'invalid_value', 'experiments': 'invalid_value'}, skip_json_validation=True, @@ -302,8 +302,8 @@ def test_activate(self): """ Test that activate calls process with right params and returns expected variation. """ with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ) as mock_decision, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -349,9 +349,10 @@ def test_activate(self): } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + user_context = mock_decision.call_args[0][2] mock_decision.assert_called_once_with( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', None, + self.project_config, self.project_config.get_experiment_from_key('test_experiment'), user_context ) self.assertEqual(1, mock_process.call_count) @@ -381,8 +382,8 @@ def on_activate(experiment, user_id, attributes, variation, event): enums.NotificationTypes.ACTIVATE, on_activate ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) @@ -414,8 +415,8 @@ def on_track(event_key, user_id, attributes, event_tags, event): note_id = self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.optimizely.track('test_event', 'test_user') @@ -440,10 +441,11 @@ def on_activate(event_key, user_id, attributes, event_tags, event): pass self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate) + variation = (self.project_config.get_variation_from_id('test_experiment', '111129'), []) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=variation, ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: @@ -460,7 +462,7 @@ def on_activate(event_key, user_id, attributes, event_tags, event): 'ab-test', 'test_user', {}, - {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + {'experiment_key': 'test_experiment', 'variation_key': variation[0].key}, ), mock.call( enums.NotificationTypes.ACTIVATE, @@ -480,10 +482,11 @@ def on_activate(event_key, user_id, attributes, event_tags, event): pass self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate) + variation = (self.project_config.get_variation_from_id('test_experiment', '111129'), []) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=variation, ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: @@ -502,7 +505,7 @@ def on_activate(event_key, user_id, attributes, event_tags, event): 'ab-test', 'test_user', {'test_attribute': 'test_value'}, - {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + {'experiment_key': 'test_experiment', 'variation_key': variation[0].key}, ), mock.call( enums.NotificationTypes.ACTIVATE, @@ -515,12 +518,22 @@ def on_activate(event_key, user_id, attributes, event_tags, event): ] ) + """ + mock_broadcast.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-test', + 'test_user', + {}, + {'experiment_key': 'test_experiment', 'variation_key': variation}, + ) + """ + def test_decision_listener__user_not_in_experiment(self): """ Test that activate calls broadcast decision with variation_key 'None' \ when user not in experiment. """ with mock.patch('optimizely.decision_service.DecisionService.get_variation', - return_value=(None, []),), mock.patch( + return_value=(None, []), ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' @@ -544,8 +557,8 @@ def on_track(event_key, user_id, attributes, event_tags, event): self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: @@ -566,8 +579,8 @@ def on_track(event_key, user_id, attributes, event_tags, event): self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: @@ -593,8 +606,8 @@ def on_track(event_key, user_id, attributes, event_tags, event): self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111128'), []), ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_event_tracked: @@ -636,12 +649,13 @@ def on_activate(experiment, user_id, attributes, variation, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=( - decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), []), + return_value=( + decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), []), ) as mock_decision, mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, user_context) self.assertTrue(access_callback[0]) def test_is_feature_enabled_rollout_callback_listener(self): @@ -663,14 +677,15 @@ def on_activate(experiment, user_id, attributes, variation, event): mock_variation = project_config.get_variation_from_id('test_experiment', '111129') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) - mock_decision.assert_called_once_with(project_config, feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(project_config, feature, user_context) # Check that impression event is sent for rollout and send_flag_decisions = True self.assertEqual(1, mock_process.call_count) @@ -682,7 +697,7 @@ def test_activate__with_attributes__audience_match(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ) as mock_get_variation, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -731,12 +746,12 @@ def test_activate__with_attributes__audience_match(self): } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + user_context = mock_get_variation.call_args[0][2] mock_get_variation.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - {'test_attribute': 'test_value'}, + user_context ) self.assertEqual(1, mock_process.call_count) self._validate_event_object( @@ -753,13 +768,12 @@ def test_activate__with_attributes_of_different_types(self): with mock.patch( 'optimizely.bucketer.Bucketer.bucket', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ) as mock_bucket, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: - attributes = { 'test_attribute': 'test_value_1', 'boolean_key': False, @@ -952,7 +966,6 @@ def test_activate__with_attributes__complex_audience_mismatch(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - user_attr = {'house': 'Hufflepuff', 'lasers': 45.5} self.assertIsNone(opt_obj.activate('audience_combinations_experiment', 'test_user', user_attr)) @@ -964,7 +977,7 @@ def test_activate__with_attributes__audience_match__forced_bucketing(self): set_forced_variation is called. """ with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'control')) self.assertEqual( @@ -1027,7 +1040,7 @@ def test_activate__with_attributes__audience_match__bucketing_id_provided(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ) as mock_get_variation, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -1087,12 +1100,12 @@ def test_activate__with_attributes__audience_match__bucketing_id_provided(self): } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + user_context = mock_get_variation.call_args[0][2] mock_get_variation.assert_called_once_with( self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - {'test_attribute': 'test_value', '$opt_bucketing_id': 'user_bucket_value'}, + user_context ) self.assertEqual(1, mock_process.call_count) self._validate_event_object( @@ -1109,7 +1122,7 @@ def test_activate__with_attributes__no_audience_match(self): with mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=(False, [])) as mock_audience_check: self.assertIsNone( - self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'},) + self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, ) ) expected_experiment = self.project_config.get_experiment_from_key('test_experiment') mock_audience_check.assert_called_once_with( @@ -1125,7 +1138,7 @@ def test_activate__with_attributes__invalid_attributes(self): """ Test that activate returns None and does not bucket or process event when attributes are invalid. """ with mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: self.assertIsNone(self.optimizely.activate('test_experiment', 'test_user', attributes='invalid')) @@ -1136,7 +1149,7 @@ def test_activate__experiment_not_running(self): """ Test that activate returns None and does not process event when experiment is not Running. """ with mock.patch( - 'optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=True + 'optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=True ) as mock_audience_check, mock.patch( 'optimizely.helpers.experiment.is_experiment_running', return_value=False ) as mock_is_experiment_running, mock.patch( @@ -1145,7 +1158,7 @@ def test_activate__experiment_not_running(self): 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: self.assertIsNone( - self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'},) + self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, ) ) mock_is_experiment_running.assert_called_once_with( @@ -1159,7 +1172,7 @@ def test_activate__whitelisting_overrides_audience_check(self): """ Test that during activate whitelist overrides audience check if user is in the whitelist. """ with mock.patch( - 'optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=False + 'optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=False ) as mock_audience_check, mock.patch( 'optimizely.helpers.experiment.is_experiment_running', return_value=True ) as mock_is_experiment_running: @@ -1174,13 +1187,13 @@ def test_activate__bucketer_returns_none(self): with mock.patch( 'optimizely.helpers.audience.does_user_meet_audience_conditions', - return_value=(True, [])), mock.patch( + return_value=(True, [])), mock.patch( 'optimizely.bucketer.Bucketer.bucket', return_value=(None, [])) as mock_bucket, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: self.assertIsNone( - self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'},) + self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, ) ) mock_bucket.assert_called_once_with( self.project_config, @@ -1219,7 +1232,7 @@ def test_track__with_attributes(self): """ Test that track calls process with right params when attributes are provided. """ with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}) @@ -1345,7 +1358,7 @@ def test_track__with_attributes__bucketing_id_provided(self): attributes (including bucketing ID) are provided. """ with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', @@ -1404,7 +1417,7 @@ def test_track__with_attributes__no_audience_match(self): """ Test that track calls process even if audience conditions do not match. """ with mock.patch('time.time', return_value=42), mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: self.optimizely.track( 'test_event', 'test_user', attributes={'test_attribute': 'wrong_test_value'}, @@ -1416,7 +1429,7 @@ def test_track__with_attributes__invalid_attributes(self): """ Test that track does not bucket or process event if attributes are invalid. """ with mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: self.optimizely.track('test_event', 'test_user', attributes='invalid') @@ -1427,7 +1440,7 @@ def test_track__with_event_tags(self): """ Test that track calls process with right params when event tags are provided. """ with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', @@ -1484,7 +1497,7 @@ def test_track__with_event_tags_revenue(self): event tags are provided only. """ with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', @@ -1570,7 +1583,7 @@ def test_track__with_event_tags__forced_bucketing(self): after a forced bucket. """ with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) self.optimizely.track( @@ -1628,7 +1641,7 @@ def test_track__with_invalid_event_tags(self): """ Test that track calls process with right params when invalid event tags are provided. """ with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track( 'test_event', @@ -1683,7 +1696,7 @@ def test_track__experiment_not_running(self): """ Test that track calls process even if experiment is not running. """ with mock.patch( - 'optimizely.helpers.experiment.is_experiment_running', return_value=False + 'optimizely.helpers.experiment.is_experiment_running', return_value=False ) as mock_is_experiment_running, mock.patch('time.time', return_value=42), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: @@ -1697,7 +1710,7 @@ def test_track_invalid_event_key(self): """ Test that track does not call process when event does not exist. """ with mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch.object(self.optimizely, 'logger') as mock_client_logging: self.optimizely.track('aabbcc_event', 'test_user') @@ -1708,7 +1721,7 @@ def test_track__whitelisted_user_overrides_audience_check(self): """ Test that event is tracked when user is whitelisted. """ with mock.patch('time.time', return_value=42), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: self.optimizely.track('test_event', 'user_1') @@ -1744,7 +1757,7 @@ def test_track__invalid_experiment_key(self): when exp_key is in invalid format. """ with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.is_non_empty_string', return_value=False + 'optimizely.helpers.validator.is_non_empty_string', return_value=False ) as mock_validator: self.assertIsNone(self.optimizely.track(99, 'test_user')) @@ -1764,11 +1777,12 @@ def test_get_variation(self): """ Test that get_variation returns valid variation and broadcasts decision with proper parameters. """ with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: + variation = self.optimizely.get_variation('test_experiment', 'test_user') self.assertEqual( - 'variation', self.optimizely.get_variation('test_experiment', 'test_user'), + 'variation', variation, ) self.assertEqual(mock_broadcast.call_count, 1) @@ -1778,7 +1792,7 @@ def test_get_variation(self): 'ab-test', 'test_user', {}, - {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + {'experiment_key': 'test_experiment', 'variation_key': variation}, ) def test_get_variation_with_experiment_in_feature(self): @@ -1789,10 +1803,11 @@ def test_get_variation_with_experiment_in_feature(self): project_config = opt_obj.config_manager.get_config() with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: - self.assertEqual('variation', opt_obj.get_variation('test_experiment', 'test_user')) + variation = opt_obj.get_variation('test_experiment', 'test_user') + self.assertEqual('variation', variation) self.assertEqual(mock_broadcast.call_count, 1) @@ -1801,14 +1816,14 @@ def test_get_variation_with_experiment_in_feature(self): 'feature-test', 'test_user', {}, - {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + {'experiment_key': 'test_experiment', 'variation_key': variation}, ) def test_get_variation__returns_none(self): """ Test that get_variation returns no variation and broadcasts decision with proper parameters. """ with mock.patch('optimizely.decision_service.DecisionService.get_variation', - return_value=(None, []),), mock.patch( + return_value=(None, []), ), mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: self.assertEqual( @@ -1868,7 +1883,7 @@ def test_is_feature_enabled__returns_false_for_invalid_feature_key(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) with mock.patch.object(opt_obj, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.is_non_empty_string', return_value=False + 'optimizely.helpers.validator.is_non_empty_string', return_value=False ) as mock_validator: self.assertFalse(opt_obj.is_feature_enabled(None, 'test_user')) @@ -1889,7 +1904,7 @@ def test_is_feature_enabled__returns_false_for__invalid_attributes(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) with mock.patch.object(opt_obj, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.are_attributes_valid', return_value=False + 'optimizely.helpers.validator.are_attributes_valid', return_value=False ) as mock_validator: self.assertFalse(opt_obj.is_feature_enabled('feature_key', 'test_user', attributes='invalid')) @@ -1938,7 +1953,7 @@ def test_is_feature_enabled__returns_false_for_invalid_feature(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature' + 'optimizely.decision_service.DecisionService.get_variation_for_feature' ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process: @@ -1949,7 +1964,7 @@ def test_is_feature_enabled__returns_false_for_invalid_feature(self): # Check that no event is sent self.assertEqual(0, mock_process.call_count) - def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enabled_for_variation(self,): + def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enabled_for_variation(self, ): """ Test that the feature is enabled for the user if bucketed into variation of an experiment and the variation's featureEnabled property is True. Also confirm that impression event is processed and decision listener is called with proper parameters """ @@ -1965,9 +1980,9 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab self.assertTrue(mock_variation.featureEnabled) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -1979,7 +1994,8 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab ): self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, user_context) mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, @@ -2048,7 +2064,7 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab {'Content-Type': 'application/json'}, ) - def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_disabled_for_variation(self,): + def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_disabled_for_variation(self, ): """ Test that the feature is disabled for the user if bucketed into variation of an experiment and the variation's featureEnabled property is False. Also confirm that impression event is processed and decision is broadcasted with proper parameters """ @@ -2064,9 +2080,9 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis self.assertFalse(mock_variation.featureEnabled) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2078,7 +2094,8 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis ): self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, user_context) mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, @@ -2147,7 +2164,7 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis {'Content-Type': 'application/json'}, ) - def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled(self,): + def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled(self, ): """ Test that the feature is enabled for the user if bucketed into variation of a rollout and the variation's featureEnabled property is True. Also confirm that no impression event is processed and decision is broadcasted with proper parameters """ @@ -2163,9 +2180,9 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled self.assertTrue(mock_variation.featureEnabled) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2177,7 +2194,8 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled ): self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, user_context) mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, @@ -2195,7 +2213,7 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled # Check that impression event is sent for rollout and send_flag_decisions = True self.assertEqual(1, mock_process.call_count) - def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled_with_sending_decisions(self,): + def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled_with_sending_decisions(self, ): """ Test that the feature is enabled for the user if bucketed into variation of a rollout and the variation's featureEnabled property is True. Also confirm that an impression event is processed and decision is broadcasted with proper parameters, as send_flag_decisions is set to true """ @@ -2212,9 +2230,9 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled self.assertTrue(mock_variation.featureEnabled) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2226,7 +2244,8 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled ): self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, user_context) mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, @@ -2297,7 +2316,7 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled {'Content-Type': 'application/json'}, ) - def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabled(self,): + def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabled(self, ): """ Test that the feature is disabled for the user if bucketed into variation of a rollout and the variation's featureEnabled property is False. Also confirm that no impression event is processed and decision is broadcasted with proper parameters """ @@ -2313,9 +2332,9 @@ def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabl mock_variation.featureEnabled = False with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2327,7 +2346,8 @@ def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabl ): self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, user_context) mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, @@ -2345,17 +2365,18 @@ def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabl # Check that impression event is sent for rollout and send_flag_decisions = True self.assertEqual(1, mock_process.call_count) - def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_variation(self,): + def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_variation(self, ): """ Test that the feature is not enabled for the user if user is neither bucketed for Feature Experiment nor for Feature Rollout. Also confirm that impression event is not processed. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() feature = project_config.get_feature_from_key('test_feature_in_experiment') with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2370,7 +2391,8 @@ def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_va # Check that impression event is sent for rollout and send_flag_decisions = True self.assertEqual(1, mock_process.call_count) - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, user_context) mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, @@ -2388,16 +2410,17 @@ def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_va # Check that impression event is sent for rollout and send_flag_decisions = True self.assertEqual(1, mock_process.call_count) - def test_is_feature_enabled__returns_false_when_variation_is_nil(self,): + def test_is_feature_enabled__returns_false_when_variation_is_nil(self, ): """ Test that the feature is not enabled with nil variation Also confirm that impression event is processed. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() feature = project_config.get_feature_from_key('test_feature_in_experiment_and_rollout') + with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ) as mock_process, mock.patch( @@ -2412,7 +2435,8 @@ def test_is_feature_enabled__returns_false_when_variation_is_nil(self,): # Check that impression event is sent for rollout and send_flag_decisions = True self.assertEqual(1, mock_process.call_count) - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + user_context = mock_decision.call_args[0][2] + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, user_context) mock_broadcast_decision.assert_called_with( enums.NotificationTypes.DECISION, @@ -2451,7 +2475,7 @@ def test_is_feature_enabled__invalid_config(self): opt_obj = optimizely.Optimizely('invalid_file') with mock.patch.object(opt_obj, 'logger') as mock_client_logging, mock.patch( - 'optimizely.event_dispatcher.EventDispatcher.dispatch_event' + 'optimizely.event_dispatcher.EventDispatcher.dispatch_event' ) as mock_dispatch_event: self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'user_1')) @@ -2475,7 +2499,7 @@ def side_effect(*args, **kwargs): return False with mock.patch( - 'optimizely.optimizely.Optimizely.is_feature_enabled', side_effect=side_effect, + 'optimizely.optimizely.Optimizely.is_feature_enabled', side_effect=side_effect, ) as mock_is_feature_enabled: received_features = opt_obj.get_enabled_features('user_1') @@ -2508,14 +2532,14 @@ def side_effect(*args, **kwargs): response = decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT) elif feature.key == 'test_feature_in_experiment_and_rollout': response = decision_service.Decision( - mock_experiment, mock_variation_2, enums.DecisionSources.FEATURE_TEST,) + mock_experiment, mock_variation_2, enums.DecisionSources.FEATURE_TEST, ) else: response = decision_service.Decision(mock_experiment, mock_variation_2, enums.DecisionSources.ROLLOUT) return (response, []) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', side_effect=side_effect, + 'optimizely.decision_service.DecisionService.get_variation_for_feature', side_effect=side_effect, ), mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2591,7 +2615,7 @@ def test_get_enabled_features_invalid_user_id(self): def test_get_enabled_features__invalid_attributes(self): """ Test that get_enabled_features returns empty list if attributes are in an invalid format. """ with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.are_attributes_valid', return_value=False + 'optimizely.helpers.validator.are_attributes_valid', return_value=False ) as mock_validator: self.assertEqual( [], self.optimizely.get_enabled_features('test_user', attributes='invalid'), @@ -2635,9 +2659,9 @@ def test_get_feature_variable_boolean(self): mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('test_experiment') mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2673,9 +2697,9 @@ def test_get_feature_variable_double(self): mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('test_experiment') mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2711,9 +2735,9 @@ def test_get_feature_variable_integer(self): mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('test_experiment') mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2749,9 +2773,9 @@ def test_get_feature_variable_string(self): mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('test_experiment') mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2788,9 +2812,9 @@ def test_get_feature_variable_json(self): mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('test_experiment') mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2835,15 +2859,15 @@ def test_get_all_feature_variables(self): 'true_object': {'true_test': 1.4}, 'variable_without_usage': 45} with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: self.assertEqual( expected_results, - opt_obj.get_all_feature_variables('test_feature_in_experiment', 'test_user'), + opt_obj.get_all_feature_variables('test_feature_in_experiment', 'test_user', {}), ) self.assertEqual(7, mock_logger.debug.call_count) @@ -2892,9 +2916,9 @@ def test_get_feature_variable(self): mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') # Boolean with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2921,9 +2945,9 @@ def test_get_feature_variable(self): ) # Double with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2952,9 +2976,9 @@ def test_get_feature_variable(self): ) # Integer with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2983,9 +3007,9 @@ def test_get_feature_variable(self): ) # String with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3014,9 +3038,9 @@ def test_get_feature_variable(self): ) # JSON with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3054,9 +3078,9 @@ def test_get_feature_variable_boolean_for_feature_in_rollout(self): user_attributes = {'test_attribute': 'test_value'} with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3096,9 +3120,9 @@ def test_get_feature_variable_double_for_feature_in_rollout(self): user_attributes = {'test_attribute': 'test_value'} with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3138,9 +3162,9 @@ def test_get_feature_variable_integer_for_feature_in_rollout(self): user_attributes = {'test_attribute': 'test_value'} with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3180,9 +3204,9 @@ def test_get_feature_variable_string_for_feature_in_rollout(self): user_attributes = {'test_attribute': 'test_value'} with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3222,9 +3246,9 @@ def test_get_feature_variable_json_for_feature_in_rollout(self): user_attributes = {'test_attribute': 'test_value'} with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3264,9 +3288,9 @@ def test_get_all_feature_variables_for_feature_in_rollout(self): user_attributes = {'test_attribute': 'test_value'} with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3318,9 +3342,9 @@ def test_get_feature_variable_for_feature_in_rollout(self): # Boolean with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3351,9 +3375,9 @@ def test_get_feature_variable_for_feature_in_rollout(self): ) # Double with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3384,9 +3408,9 @@ def test_get_feature_variable_for_feature_in_rollout(self): ) # Integer with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3417,9 +3441,9 @@ def test_get_feature_variable_for_feature_in_rollout(self): ) # String with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3451,9 +3475,9 @@ def test_get_feature_variable_for_feature_in_rollout(self): # JSON with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3483,7 +3507,7 @@ def test_get_feature_variable_for_feature_in_rollout(self): }, ) - def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_variation(self,): + def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_variation(self, ): """ Test that get_feature_variable_* returns default value if variable usage not present in variation. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -3495,9 +3519,9 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # Boolean with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertTrue( opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user') @@ -3505,9 +3529,9 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # Double with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), @@ -3515,9 +3539,9 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # Integer with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), @@ -3525,9 +3549,9 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # String with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 'devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), @@ -3535,9 +3559,9 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # JSON with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( {"test": 12}, opt_obj.get_feature_variable_json('test_feature_in_experiment', 'object', 'test_user'), @@ -3545,34 +3569,34 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va # Non-typed with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 10.99, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ): self.assertEqual( 'devel', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), @@ -3586,8 +3610,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Boolean with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3620,8 +3644,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Double with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3654,8 +3678,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Integer with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3688,8 +3712,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # String with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3722,8 +3746,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # JSON with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3756,8 +3780,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Non-typed with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3787,8 +3811,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): mock_client_logger.info.reset_mock() with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3820,8 +3844,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): mock_client_logger.info.reset_mock() with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3853,8 +3877,8 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): mock_client_logger.info.reset_mock() with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3994,9 +4018,8 @@ def test_get_feature_variable__invalid_attributes(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) with mock.patch.object(opt_obj, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.are_attributes_valid', return_value=False + 'optimizely.helpers.validator.are_attributes_valid', return_value=False ) as mock_validator: - # get_feature_variable_boolean self.assertIsNone( opt_obj.get_feature_variable_boolean( @@ -4064,7 +4087,7 @@ def test_get_feature_variable__invalid_attributes(self): mock_client_logging.reset_mock() self.assertIsNone( - opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user', attributes='invalid',) + opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user', attributes='invalid', ) ) mock_validator.assert_called_once_with('invalid') mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') @@ -4072,7 +4095,7 @@ def test_get_feature_variable__invalid_attributes(self): mock_client_logging.reset_mock() self.assertIsNone( - opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user', attributes='invalid',) + opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user', attributes='invalid', ) ) mock_validator.assert_called_once_with('invalid') mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') @@ -4166,11 +4189,10 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # Boolean with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: - self.assertTrue( opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user') ) @@ -4182,9 +4204,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # Double with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), @@ -4197,9 +4219,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # Integer with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), @@ -4212,9 +4234,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # String with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), @@ -4227,9 +4249,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # JSON with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( {"test": 12}, opt_obj.get_feature_variable_json('test_feature_in_experiment', 'object', 'test_user'), @@ -4242,11 +4264,10 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self # Non-typed with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: - self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) mock_client_logger.info.assert_called_once_with( @@ -4255,9 +4276,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 10.99, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), @@ -4269,9 +4290,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), @@ -4283,9 +4304,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'devel', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), @@ -4296,7 +4317,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self 'Returning the default variable value "devel".' ) - def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_rollout(self,): + def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_rollout(self, ): """ Test that get_feature_variable_* returns default value if feature is not enabled for the user. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -4305,9 +4326,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # Boolean with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertFalse(opt_obj.get_feature_variable_boolean('test_feature_in_rollout', 'is_running', 'test_user')) @@ -4318,9 +4339,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # Double with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 99.99, opt_obj.get_feature_variable_double('test_feature_in_rollout', 'price', 'test_user'), @@ -4333,9 +4354,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # Integer with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_rollout', 'count', 'test_user'), @@ -4348,9 +4369,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # String with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'Hello', opt_obj.get_feature_variable_string('test_feature_in_rollout', 'message', 'test_user'), @@ -4362,9 +4383,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # JSON with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( {"field": 1}, opt_obj.get_feature_variable_json('test_feature_in_rollout', 'object', 'test_user'), @@ -4376,9 +4397,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r # Non-typed with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertFalse(opt_obj.get_feature_variable('test_feature_in_rollout', 'is_running', 'test_user')) @@ -4388,9 +4409,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 99.99, opt_obj.get_feature_variable('test_feature_in_rollout', 'price', 'test_user'), @@ -4402,9 +4423,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_rollout', 'count', 'test_user'), @@ -4416,9 +4437,9 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r ) with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.ROLLOUT), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'Hello', opt_obj.get_feature_variable('test_feature_in_rollout', 'message', 'test_user'), @@ -4435,9 +4456,9 @@ def test_get_feature_variable__returns_none_if_type_mismatch(self): mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('test_experiment') mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: # "is_working" is boolean variable and we are using double method on it. self.assertIsNone( @@ -4456,9 +4477,9 @@ def test_get_feature_variable__returns_none_if_unable_to_cast(self): mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('test_experiment') mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111129') with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), ), mock.patch( 'optimizely.project_config.ProjectConfig.get_typecast_value', side_effect=ValueError(), ), mock.patch.object( @@ -4674,8 +4695,8 @@ def test_activate(self): user_id = 'test_user' with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('time.time', return_value=42), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch.object( @@ -4694,7 +4715,7 @@ def test_track(self): event_builder.Event('logx.optimizely.com', {'event_key': event_key}) with mock.patch( - 'optimizely.event.event_processor.ForwardingEventProcessor.process' + 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock_client_logger as mock_client_logging: self.optimizely.track(event_key, user_id) @@ -4708,7 +4729,7 @@ def test_activate__experiment_not_running(self): mock_client_logger = mock.patch.object(self.optimizely, 'logger') mock_decision_logger = mock.patch.object(self.optimizely.decision_service, 'logger') with mock_client_logger as mock_client_logging, mock_decision_logger as mock_decision_logging, mock.patch( - 'optimizely.helpers.experiment.is_experiment_running', return_value=False + 'optimizely.helpers.experiment.is_experiment_running', return_value=False ) as mock_is_experiment_running: self.optimizely.activate( 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, @@ -4770,7 +4791,7 @@ def test_get_variation__invalid_experiment_key(self): when exp_key is in invalid format. """ with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.is_non_empty_string', return_value=False + 'optimizely.helpers.validator.is_non_empty_string', return_value=False ) as mock_validator: self.assertIsNone(self.optimizely.get_variation(99, 'test_user')) @@ -4790,7 +4811,7 @@ def test_activate__invalid_experiment_key(self): when exp_key is in invalid format. """ with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.is_non_empty_string', return_value=False + 'optimizely.helpers.validator.is_non_empty_string', return_value=False ) as mock_validator: self.assertIsNone(self.optimizely.activate(99, 'test_user')) @@ -4815,8 +4836,8 @@ def test_activate__empty_user_id(self): user_id = '' with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), ), mock.patch('time.time', return_value=42), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch.object( @@ -4838,7 +4859,7 @@ def test_get_variation__experiment_not_running(self): """ Test that expected log messages are logged during get variation when experiment is not running. """ with mock.patch.object(self.optimizely.decision_service, 'logger') as mock_decision_logging, mock.patch( - 'optimizely.helpers.experiment.is_experiment_running', return_value=False + 'optimizely.helpers.experiment.is_experiment_running', return_value=False ) as mock_is_experiment_running: self.optimizely.get_variation( 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, @@ -4882,7 +4903,7 @@ def test_get_variation__experiment_not_running__forced_bucketing(self): """ Test that the expected forced variation is called if an experiment is not running """ with mock.patch( - 'optimizely.helpers.experiment.is_experiment_running', return_value=False + 'optimizely.helpers.experiment.is_experiment_running', return_value=False ) as mock_is_experiment_running: self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation') self.assertEqual( @@ -4910,8 +4931,8 @@ def test_get_variation__whitelisted_user_forced_bucketing(self): def test_get_variation__user_profile__forced_bucketing(self): """ Test that the expected forced variation is called if a user profile exists """ with mock.patch( - 'optimizely.decision_service.DecisionService.get_stored_variation', - return_value=entities.Variation('111128', 'control'), + 'optimizely.decision_service.DecisionService.get_stored_variation', + return_value=entities.Variation('111128', 'control'), ): self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) self.assertEqual( @@ -4932,6 +4953,7 @@ def test_get_variation__invalid_attributes__forced_bucketing(self): variation_key = self.optimizely.get_variation( 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value_invalid'}, ) + variation_key = variation_key self.assertEqual('variation', variation_key) def test_set_forced_variation__invalid_object(self): @@ -4966,7 +4988,7 @@ def test_set_forced_variation__invalid_experiment_key(self): when exp_key is in invalid format. """ with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.is_non_empty_string', return_value=False + 'optimizely.helpers.validator.is_non_empty_string', return_value=False ) as mock_validator: self.assertFalse(self.optimizely.set_forced_variation(99, 'test_user', 'variation')) @@ -5014,7 +5036,7 @@ def test_get_forced_variation__invalid_experiment_key(self): when exp_key is in invalid format. """ with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( - 'optimizely.helpers.validator.is_non_empty_string', return_value=False + 'optimizely.helpers.validator.is_non_empty_string', return_value=False ) as mock_validator: self.assertIsNone(self.optimizely.get_forced_variation(99, 'test_user')) @@ -5040,3 +5062,10 @@ def test_user_context_invalid_user_id(self): for u in user_ids: uc = self.optimizely.create_user_context(u) self.assertIsNone(uc, "invalid user id should return none") + + def test_invalid_flag_key(self): + """ + Tests invalid flag key in function get_flag_variation_by_key(). + """ + # TODO mock function get_flag_variation_by_key + pass diff --git a/tests/test_user_context.py b/tests/test_user_context.py index fcffc415..e871827a 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -13,14 +13,15 @@ import json import mock +import threading -from optimizely.decision.optimizely_decision import OptimizelyDecision +from optimizely import optimizely, decision_service from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption as DecideOption +from optimizely.decision.optimizely_decision import OptimizelyDecision from optimizely.helpers import enums -from . import base -from optimizely import optimizely, decision_service from optimizely.optimizely_user_context import OptimizelyUserContext from optimizely.user_profile import UserProfileService +from . import base class UserContextTest(base.BaseTest): @@ -40,7 +41,7 @@ def test_user_context(self): """ tests user context creating and setting attributes """ - uc = OptimizelyUserContext(self.optimizely, "test_user") + uc = OptimizelyUserContext(self.optimizely, None, "test_user") # user attribute should be empty dict self.assertEqual({}, uc.get_user_attributes()) @@ -65,7 +66,7 @@ def test_user_and_attributes_as_json(self): """ tests user context as json """ - uc = OptimizelyUserContext(self.optimizely, "test_user") + uc = OptimizelyUserContext(self.optimizely, None, "test_user") # set an attribute uc.set_attribute("browser", "safari") @@ -81,25 +82,25 @@ def test_user_and_attributes_as_json(self): def test_attributes_are_cloned_when_passed_to_user_context(self): user_id = 'test_user' attributes = {"browser": "chrome"} - uc = OptimizelyUserContext(self.optimizely, user_id, attributes) + uc = OptimizelyUserContext(self.optimizely, None, user_id, attributes) self.assertEqual(attributes, uc.get_user_attributes()) attributes['new_key'] = 'test_value' self.assertNotEqual(attributes, uc.get_user_attributes()) def test_attributes_default_to_dict_when_passes_as_non_dict(self): - uc = OptimizelyUserContext(self.optimizely, "test_user", True) + uc = OptimizelyUserContext(self.optimizely, None, "test_user", True) # user attribute should be empty dict self.assertEqual({}, uc.get_user_attributes()) - uc = OptimizelyUserContext(self.optimizely, "test_user", 10) + uc = OptimizelyUserContext(self.optimizely, None, "test_user", 10) # user attribute should be empty dict self.assertEqual({}, uc.get_user_attributes()) - uc = OptimizelyUserContext(self.optimizely, "test_user", 'helloworld') + uc = OptimizelyUserContext(self.optimizely, None, "test_user", 'helloworld') # user attribute should be empty dict self.assertEqual({}, uc.get_user_attributes()) - uc = OptimizelyUserContext(self.optimizely, "test_user", []) + uc = OptimizelyUserContext(self.optimizely, None, "test_user", []) # user attribute should be empty dict self.assertEqual({}, uc.get_user_attributes()) @@ -790,7 +791,7 @@ def test_decide__option__include_reasons__feature_rollout(self): 'Evaluating audiences for rule 1: ["11154"].', 'Audiences for rule 1 collectively evaluated to TRUE.', 'User "test_user" meets audience conditions for targeting rule 1.', - 'User "test_user" is in the traffic group of targeting rule 1.' + 'User "test_user" bucketed into a targeting rule 1.' ] self.assertEqual(expected_reasons, actual.reasons) @@ -1142,14 +1143,13 @@ def test_decide_reasons__hit_everyone_else_rule__fails_bucketing(self): actual = user_context.decide('test_feature_in_rollout', ['INCLUDE_REASONS']) expected_reasons = [ - 'Evaluating audiences for rule 1: ["11154"].', - 'Audiences for rule 1 collectively evaluated to FALSE.', - 'User "test_user" does not meet conditions for targeting rule 1.', - 'Evaluating audiences for rule 2: ["11159"].', - 'Audiences for rule 2 collectively evaluated to FALSE.', - 'User "test_user" does not meet conditions for targeting rule 2.', + 'Evaluating audiences for rule 1: ["11154"].', 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "test_user" does not meet audience conditions for targeting rule 1.', + 'Evaluating audiences for rule 2: ["11159"].', 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "test_user" does not meet audience conditions for targeting rule 2.', 'Evaluating audiences for rule Everyone Else: [].', 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "test_user" meets audience conditions for targeting rule Everyone Else.', 'Bucketed into an empty traffic range. Returning nil.' ] @@ -1165,13 +1165,14 @@ def test_decide_reasons__hit_everyone_else_rule(self): expected_reasons = [ 'Evaluating audiences for rule 1: ["11154"].', 'Audiences for rule 1 collectively evaluated to FALSE.', - 'User "abcde" does not meet conditions for targeting rule 1.', + 'User "abcde" does not meet audience conditions for targeting rule 1.', 'Evaluating audiences for rule 2: ["11159"].', 'Audiences for rule 2 collectively evaluated to FALSE.', - 'User "abcde" does not meet conditions for targeting rule 2.', + 'User "abcde" does not meet audience conditions for targeting rule 2.', 'Evaluating audiences for rule Everyone Else: [].', 'Audiences for rule Everyone Else collectively evaluated to TRUE.', - 'User "abcde" meets conditions for targeting rule "Everyone Else".' + 'User "abcde" meets audience conditions for targeting rule Everyone Else.', + 'User "abcde" bucketed into a targeting rule Everyone Else.' ] self.assertEqual(expected_reasons, actual.reasons) @@ -1184,16 +1185,15 @@ def test_decide_reasons__hit_rule2__fails_bucketing(self): actual = user_context.decide('test_feature_in_rollout', ['INCLUDE_REASONS']) expected_reasons = [ - 'Evaluating audiences for rule 1: ["11154"].', - 'Audiences for rule 1 collectively evaluated to FALSE.', - 'User "test_user" does not meet conditions for targeting rule 1.', - 'Evaluating audiences for rule 2: ["11159"].', - 'Audiences for rule 2 collectively evaluated to TRUE.', + 'Evaluating audiences for rule 1: ["11154"].', 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "test_user" does not meet audience conditions for targeting rule 1.', + 'Evaluating audiences for rule 2: ["11159"].', 'Audiences for rule 2 collectively evaluated to TRUE.', 'User "test_user" meets audience conditions for targeting rule 2.', 'Bucketed into an empty traffic range. Returning nil.', - 'User "test_user" is not in the traffic group for targeting rule 2. Checking "Everyone Else" rule now.', + 'User "test_user" not bucketed into a targeting rule 2. Checking "Everyone Else" rule now.', 'Evaluating audiences for rule Everyone Else: [].', 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "test_user" meets audience conditions for targeting rule Everyone Else.', 'Bucketed into an empty traffic range. Returning nil.' ] @@ -1230,8 +1230,10 @@ def save(self, user_profile): actual = user_context.decide('test_feature_in_experiment', options) - expected_reasons = [('Returning previously activated variation ID "control" of experiment ' - '"test_experiment" for user "test_user" from user profile.')] + expected_reasons = [ + 'Returning previously activated variation ID "control" of experiment ' + '"test_experiment" for user "test_user" from user profile.' + ] self.assertEqual(expected_reasons, actual.reasons) @@ -1247,8 +1249,10 @@ def test_decide_reasons__forced_variation(self): actual = user_context.decide('test_feature_in_experiment', options) - expected_reasons = [('Variation "control" is mapped to experiment ' - '"test_experiment" and user "test_user" in the forced variation map')] + expected_reasons = [ + 'Variation "control" is mapped to experiment "test_experiment" and ' + 'user "test_user" in the forced variation map' + ] self.assertEqual(expected_reasons, actual.reasons) @@ -1261,7 +1265,6 @@ def test_decide_reasons__whitelisted_variation(self): options = ['INCLUDE_REASONS'] actual = user_context.decide('test_feature_in_experiment', options) - expected_reasons = ['User "user_1" is forced in variation "control".'] self.assertEqual(expected_reasons, actual.reasons) @@ -1296,3 +1299,591 @@ def test_decide_experiment(self): user_context = opt_obj.create_user_context('test_user') decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) self.assertTrue(decision.enabled, "decision should be enabled") + + def test_forced_decision_return_status__invalid_datafile(self): + """ + Should return invalid status for invalid datafile in forced decision calls. + """ + opt_obj = optimizely.Optimizely(json.dumps("invalid datafile")) + user_context = opt_obj.create_user_context("test_user", {}) + + context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_rollout', None) + decision = OptimizelyUserContext.OptimizelyForcedDecision('211129') + + status = user_context.set_forced_decision(context, decision) + self.assertFalse(status) + status = user_context.get_forced_decision(context) + self.assertIsNone(status) + status = user_context.remove_forced_decision(context) + self.assertFalse(status) + status = user_context.remove_all_forced_decisions() + self.assertFalse(status) + + def test_forced_decision_return_status__valid_datafile(self): + """ + Should return valid status for valid datafile in forced decision calls. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_rollout', None) + decision = OptimizelyUserContext.OptimizelyForcedDecision('211129') + + status = user_context.set_forced_decision(context, decision) + self.assertTrue(status) + status = user_context.get_forced_decision(context) + self.assertEqual(status.variation_key, '211129') + status = user_context.remove_forced_decision(context) + self.assertTrue(status) + status = user_context.remove_all_forced_decisions() + self.assertTrue(status) + + def test_should_return_valid_decision_after_setting_and_removing_forced_decision(self): + """ + Should return valid forced decision after setting and removing forced decision. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + user_context = opt_obj.create_user_context("test_user", {}) + + context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_experiment', None) + decision = OptimizelyUserContext.OptimizelyForcedDecision('211129') + + status = user_context.set_forced_decision(context, decision) + self.assertTrue(status) + status = user_context.get_forced_decision(context) + self.assertEqual(status.variation_key, '211129') + + with mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'optimizely.optimizely.Optimizely._send_impression_event' + ) as mock_send_event: + decide_decision = user_context.decide('test_feature_in_experiment', ['INCLUDE_REASONS']) + + self.assertEqual(decide_decision.variation_key, 'control') + self.assertEqual(decide_decision.rule_key, 'test_experiment') + self.assertFalse(decide_decision.enabled) + self.assertEqual(decide_decision.flag_key, 'test_feature_in_experiment') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + self.assertEqual(decide_decision.reasons, [ + 'Invalid variation is mapped to flag (test_feature_in_experiment) ' + 'and user (test_user) in the forced decision map.', + 'Evaluating audiences for experiment "test_experiment": [].', + 'Audiences for experiment "test_experiment" collectively evaluated to TRUE.', + 'User "test_user" is in variation "control" of experiment test_experiment.']) + + expected_variables = { + 'is_working': True, + 'environment': 'devel', + 'cost': 10.99, + 'count': 999, + 'variable_without_usage': 45, + 'object': {'test': 12}, + 'true_object': {'true_test': 23.54} + } + + expected = OptimizelyDecision( + variation_key='control', + rule_key='test_experiment', + enabled=False, + variables=expected_variables, + flag_key='test_feature_in_experiment', + user_context=user_context, + reasons=['Invalid variation is mapped to flag (test_feature_in_experiment) ' + 'and user (test_user) in the forced decision map.', + 'Evaluating audiences for experiment "test_experiment": [].', + 'Audiences for experiment "test_experiment" collectively evaluated to TRUE.', + 'User "test_user" is in variation "control" of experiment test_experiment.'] + ) + + # assert notification count + self.assertEqual(1, mock_broadcast_decision.call_count) + + # assert notification + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'flag', + 'test_user', + {}, + { + 'flag_key': expected.flag_key, + 'enabled': expected.enabled, + 'variation_key': expected.variation_key, + 'rule_key': expected.rule_key, + 'reasons': expected.reasons, + 'decision_event_dispatched': True, + 'variables': expected.variables, + }, + ) + + expected_experiment = project_config.get_experiment_from_key(expected.rule_key) + expected_var = project_config.get_variation_from_key('test_experiment', expected.variation_key) + + mock_send_event.assert_called_with( + project_config, + expected_experiment, + expected_var, + expected.flag_key, + 'test_experiment', + 'feature-test', + expected.enabled, + 'test_user', + {} + ) + + self.assertTrue('User "test_user" is in variation "control" of experiment test_experiment.' + in decide_decision.reasons) + + status = user_context.remove_forced_decision(context) + self.assertTrue(status) + + decide_decision = user_context.decide('test_feature_in_experiment', ['INCLUDE_REASONS']) + + self.assertEqual(decide_decision.variation_key, 'control') + self.assertEqual(decide_decision.rule_key, 'test_experiment') + self.assertFalse(decide_decision.enabled) + self.assertEqual(decide_decision.flag_key, 'test_feature_in_experiment') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + + def test_should_return_valid_delivery_rule_decision_after_setting_forced_decision(self): + """ + Should return valid delivery rule decision after setting forced decision. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_experiment', None) + decision = OptimizelyUserContext.OptimizelyForcedDecision('211129') + + status = user_context.set_forced_decision(context, decision) + self.assertTrue(status) + status = user_context.get_forced_decision(context) + self.assertEqual(status.variation_key, '211129') + + decide_decision = user_context.decide('test_feature_in_experiment', ['INCLUDE_REASONS']) + self.assertEqual(decide_decision.variation_key, 'control') + self.assertEqual(decide_decision.rule_key, 'test_experiment') + self.assertFalse(decide_decision.enabled) + self.assertEqual(decide_decision.flag_key, 'test_feature_in_experiment') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + self.assertEqual(decide_decision.reasons, [ + 'Invalid variation is mapped to flag (test_feature_in_experiment) and user (test_user) in the ' + 'forced decision map.', 'Evaluating audiences for experiment "test_experiment": [].', + 'Audiences for experiment "test_experiment" collectively evaluated to TRUE.', + 'User "test_user" is in variation "control" of experiment test_experiment.']) + status = user_context.remove_forced_decision(context) + self.assertTrue(status) + + decide_decision = user_context.decide('test_feature_in_experiment', ['INCLUDE_REASONS']) + self.assertEqual(decide_decision.variation_key, 'control') + self.assertEqual(decide_decision.rule_key, 'test_experiment') + self.assertFalse(decide_decision.enabled) + self.assertEqual(decide_decision.flag_key, 'test_feature_in_experiment') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + + expected_reasons = [ + 'Evaluating audiences for experiment "test_experiment": [].', + 'Audiences for experiment "test_experiment" collectively evaluated to TRUE.', + 'User "test_user" is in variation "control" of experiment test_experiment.'] + self.assertEqual(decide_decision.reasons, expected_reasons) + + def test_should_return_valid_experiment_decision_after_setting_forced_decision(self): + """ + Should return valid experiment decision after setting forced decision. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_experiment_and_rollout', + 'group_exp_2') + decision = OptimizelyUserContext.OptimizelyForcedDecision('group_exp_2_variation') + + status = user_context.set_forced_decision(context, decision) + self.assertTrue(status) + status = user_context.get_forced_decision(context) + self.assertEqual(status.variation_key, 'group_exp_2_variation') + + decide_decision = user_context.decide('test_feature_in_experiment_and_rollout', ['INCLUDE_REASONS']) + self.assertEqual(decide_decision.variation_key, 'group_exp_2_variation') + self.assertEqual(decide_decision.rule_key, 'group_exp_2') + self.assertFalse(decide_decision.enabled) + self.assertEqual(decide_decision.flag_key, 'test_feature_in_experiment_and_rollout') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + self.assertTrue(set(decide_decision.reasons).issuperset(set([ + 'Variation (group_exp_2_variation) is mapped to flag ' + '(test_feature_in_experiment_and_rollout), rule (group_exp_2) and ' + 'user (test_user) in the forced decision map.' + ]))) + status = user_context.remove_forced_decision(context) + self.assertTrue(status) + + decide_decision = user_context.decide('test_feature_in_experiment_and_rollout', ['INCLUDE_REASONS']) + self.assertEqual(decide_decision.variation_key, 'group_exp_2_control') + self.assertEqual(decide_decision.rule_key, 'group_exp_2') + self.assertFalse(decide_decision.enabled) + self.assertEqual(decide_decision.flag_key, 'test_feature_in_experiment_and_rollout') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + + expected_reasons = [ + 'Evaluating audiences for experiment "group_exp_2": [].', + 'Audiences for experiment "group_exp_2" collectively evaluated to TRUE.', + 'User "test_user" is in experiment group_exp_2 of group 19228.', + 'User "test_user" is in variation "group_exp_2_control" of experiment group_exp_2.' + ] + + self.assertEqual(decide_decision.reasons, expected_reasons) + + def test_should_return_valid_decision_after_setting_invalid_delivery_rule_variation_in_forced_decision(self): + """ + Should return valid decision after setting invalid delivery rule variation in forced decision. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_rollout', '211127') + decision = OptimizelyUserContext.OptimizelyForcedDecision('invalid') + + status = user_context.set_forced_decision(context, decision) + self.assertTrue(status) + status = user_context.get_forced_decision(context) + self.assertEqual(status.variation_key, 'invalid') + + decide_decision = user_context.decide('test_feature_in_rollout', ['INCLUDE_REASONS']) + + self.assertEqual(decide_decision.variation_key, None) + self.assertEqual(decide_decision.rule_key, None) + self.assertFalse(decide_decision.enabled) + self.assertEqual(decide_decision.flag_key, 'test_feature_in_rollout') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + self.assertTrue(set(decide_decision.reasons).issuperset(set([ + 'Invalid variation is mapped to flag (test_feature_in_rollout), ' + 'rule (211127) and user (test_user) in the forced decision map.' + ]))) + + def test_should_return_valid_decision_after_setting_invalid_experiment_rule_variation_in_forced_decision(self): + """ + Should return valid decision after setting invalid experiment rule variation in forced decision. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_experiment', + 'test_experiment') + decision = OptimizelyUserContext.OptimizelyForcedDecision('invalid') + + status = user_context.set_forced_decision(context, decision) + self.assertTrue(status) + status = user_context.get_forced_decision(context) + self.assertEqual(status.variation_key, 'invalid') + + decide_decision = user_context.decide('test_feature_in_experiment', ['INCLUDE_REASONS']) + self.assertEqual(decide_decision.variation_key, 'control') + self.assertEqual(decide_decision.rule_key, 'test_experiment') + self.assertFalse(decide_decision.enabled) + + self.assertEqual(decide_decision.flag_key, 'test_feature_in_experiment') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + + expected_reasons = [ + 'Invalid variation is mapped to flag (test_feature_in_experiment), rule (test_experiment) ' + 'and user (test_user) in the forced decision map.', + 'Evaluating audiences for experiment "test_experiment": [].', + 'Audiences for experiment "test_experiment" collectively evaluated to TRUE.', + 'User "test_user" is in variation "control" of experiment test_experiment.' + ] + + self.assertEqual(decide_decision.reasons, expected_reasons) + + def test_conflicts_return_valid_decision__forced_decision(self): + """ + Should return valid forced decision after setting conflicting forced decisions. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context_with_flag = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_rollout', None) + decision_for_flag = OptimizelyUserContext.OptimizelyForcedDecision('211129') + + context_with_rule = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_rollout', '211127') + decision_for_rule = OptimizelyUserContext.OptimizelyForcedDecision('211229') + + status = user_context.set_forced_decision(context_with_flag, decision_for_flag) + self.assertTrue(status) + + status = user_context.set_forced_decision(context_with_rule, decision_for_rule) + self.assertTrue(status) + + decide_decision = user_context.decide('test_feature_in_rollout', ['INCLUDE_REASONS']) + self.assertEqual(decide_decision.variation_key, '211129') + self.assertIsNone(decide_decision.rule_key) + self.assertTrue(decide_decision.enabled) + self.assertEqual(decide_decision.flag_key, 'test_feature_in_rollout') + self.assertEqual(decide_decision.user_context.user_id, 'test_user') + self.assertEqual(decide_decision.user_context.get_user_attributes(), {}) + self.assertTrue(set(decide_decision.reasons).issuperset(set([ + 'Variation (211129) is mapped to flag (test_feature_in_rollout) and ' + 'user (test_user) in the forced decision map.' + ]))) + + def test_get_forced_decision_return_valid_decision__forced_decision(self): + """ + Should return valid forced decision on getting forced decision. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context_with_flag_1 = OptimizelyUserContext.OptimizelyDecisionContext('f1', None) + decision_for_flag_1 = OptimizelyUserContext.OptimizelyForcedDecision('v1') + + context_with_flag_2 = OptimizelyUserContext.OptimizelyDecisionContext('f1', None) + decision_for_flag_2 = OptimizelyUserContext.OptimizelyForcedDecision('v2') + status = user_context.set_forced_decision(context_with_flag_1, decision_for_flag_1) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_flag_1) + self.assertEqual(status.variation_key, decision_for_flag_1.variation_key) + + status = user_context.set_forced_decision(context_with_flag_2, decision_for_flag_2) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_flag_2) + self.assertEqual(status.variation_key, decision_for_flag_2.variation_key) + + context_with_rule_1 = OptimizelyUserContext.OptimizelyDecisionContext('f1', 'r1') + decision_for_rule_1 = OptimizelyUserContext.OptimizelyForcedDecision('v3') + + context_with_rule_2 = OptimizelyUserContext.OptimizelyDecisionContext('f1', 'r2') + decision_for_rule_2 = OptimizelyUserContext.OptimizelyForcedDecision('v4') + + status = user_context.set_forced_decision(context_with_rule_1, decision_for_rule_1) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_rule_1) + self.assertEqual(status.variation_key, decision_for_rule_1.variation_key) + + status = user_context.set_forced_decision(context_with_rule_2, decision_for_rule_2) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_rule_2) + self.assertEqual(status.variation_key, decision_for_rule_2.variation_key) + + status = user_context.get_forced_decision(context_with_flag_1) + self.assertEqual(status.variation_key, decision_for_flag_2.variation_key) + + def test_remove_forced_decision_return_valid_decision__forced_decision(self): + """ + Should remove forced decision on removing forced decision. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context_with_flag_1 = OptimizelyUserContext.OptimizelyDecisionContext('f1', None) + decision_for_flag_1 = OptimizelyUserContext.OptimizelyForcedDecision('v1') + + status = user_context.set_forced_decision(context_with_flag_1, decision_for_flag_1) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_flag_1) + self.assertEqual(status.variation_key, decision_for_flag_1.variation_key) + + status = user_context.remove_forced_decision(context_with_flag_1) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_flag_1) + self.assertIsNone(status) + + context_with_rule_1 = OptimizelyUserContext.OptimizelyDecisionContext('f1', 'r1') + decision_for_rule_1 = OptimizelyUserContext.OptimizelyForcedDecision('v3') + + status = user_context.set_forced_decision(context_with_rule_1, decision_for_rule_1) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_rule_1) + self.assertEqual(status.variation_key, decision_for_rule_1.variation_key) + + status = user_context.remove_forced_decision(context_with_rule_1) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_rule_1) + self.assertIsNone(status) + + status = user_context.get_forced_decision(context_with_flag_1) + self.assertIsNone(status) + + def test_remove_all_forced_decision_return_valid_decision__forced_decision(self): + """ + Should remove all forced decision on removing all forced decision. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context_with_flag_1 = OptimizelyUserContext.OptimizelyDecisionContext('f1', None) + decision_for_flag_1 = OptimizelyUserContext.OptimizelyForcedDecision('v1') + + status = user_context.remove_all_forced_decisions() + self.assertTrue(status) + + status = user_context.set_forced_decision(context_with_flag_1, decision_for_flag_1) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_flag_1) + self.assertEqual(status.variation_key, decision_for_flag_1.variation_key) + + context_with_rule_1 = OptimizelyUserContext.OptimizelyDecisionContext('f1', 'r1') + decision_for_rule_1 = OptimizelyUserContext.OptimizelyForcedDecision('v3') + + status = user_context.set_forced_decision(context_with_rule_1, decision_for_rule_1) + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_rule_1) + self.assertEqual(status.variation_key, decision_for_rule_1.variation_key) + + status = user_context.remove_all_forced_decisions() + self.assertTrue(status) + + status = user_context.get_forced_decision(context_with_rule_1) + self.assertIsNone(status) + + status = user_context.get_forced_decision(context_with_flag_1) + self.assertIsNone(status) + + status = user_context.remove_all_forced_decisions() + self.assertTrue(status) + + def test_forced_decision_return_status(self): + """ + Should return valid status for a valid datafile in forced decision calls. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_rollout', None) + decision = OptimizelyUserContext.OptimizelyForcedDecision('211129') + + status = user_context.set_forced_decision(context, decision) + self.assertTrue(status) + status = user_context.get_forced_decision(context) + self.assertEqual(status.variation_key, '211129') + status = user_context.remove_forced_decision(context) + self.assertTrue(status) + status = user_context.remove_all_forced_decisions() + self.assertTrue(status) + + def test_forced_decision_clone_return_valid_forced_decision(self): + """ + Should return valid forced decision on cloning. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + + context_with_flag = OptimizelyUserContext.OptimizelyDecisionContext('f1', None) + decision_for_flag = OptimizelyUserContext.OptimizelyForcedDecision('v1') + context_with_rule = OptimizelyUserContext.OptimizelyDecisionContext('f1', 'r1') + decision_for_rule = OptimizelyUserContext.OptimizelyForcedDecision('v2') + context_with_empty_rule = OptimizelyUserContext.OptimizelyDecisionContext('f1', '') + decision_for_empty_rule = OptimizelyUserContext.OptimizelyForcedDecision('v3') + + user_context.set_forced_decision(context_with_flag, decision_for_flag) + user_context.set_forced_decision(context_with_rule, decision_for_rule) + user_context.set_forced_decision(context_with_empty_rule, decision_for_empty_rule) + + user_context_2 = user_context._clone() + self.assertEqual(user_context_2.user_id, 'test_user') + self.assertEqual(user_context_2.get_user_attributes(), {}) + self.assertIsNotNone(user_context_2.forced_decisions_map) + + self.assertEqual(user_context_2.get_forced_decision(context_with_flag).variation_key, 'v1') + self.assertEqual(user_context_2.get_forced_decision(context_with_rule).variation_key, 'v2') + self.assertEqual(user_context_2.get_forced_decision(context_with_empty_rule).variation_key, 'v3') + + context_with_rule = OptimizelyUserContext.OptimizelyDecisionContext('x', 'y') + decision_for_rule = OptimizelyUserContext.OptimizelyForcedDecision('z') + user_context.set_forced_decision(context_with_rule, decision_for_rule) + self.assertEqual(user_context.get_forced_decision(context_with_rule).variation_key, 'z') + self.assertIsNone(user_context_2.get_forced_decision(context_with_rule)) + + def test_forced_decision_sync_return_correct_number_of_calls(self): + """ + Should return valid number of call on running forced decision calls in thread. + """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + user_context = opt_obj.create_user_context("test_user", {}) + context_1 = OptimizelyUserContext.OptimizelyDecisionContext('f1', None) + decision_1 = OptimizelyUserContext.OptimizelyForcedDecision('v1') + context_2 = OptimizelyUserContext.OptimizelyDecisionContext('f2', None) + decision_2 = OptimizelyUserContext.OptimizelyForcedDecision('v1') + + with mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext.set_forced_decision' + ) as set_forced_decision_mock, mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext.get_forced_decision' + ) as get_forced_decision_mock, mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext.remove_forced_decision' + ) as remove_forced_decision_mock, mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext.remove_all_forced_decisions' + ) as remove_all_forced_decisions_mock, mock.patch( + 'optimizely.optimizely_user_context.OptimizelyUserContext._clone' + ) as clone_mock: + def set_forced_decision_loop(user_context, context, decision): + for x in range(100): + user_context.set_forced_decision(context, decision) + + def get_forced_decision_loop(user_context, context): + for x in range(100): + user_context.get_forced_decision(context) + + def remove_forced_decision_loop(user_context, context): + for x in range(100): + user_context.remove_forced_decision(context) + + def remove_all_forced_decisions_loop(user_context): + for x in range(100): + user_context.remove_all_forced_decisions() + + def clone_loop(user_context): + for x in range(100): + user_context._clone() + + set_thread_1 = threading.Thread(target=set_forced_decision_loop, args=(user_context, context_1, decision_1)) + set_thread_2 = threading.Thread(target=set_forced_decision_loop, args=(user_context, context_2, decision_2)) + set_thread_3 = threading.Thread(target=get_forced_decision_loop, args=(user_context, context_1)) + set_thread_4 = threading.Thread(target=get_forced_decision_loop, args=(user_context, context_2)) + set_thread_5 = threading.Thread(target=remove_forced_decision_loop, args=(user_context, context_1)) + set_thread_6 = threading.Thread(target=remove_forced_decision_loop, args=(user_context, context_2)) + set_thread_7 = threading.Thread(target=remove_all_forced_decisions_loop, args=(user_context,)) + set_thread_8 = threading.Thread(target=clone_loop, args=(user_context,)) + + # Starting the threads + set_thread_1.start() + set_thread_2.start() + set_thread_3.start() + set_thread_4.start() + set_thread_5.start() + set_thread_6.start() + set_thread_7.start() + set_thread_8.start() + + # Waiting for all the threads to finish executing + set_thread_1.join() + set_thread_2.join() + set_thread_3.join() + set_thread_4.join() + set_thread_5.join() + set_thread_6.join() + set_thread_7.join() + set_thread_8.join() + + self.assertEqual(200, set_forced_decision_mock.call_count) + self.assertEqual(200, get_forced_decision_mock.call_count) + self.assertEqual(200, remove_forced_decision_mock.call_count) + self.assertEqual(100, remove_all_forced_decisions_mock.call_count) + self.assertEqual(100, clone_mock.call_count)